fix: fixed callbacks issue

go wrapper test working now
This commit is contained in:
shash256 2025-04-20 00:04:24 +05:30
parent bd4c81c73d
commit f162630b63
16 changed files with 979 additions and 527 deletions

BIN
.DS_Store vendored

Binary file not shown.

View File

@ -1,21 +1,19 @@
import std/[locks, typetraits, tables] # Added tables
import std/typetraits
import system # for GC thread setup/teardown
import chronos
import results
import ../src/[reliability, reliability_utils, message]
type
CReliabilityManagerHandle* = pointer
type CReliabilityManagerHandle* = pointer
type
# Callback Types (Imported from C Header)
CEventType* {.importc: "CEventType", header: "bindings.h", pure.} = enum
EVENT_MESSAGE_READY = 1,
EVENT_MESSAGE_SENT = 2,
EVENT_MISSING_DEPENDENCIES = 3,
EVENT_MESSAGE_READY = 1
EVENT_MESSAGE_SENT = 2
EVENT_MISSING_DEPENDENCIES = 3
EVENT_PERIODIC_SYNC = 4
CEventCallback* = proc(handle: pointer, eventType: CEventType, data1: pointer, data2: pointer, data3: csize_t) {.cdecl.} # Use csize_t
CResult* {.importc: "CResult", header: "bindings.h", bycopy.} = object
is_ok*: bool
error_message*: cstring
@ -32,54 +30,68 @@ type
missing_deps*: ptr cstring
missing_deps_count*: csize_t
# --- Callback Registry ---
type
CallbackRegistry = Table[CReliabilityManagerHandle, CEventCallback]
var
callbackRegistry: CallbackRegistry
registryLock: Lock
initLock(registryLock)
# --- Memory Management Helpers ---
proc allocCString*(s: string): cstring {.inline, gcsafe.} =
if s.len == 0: return nil
if s.len == 0:
echo "[Nim Binding][allocCString] Allocating empty string"
return nil
result = cast[cstring](allocShared(s.len + 1))
copyMem(result, s.cstring, s.len + 1)
echo "[Nim Binding][allocCString] Allocated cstring at ",
cast[int](result), " for: ", s
proc allocSeqByte*(s: seq[byte]): (pointer, csize_t) {.inline, gcsafe.} =
if s.len == 0: return (nil, 0)
if s.len == 0:
echo "[Nim Binding][allocSeqByte] Allocating empty seq[byte]"
return (nil, 0)
let len = s.len
let bufferPtr = allocShared(len)
if len > 0:
copyMem(bufferPtr, cast[pointer](s[0].unsafeAddr), len.Natural)
echo "[Nim Binding][allocSeqByte] Allocated buffer at ",
cast[int](bufferPtr), " of length ", len
return (bufferPtr, len.csize_t)
proc allocSeqCString*(s: seq[string]): (ptr cstring, csize_t) {.inline, gcsafe, cdecl.} =
if s.len == 0: return (nil, 0)
proc allocSeqCString*(
s: seq[string]
): (ptr cstring, csize_t) {.inline, gcsafe, cdecl.} =
if s.len == 0:
echo "[Nim Binding][allocSeqCString] Allocating empty seq[string]"
return (nil, 0)
let count = s.len
# Allocate memory for 'count' cstring pointers, cast to ptr UncheckedArray
let arrPtr = cast[ptr UncheckedArray[cstring]](allocShared(count * sizeof(cstring)))
for i in 0..<count:
for i in 0 ..< count:
# Allocate each string and store its pointer in the array using unchecked array indexing
arrPtr[i] = allocCString(s[i])
echo "[Nim Binding][allocSeqCString] Allocated cstring for missingDep[",
i, "]: ", s[i], " at ", cast[int](arrPtr[i])
# Return pointer to the first element, cast back to ptr cstring
echo "[Nim Binding][allocSeqCString] Allocated array at ",
cast[int](arrPtr), " with count ", count
return (cast[ptr cstring](arrPtr), count.csize_t)
proc freeCString*(cs: cstring) {.inline, gcsafe.} =
if cs != nil: deallocShared(cs)
if cs != nil:
echo "[Nim Binding][freeCString] Freeing cstring at ", cast[int](cs)
deallocShared(cs)
proc freeSeqByte*(bufferPtr: pointer) {.inline, gcsafe, cdecl.} =
if bufferPtr != nil: deallocShared(bufferPtr)
if bufferPtr != nil:
echo "[Nim Binding][freeSeqByte] Freeing buffer at ", cast[int](bufferPtr)
deallocShared(bufferPtr)
# Corrected to accept ptr cstring
proc freeSeqCString*(arrPtr: ptr cstring, count: csize_t) {.inline, gcsafe, cdecl.} =
if arrPtr != nil:
echo "[Nim Binding][freeSeqCString] Freeing array at ",
cast[int](arrPtr), " with count ", count
# Cast to ptr UncheckedArray for proper iteration/indexing before freeing
let arr = cast[ptr UncheckedArray[cstring]](arrPtr)
for i in 0..<count:
for i in 0 ..< count:
echo "[Nim Binding][freeSeqCString] Freeing cstring[",
i, "] at ", cast[int](arr[i])
freeCString(arr[i]) # Free each individual cstring
deallocShared(arrPtr) # Free the array pointer itself
@ -95,42 +107,54 @@ proc toCResultErrStr*(errMsg: string): CResult =
CResult(is_ok: false, error_message: allocCString(errMsg))
# --- Callback Wrappers (Nim -> C) ---
# These wrappers call the single global Go callback relay.
# These wrappers retrieve the C callback info from the ReliabilityManager object.
proc nimMessageReadyCallback(rm: ReliabilityManager, messageId: MessageID) {.gcsafe.} =
setupForeignThreadGc() # Setup GC for this Go thread
defer:
tearDownForeignThreadGc() # Ensure teardown even if callback errors
proc nimMessageReadyCallback(rm: ReliabilityManager, messageId: MessageID) =
echo "[Nim Binding] nimMessageReadyCallback called for: ", messageId
let handle = cast[CReliabilityManagerHandle](rm)
var cb: CEventCallback
withLock registryLock:
if not callbackRegistry.hasKey(handle):
echo "[Nim Binding] No callback registered for handle: ", cast[int](handle)
return
cb = callbackRegistry[handle]
let handle = cast[CReliabilityManagerHandle](rm) # Still use handle for C side
let cb = rm.cCallback
# Pass handle, event type, and messageId (as data1)
if cb == nil:
echo "[Nim Binding] No C callback stored in handle: ", cast[int](handle)
return
# Pass handle, event type, and messageId (as data1), plus user_data
cb(handle, EVENT_MESSAGE_READY, cast[pointer](messageId.cstring), nil, 0)
proc nimMessageSentCallback(rm: ReliabilityManager, messageId: MessageID) =
proc nimMessageSentCallback(rm: ReliabilityManager, messageId: MessageID) {.gcsafe.} =
setupForeignThreadGc()
defer:
tearDownForeignThreadGc()
echo "[Nim Binding] nimMessageSentCallback called for: ", messageId
let handle = cast[CReliabilityManagerHandle](rm)
var cb: CEventCallback
withLock registryLock:
if not callbackRegistry.hasKey(handle):
echo "[Nim Binding] No callback registered for handle: ", cast[int](handle)
let cb = rm.cCallback
if cb == nil:
echo "[Nim Binding] No C callback stored in handle: ", cast[int](handle)
return
cb = callbackRegistry[handle]
cb(handle, EVENT_MESSAGE_SENT, cast[pointer](messageId.cstring), nil, 0)
proc nimMissingDependenciesCallback(rm: ReliabilityManager, messageId: MessageID, missingDeps: seq[MessageID]) =
echo "[Nim Binding] nimMissingDependenciesCallback called for: ", messageId, " with deps: ", $missingDeps
proc nimMissingDependenciesCallback(
rm: ReliabilityManager, messageId: MessageID, missingDeps: seq[MessageID]
) {.gcsafe.} =
setupForeignThreadGc()
defer:
tearDownForeignThreadGc()
echo "[Nim Binding] nimMissingDependenciesCallback called for: ",
messageId, " with deps: ", $missingDeps
let handle = cast[CReliabilityManagerHandle](rm)
var cb: CEventCallback
withLock registryLock:
if not callbackRegistry.hasKey(handle):
echo "[Nim Binding] No callback registered for handle: ", cast[int](handle)
let cb = rm.cCallback
if cb == nil:
echo "[Nim Binding] No C callback stored in handle: ", cast[int](handle)
return
cb = callbackRegistry[handle]
# Prepare data for the callback
var cDepsPtr: ptr cstring = nil
@ -142,24 +166,38 @@ proc nimMissingDependenciesCallback(rm: ReliabilityManager, messageId: MessageID
cDepsNim[i] = dep.cstring # Nim GC manages these cstrings via the seq
cDepsPtr = cast[ptr cstring](cDepsNim[0].addr)
cDepsCount = missingDeps.len.csize_t
# Ensure cDepsNim stays alive during the call if cDepsPtr points into it
# Using allocSeqCString might be safer if Go needs to hold onto the data.
# For now, assuming Go copies the data immediately during the callback.
cb(handle, EVENT_MISSING_DEPENDENCIES, cast[pointer](messageId.cstring), cast[pointer](cDepsPtr), cDepsCount)
cb(
handle,
EVENT_MISSING_DEPENDENCIES,
cast[pointer](messageId.cstring),
cast[pointer](cDepsPtr),
cDepsCount,
)
proc nimPeriodicSyncCallback(rm: ReliabilityManager) {.gcsafe.} =
setupForeignThreadGc()
defer:
tearDownForeignThreadGc()
proc nimPeriodicSyncCallback(rm: ReliabilityManager) =
echo "[Nim Binding] nimPeriodicSyncCallback called"
let handle = cast[CReliabilityManagerHandle](rm)
var cb: CEventCallback
withLock registryLock:
if not callbackRegistry.hasKey(handle):
echo "[Nim Binding] No callback registered for handle: ", cast[int](handle)
let cb = rm.cCallback
if cb == nil:
echo "[Nim Binding] No C callback stored in handle: ", cast[int](handle)
return
cb = callbackRegistry[handle]
cb(handle, EVENT_PERIODIC_SYNC, nil, nil, 0)
# --- Exported C Functions - Using Opaque Pointer ---
proc NewReliabilityManager*(channelIdCStr: cstring): CReliabilityManagerHandle {.exportc, dynlib, cdecl, gcsafe.} =
proc NewReliabilityManager*(
channelIdCStr: cstring
): CReliabilityManagerHandle {.exportc, dynlib, cdecl, gcsafe.} =
let channelId = $channelIdCStr
if channelId.len == 0:
echo "Error creating ReliabilityManager: Channel ID cannot be empty"
@ -167,12 +205,16 @@ proc NewReliabilityManager*(channelIdCStr: cstring): CReliabilityManagerHandle {
let rmResult = newReliabilityManager(channelId)
if rmResult.isOk:
let rm = rmResult.get()
# Assign anonymous procs that capture 'rm' and call the wrappers
# Ensure signatures match the non-gcsafe fields in ReliabilityManager
rm.onMessageReady = proc(msgId: MessageID) = nimMessageReadyCallback(rm, msgId)
rm.onMessageSent = proc(msgId: MessageID) = nimMessageSentCallback(rm, msgId)
rm.onMissingDependencies = proc(msgId: MessageID, deps: seq[MessageID]) = nimMissingDependenciesCallback(rm, msgId, deps)
rm.onPeriodicSync = proc() = nimPeriodicSyncCallback(rm)
rm.onMessageReady = proc(rmArg: ReliabilityManager, msgId: MessageID) {.gcsafe.} =
nimMessageReadyCallback(rmArg, msgId)
rm.onMessageSent = proc(rmArg: ReliabilityManager, msgId: MessageID) {.gcsafe.} =
nimMessageSentCallback(rmArg, msgId)
rm.onMissingDependencies = proc(
rmArg: ReliabilityManager, msgId: MessageID, deps: seq[MessageID]
) {.gcsafe.} =
nimMissingDependenciesCallback(rmArg, msgId, deps)
rm.onPeriodicSync = proc(rmArg: ReliabilityManager) {.gcsafe.} =
nimPeriodicSyncCallback(rmArg)
# Return the Nim ref object cast to the opaque pointer type
let handle = cast[CReliabilityManagerHandle](rm)
@ -182,21 +224,29 @@ proc NewReliabilityManager*(channelIdCStr: cstring): CReliabilityManagerHandle {
echo "Error creating ReliabilityManager: ", rmResult.error
return nil # Return nil pointer
proc CleanupReliabilityManager*(handle: CReliabilityManagerHandle) {.exportc, dynlib, cdecl.} =
proc CleanupReliabilityManager*(
handle: CReliabilityManagerHandle
) {.exportc, dynlib, cdecl.} =
let handlePtr = handle
echo "[Nim Binding][Cleanup] Called with handle: ", cast[int](handlePtr)
if handlePtr != nil:
# Go side should handle removing the handle from its registry.
# We just need to unref the Nim object.
# No need to interact with gEventCallback here.
# Cast opaque pointer back to Nim ref type
let rm = cast[ReliabilityManager](handlePtr)
cleanup(rm) # Call Nim cleanup
echo "[Nim Binding][Cleanup] Calling Nim core cleanup for handle: ",
cast[int](handlePtr)
cleanup(rm)
echo "[Nim Binding][Cleanup] Calling GC_unref for handle: ", cast[int](handlePtr)
GC_unref(rm) # Allow GC to collect the object now that Go is done
echo "[Nim Binding][Cleanup] GC_unref returned for handle: ", cast[int](handlePtr)
else:
echo "Warning: CleanupReliabilityManager called with NULL handle"
echo "[Nim Binding][Cleanup] Warning: CleanupReliabilityManager called with NULL handle"
proc ResetReliabilityManager*(handle: CReliabilityManagerHandle): CResult {.exportc, dynlib, cdecl, gcsafe.} =
proc ResetReliabilityManager*(
handle: CReliabilityManagerHandle
): CResult {.exportc, dynlib, cdecl, gcsafe.} =
if handle == nil:
return toCResultErrStr("ReliabilityManager handle is NULL")
let rm = cast[ReliabilityManager](handle)
@ -206,14 +256,27 @@ proc ResetReliabilityManager*(handle: CReliabilityManagerHandle): CResult {.expo
else:
return toCResultErr(result.error)
proc WrapOutgoingMessage*(handle: CReliabilityManagerHandle, messageC: pointer, messageLen: csize_t, messageIdCStr: cstring): CWrapResult {.exportc, dynlib, cdecl.} = # Keep non-gcsafe
proc WrapOutgoingMessage*(
handle: CReliabilityManagerHandle,
messageC: pointer,
messageLen: csize_t,
messageIdCStr: cstring,
): CWrapResult {.exportc, dynlib, cdecl.} = # Keep non-gcsafe
echo "[Nim Binding][WrapOutgoingMessage] Called with handle=",
cast[int](handle), " messageLen=", messageLen, " messageId=", $messageIdCStr
if handle == nil:
return CWrapResult(base_result: toCResultErrStr("ReliabilityManager handle is NULL"))
echo "[Nim Binding][WrapOutgoingMessage] Error: handle is nil"
return
CWrapResult(base_result: toCResultErrStr("ReliabilityManager handle is NULL"))
let rm = cast[ReliabilityManager](handle)
if messageC == nil and messageLen > 0:
return CWrapResult(base_result: toCResultErrStr("Message pointer is NULL but length > 0"))
echo "[Nim Binding][WrapOutgoingMessage] Error: message pointer is NULL but length > 0"
return CWrapResult(
base_result: toCResultErrStr("Message pointer is NULL but length > 0")
)
if messageIdCStr == nil:
echo "[Nim Binding][WrapOutgoingMessage] Error: messageId pointer is NULL"
return CWrapResult(base_result: toCResultErrStr("Message ID pointer is NULL"))
let messageId = $messageIdCStr
@ -227,21 +290,31 @@ proc WrapOutgoingMessage*(handle: CReliabilityManagerHandle, messageC: pointer,
let wrapResult = wrapOutgoingMessage(rm, messageNim, messageId)
if wrapResult.isOk:
let (wrappedDataPtr, wrappedDataLen) = allocSeqByte(wrapResult.get())
echo "[Nim Binding][WrapOutgoingMessage] Returning wrapped message at ",
cast[int](wrappedDataPtr), " len=", wrappedDataLen
return CWrapResult(
base_result: toCResultOk(),
message: wrappedDataPtr,
message_len: wrappedDataLen
base_result: toCResultOk(), message: wrappedDataPtr, message_len: wrappedDataLen
)
else:
echo "[Nim Binding][WrapOutgoingMessage] Error: ", $wrapResult.error
return CWrapResult(base_result: toCResultErr(wrapResult.error))
proc UnwrapReceivedMessage*(handle: CReliabilityManagerHandle, messageC: pointer, messageLen: csize_t): CUnwrapResult {.exportc, dynlib, cdecl.} = # Keep non-gcsafe
proc UnwrapReceivedMessage*(
handle: CReliabilityManagerHandle, messageC: pointer, messageLen: csize_t
): CUnwrapResult {.exportc, dynlib, cdecl.} =
echo "[Nim Binding][UnwrapReceivedMessage] Called with handle=",
cast[int](handle), " messageLen=", messageLen
if handle == nil:
return CUnwrapResult(base_result: toCResultErrStr("ReliabilityManager handle is NULL"))
echo "[Nim Binding][UnwrapReceivedMessage] Error: handle is nil"
return
CUnwrapResult(base_result: toCResultErrStr("ReliabilityManager handle is NULL"))
let rm = cast[ReliabilityManager](handle)
if messageC == nil and messageLen > 0:
return CUnwrapResult(base_result: toCResultErrStr("Message pointer is NULL but length > 0"))
echo "[Nim Binding][UnwrapReceivedMessage] Error: message pointer is NULL but length > 0"
return CUnwrapResult(
base_result: toCResultErrStr("Message pointer is NULL but length > 0")
)
var messageNim: seq[byte]
if messageLen > 0:
@ -255,46 +328,73 @@ proc UnwrapReceivedMessage*(handle: CReliabilityManagerHandle, messageC: pointer
let (unwrappedContent, missingDepsNim) = unwrapResult.get()
let (contentPtr, contentLen) = allocSeqByte(unwrappedContent)
let (depsPtr, depsCount) = allocSeqCString(missingDepsNim)
echo "[Nim Binding][UnwrapReceivedMessage] Returning content at ",
cast[int](contentPtr),
" len=",
contentLen,
" missingDepsPtr=",
cast[int](depsPtr),
" count=",
depsCount
return CUnwrapResult(
base_result: toCResultOk(),
message: contentPtr,
message_len: contentLen,
missing_deps: depsPtr,
missing_deps_count: depsCount
missing_deps_count: depsCount,
)
else:
echo "[Nim Binding][UnwrapReceivedMessage] Error: ", $unwrapResult.error
return CUnwrapResult(base_result: toCResultErr(unwrapResult.error))
proc MarkDependenciesMet*(handle: CReliabilityManagerHandle, messageIDsC: ptr cstring, count: csize_t): CResult {.exportc, dynlib, cdecl.} = # Keep non-gcsafe
proc MarkDependenciesMet*(
handle: CReliabilityManagerHandle, messageIDsC: ptr cstring, count: csize_t
): CResult {.exportc, dynlib, cdecl.} =
echo "[Nim Binding][MarkDependenciesMet] Called with handle=",
cast[int](handle), " count=", count
if handle == nil:
echo "[Nim Binding][MarkDependenciesMet] Error: handle is nil"
return toCResultErrStr("ReliabilityManager handle is NULL")
let rm = cast[ReliabilityManager](handle)
if messageIDsC == nil and count > 0:
echo "[Nim Binding][MarkDependenciesMet] Error: messageIDs pointer is NULL but count > 0"
return toCResultErrStr("MessageIDs pointer is NULL but count > 0")
var messageIDsNim = newSeq[string](count)
# Cast to ptr UncheckedArray for indexing
let messageIDsCArray = cast[ptr UncheckedArray[cstring]](messageIDsC)
for i in 0..<count:
for i in 0 ..< count:
let currentCStr = messageIDsCArray[i] # Use unchecked array indexing
if currentCStr != nil:
messageIDsNim[i] = $currentCStr
echo "[Nim Binding][MarkDependenciesMet] messageID[",
i, "] = ", messageIDsNim[i], " at ", cast[int](currentCStr)
else:
echo "[Nim Binding][MarkDependenciesMet] NULL message ID found in array at index ",
i
return toCResultErrStr("NULL message ID found in array")
let result = markDependenciesMet(rm, messageIDsNim)
if result.isOk:
echo "[Nim Binding][MarkDependenciesMet] Success"
return toCResultOk()
else:
echo "[Nim Binding][MarkDependenciesMet] Error: ", $result.error
return toCResultErr(result.error)
proc RegisterCallback*(handle: CReliabilityManagerHandle,
proc RegisterCallback*(
handle: CReliabilityManagerHandle,
cEventCallback: CEventCallback,
cUserDataPtr: pointer) {.exportc, dynlib, cdecl.} =
withLock registryLock:
callbackRegistry[handle] = cEventCallback
echo "[Nim Binding] Registered callback for handle: ", cast[int](handle)
cUserDataPtr: pointer,
) {.exportc, dynlib, cdecl, gcsafe.} =
if handle == nil:
echo "[Nim Binding][RegisterCallback] Error: handle is NULL"
return
let rm = cast[ReliabilityManager](handle)
rm.cCallback = cEventCallback
rm.cUserData = cUserDataPtr # Store user data pointer
echo "[Nim Binding] Stored C callback and user data for handle: ", cast[int](handle)
proc StartPeriodicTasks*(handle: CReliabilityManagerHandle) {.exportc, dynlib, cdecl.} =
if handle == nil:

Binary file not shown.

View File

@ -17,7 +17,8 @@ task test, "Run the test suite":
task bindings, "Generate bindings":
proc compile(libName: string, flags = "") =
exec "nim c -f " & flags & " -d:release --app:lib --mm:arc --tlsEmulation:off --out:" & libName & " --outdir:bindings/generated bindings/bindings.nim"
exec "nim c -f " & flags & " -d:release --app:lib --mm:arc --tlsEmulation:off --out:" &
libName & " --outdir:bindings/generated bindings/bindings.nim"
# Create required directories
mkDir "bindings/generated"
@ -25,8 +26,10 @@ task bindings, "Generate bindings":
when defined(windows):
compile "reliability.dll"
elif defined(macosx):
compile "libsds.dylib.arm", "--cpu:arm64 -l:'-target arm64-apple-macos11' -t:'-target arm64-apple-macos11'"
compile "libsds.dylib.x64", "--cpu:amd64 -l:'-target x86_64-apple-macos10.12' -t:'-target x86_64-apple-macos10.12'"
compile "libsds.dylib.arm",
"--cpu:arm64 -l:'-target arm64-apple-macos11' -t:'-target arm64-apple-macos11'"
compile "libsds.dylib.x64",
"--cpu:amd64 -l:'-target x86_64-apple-macos10.12' -t:'-target x86_64-apple-macos10.12'"
exec "lipo bindings/generated/libsds.dylib.arm bindings/generated/libsds.dylib.x64 -output bindings/generated/libsds.dylib -create"
else:
compile "libsds.so"

View File

@ -64,13 +64,14 @@ func NewReliabilityManager(channelId string) (ReliabilityManagerHandle, error) {
// CleanupReliabilityManager frees the resources associated with the handle
func CleanupReliabilityManager(handle ReliabilityManagerHandle) {
fmt.Printf("Go: CleanupReliabilityManager called for handle %p\n", handle) // Log entry
if handle == nil {
fmt.Println("Go: CleanupReliabilityManager: handle is nil, returning.")
return
}
registryMutex.Lock()
delete(callbackRegistry, handle)
registryMutex.Unlock()
fmt.Printf("Go: CleanupReliabilityManager: Calling C.CleanupReliabilityManager for handle %p\n", handle)
C.CleanupReliabilityManager(unsafe.Pointer(handle))
fmt.Printf("Go: CleanupReliabilityManager: C.CleanupReliabilityManager returned for handle %p\n", handle) // Log exit
}
// ResetReliabilityManager resets the state of the manager
@ -212,7 +213,7 @@ func RegisterCallback(handle ReliabilityManagerHandle, callbacks Callbacks) erro
C.RegisterCallback(
unsafe.Pointer(handle),
(C.CEventCallback)(C.globalCallbackRelay), // Pass the Go relay function pointer
nil, // user_data is not used here, handle is passed directly by Nim wrapper
nil,
)
return nil
}
@ -229,6 +230,7 @@ func StartPeriodicTasks(handle ReliabilityManagerHandle) error {
// globalCallbackRelay is called by Nim for all events.
// It uses the handle to find the correct Go Callbacks struct and dispatch the call.
//export globalCallbackRelay
func globalCallbackRelay(handle unsafe.Pointer, eventType C.CEventType, data1 unsafe.Pointer, data2 unsafe.Pointer, data3 C.size_t) {
goHandle := ReliabilityManagerHandle(handle)
@ -238,12 +240,10 @@ func globalCallbackRelay(handle unsafe.Pointer, eventType C.CEventType, data1 un
registryMutex.RUnlock()
if !ok || callbacks == nil {
fmt.Printf("Go: globalCallbackRelay: No callbacks registered for handle %v\n", goHandle) // Uncommented
fmt.Printf("Go: globalCallbackRelay: No callbacks registered for handle %v\n", goHandle)
return
}
// Use a goroutine to avoid blocking the Nim thread
go func() {
switch eventType {
case C.EVENT_MESSAGE_READY:
if callbacks.OnMessageReady != nil {
@ -276,5 +276,4 @@ func globalCallbackRelay(handle unsafe.Pointer, eventType C.CEventType, data1 un
default:
fmt.Printf("Go: globalCallbackRelay: Received unknown event type %d for handle %v\n", eventType, goHandle)
}
}()
}

View File

@ -24,6 +24,28 @@ func TestLifecycle(t *testing.T) {
}
}
// Test that consecutive calls return unique handles
func TestHandleUniqueness(t *testing.T) {
channelID := "test-unique-handles"
handle1, err1 := NewReliabilityManager(channelID)
if err1 != nil || handle1 == nil {
t.Fatalf("NewReliabilityManager (1) failed: %v", err1)
}
defer CleanupReliabilityManager(handle1)
t.Logf("Handle 1: %p", handle1)
handle2, err2 := NewReliabilityManager(channelID)
if err2 != nil || handle2 == nil {
t.Fatalf("NewReliabilityManager (2) failed: %v", err2)
}
defer CleanupReliabilityManager(handle2)
t.Logf("Handle 2: %p", handle2)
if handle1 == handle2 {
t.Errorf("Expected unique handles, but both are %p", handle1)
}
}
// Test wrapping and unwrapping a simple message
func TestWrapUnwrap(t *testing.T) {
channelID := "test-wrap-unwrap"
@ -122,50 +144,279 @@ func TestDependencies(t *testing.T) {
}
}
// Test callbacks
func TestCallbacks(t *testing.T) {
channelID := "test-callbacks"
// Test OnMessageReady callback
func TestCallback_OnMessageReady(t *testing.T) {
channelID := "test-cb-ready"
// Create sender and receiver handles
handleSender, err := NewReliabilityManager(channelID)
if err != nil {
t.Fatalf("NewReliabilityManager (sender) failed: %v", err)
}
defer CleanupReliabilityManager(handleSender)
handleReceiver, err := NewReliabilityManager(channelID)
if err != nil {
t.Fatalf("NewReliabilityManager (receiver) failed: %v", err)
}
defer CleanupReliabilityManager(handleReceiver)
// Use a channel for signaling instead of WaitGroup
readyChan := make(chan MessageID, 1)
callbacks := Callbacks{
OnMessageReady: func(messageId MessageID) {
fmt.Printf("Test_OnMessageReady: Received: %s\n", messageId)
// Non-blocking send to channel
select {
case readyChan <- messageId:
fmt.Printf("Test_OnMessageReady: Sent '%s' to readyChan\n", messageId) // Log after send
default:
// Avoid blocking if channel is full or test already timed out
fmt.Printf("Test_OnMessageReady: Warning - readyChan buffer full or test finished for %s\n", messageId)
}
},
}
// Register callback only on the receiver handle
err = RegisterCallback(handleReceiver, callbacks)
if err != nil {
t.Fatalf("RegisterCallback failed: %v", err)
}
// Scenario: Wrap message on sender, unwrap on receiver
payload := []byte("ready test")
msgID := MessageID("cb-ready-1")
// Wrap on sender
t.Logf("Test_OnMessageReady: Wrapping message with handleSender: %p", handleSender) // Log sender handle
wrappedMsg, err := WrapOutgoingMessage(handleSender, payload, msgID)
if err != nil {
t.Fatalf("WrapOutgoingMessage failed: %v", err)
}
// Unwrap on receiver
t.Logf("Test_OnMessageReady: Unwrapping message with handleReceiver: %p", handleReceiver) // Log receiver handle
_, _, err = UnwrapReceivedMessage(handleReceiver, wrappedMsg)
if err != nil {
t.Fatalf("UnwrapReceivedMessage failed: %v", err)
}
// Verification - Wait on channel with timeout
select {
case receivedMsgID := <-readyChan:
// Mark as called implicitly since we received on channel
if receivedMsgID != msgID {
t.Errorf("OnMessageReady called with wrong ID: got %q, want %q", receivedMsgID, msgID)
}
case <-time.After(2 * time.Second):
// If timeout occurs, the channel receive failed.
t.Errorf("Timed out waiting for OnMessageReady callback on readyChan")
}
}
// Test OnMessageSent callback (via causal history ACK)
func TestCallback_OnMessageSent(t *testing.T) {
channelID := "test-cb-sent"
// Create two handles
handle1, err := NewReliabilityManager(channelID)
if err != nil {
t.Fatalf("NewReliabilityManager (1) failed: %v", err)
}
defer CleanupReliabilityManager(handle1)
handle2, err := NewReliabilityManager(channelID)
if err != nil {
t.Fatalf("NewReliabilityManager (2) failed: %v", err)
}
defer CleanupReliabilityManager(handle2)
var wg sync.WaitGroup
sentCalled := false
var sentMsgID MessageID
var cbMutex sync.Mutex
callbacks := Callbacks{
OnMessageSent: func(messageId MessageID) {
fmt.Printf("Test_OnMessageSent: Received: %s\n", messageId)
cbMutex.Lock()
sentCalled = true
sentMsgID = messageId
cbMutex.Unlock()
wg.Done()
},
}
// Register callback on handle1 (the original sender)
err = RegisterCallback(handle1, callbacks)
if err != nil {
t.Fatalf("RegisterCallback failed: %v", err)
}
// Scenario: handle1 sends msg1, handle2 receives msg1,
// handle2 sends msg2 (acking msg1), handle1 receives msg2.
// 1. handle1 sends msg1
payload1 := []byte("sent test 1")
msgID1 := MessageID("cb-sent-1")
wrappedMsg1, err := WrapOutgoingMessage(handle1, payload1, msgID1)
if err != nil {
t.Fatalf("WrapOutgoingMessage (1) failed: %v", err)
}
// Note: msg1 is now in handle1's outgoing buffer
// 2. handle2 receives msg1 (to update its state)
_, _, err = UnwrapReceivedMessage(handle2, wrappedMsg1)
if err != nil {
t.Fatalf("UnwrapReceivedMessage (1) on handle2 failed: %v", err)
}
// 3. handle2 sends msg2 (will include msg1 in causal history)
payload2 := []byte("sent test 2")
msgID2 := MessageID("cb-sent-2")
wrappedMsg2, err := WrapOutgoingMessage(handle2, payload2, msgID2)
if err != nil {
t.Fatalf("WrapOutgoingMessage (2) on handle2 failed: %v", err)
}
// 4. handle1 receives msg2 (should trigger ACK for msg1)
wg.Add(1) // Expect OnMessageSent for msg1 on handle1
_, _, err = UnwrapReceivedMessage(handle1, wrappedMsg2)
if err != nil {
t.Fatalf("UnwrapReceivedMessage (2) on handle1 failed: %v", err)
}
// Verification
waitTimeout(&wg, 2*time.Second, t)
cbMutex.Lock()
defer cbMutex.Unlock()
if !sentCalled {
t.Errorf("OnMessageSent was not called")
}
// We primarily care that msg1 was ACKed.
if sentMsgID != msgID1 {
t.Errorf("OnMessageSent called with wrong ID: got %q, want %q", sentMsgID, msgID1)
}
}
// Test OnMissingDependencies callback
func TestCallback_OnMissingDependencies(t *testing.T) {
channelID := "test-cb-missing"
// Use separate sender/receiver handles explicitly
handleSender, err := NewReliabilityManager(channelID)
if err != nil {
t.Fatalf("NewReliabilityManager (sender) failed: %v", err)
}
defer CleanupReliabilityManager(handleSender)
handleReceiver, err := NewReliabilityManager(channelID)
if err != nil {
t.Fatalf("NewReliabilityManager (receiver) failed: %v", err)
}
defer CleanupReliabilityManager(handleReceiver)
var wg sync.WaitGroup
missingCalled := false
var missingMsgID MessageID
var missingDepsList []MessageID
var cbMutex sync.Mutex
callbacks := Callbacks{
OnMissingDependencies: func(messageId MessageID, missingDeps []MessageID) {
fmt.Printf("Test_OnMissingDependencies: Received for %s: %v\n", messageId, missingDeps)
cbMutex.Lock()
missingCalled = true
missingMsgID = messageId
missingDepsList = missingDeps // Copy slice
cbMutex.Unlock()
wg.Done()
},
}
// Register callback only on the receiver handle
err = RegisterCallback(handleReceiver, callbacks)
if err != nil {
t.Fatalf("RegisterCallback failed: %v", err)
}
// Scenario: Sender sends msg1, then sender sends msg2 (depends on msg1),
// then receiver receives msg2 (which hasn't seen msg1).
// 1. Sender sends msg1
payload1 := []byte("missing test 1")
msgID1 := MessageID("cb-miss-1")
_, err = WrapOutgoingMessage(handleSender, payload1, msgID1) // Assign to _
if err != nil {
t.Fatalf("WrapOutgoingMessage (1) on sender failed: %v", err)
}
// _, _, err = UnwrapReceivedMessage(handleSender, wrappedMsg1) // No need to unwrap on sender
// 2. Sender sends msg2 (depends on msg1)
payload2 := []byte("missing test 2")
msgID2 := MessageID("cb-miss-2")
wrappedMsg2, err := WrapOutgoingMessage(handleSender, payload2, msgID2)
if err != nil {
t.Fatalf("WrapOutgoingMessage (2) failed: %v", err)
}
// 3. Receiver receives msg2 (haven't seen msg1)
wg.Add(1) // Expect OnMissingDependencies
_, _, err = UnwrapReceivedMessage(handleReceiver, wrappedMsg2)
if err != nil {
t.Fatalf("UnwrapReceivedMessage (2) on receiver failed: %v", err)
}
// Verification
waitTimeout(&wg, 2*time.Second, t)
cbMutex.Lock()
defer cbMutex.Unlock()
if !missingCalled {
t.Errorf("OnMissingDependencies was not called")
}
if missingMsgID != msgID2 {
t.Errorf("OnMissingDependencies called for wrong ID: got %q, want %q", missingMsgID, msgID2)
}
foundDep := false
for _, dep := range missingDepsList {
if dep == msgID1 {
foundDep = true
break
}
}
if !foundDep {
t.Errorf("OnMissingDependencies did not report %q as missing, got: %v", msgID1, missingDepsList)
}
}
// Test OnPeriodicSync callback
func TestCallback_OnPeriodicSync(t *testing.T) {
channelID := "test-cb-sync"
handle, err := NewReliabilityManager(channelID)
if err != nil {
t.Fatalf("NewReliabilityManager failed: %v", err)
}
defer CleanupReliabilityManager(handle)
var wg sync.WaitGroup
receivedReady := make(map[MessageID]bool)
receivedSent := make(map[MessageID]bool)
receivedMissing := make(map[MessageID][]MessageID)
syncRequested := false
var cbMutex sync.Mutex // Protect access to callback tracking maps/vars
syncCalled := false
var cbMutex sync.Mutex
// Use a channel to signal when the callback is hit, as WaitGroup isn't ideal for periodic calls
syncChan := make(chan bool, 1)
callbacks := Callbacks{
OnMessageReady: func(messageId MessageID) {
fmt.Printf("Test: OnMessageReady received: %s\n", messageId)
cbMutex.Lock()
receivedReady[messageId] = true
cbMutex.Unlock()
wg.Done()
},
OnMessageSent: func(messageId MessageID) {
fmt.Printf("Test: OnMessageSent received: %s\n", messageId)
cbMutex.Lock()
receivedSent[messageId] = true
cbMutex.Unlock()
wg.Done()
},
OnMissingDependencies: func(messageId MessageID, missingDeps []MessageID) {
fmt.Printf("Test: OnMissingDependencies received for %s: %v\n", messageId, missingDeps)
cbMutex.Lock()
receivedMissing[messageId] = missingDeps
cbMutex.Unlock()
wg.Done()
},
OnPeriodicSync: func() {
fmt.Println("Test: OnPeriodicSync received")
fmt.Println("Test_OnPeriodicSync: Received")
cbMutex.Lock()
syncRequested = true
if !syncCalled { // Only signal the first time
syncCalled = true
syncChan <- true
}
cbMutex.Unlock()
// Don't wg.Done() here, it might be called multiple times
},
}
@ -174,71 +425,27 @@ func TestCallbacks(t *testing.T) {
t.Fatalf("RegisterCallback failed: %v", err)
}
// Start tasks AFTER registering callbacks
// Start periodic tasks
err = StartPeriodicTasks(handle)
if err != nil {
t.Fatalf("StartPeriodicTasks failed: %v", err)
}
// --- Test Scenario ---
// 1. Send msg1
wg.Add(1) // Expect OnMessageSent for msg1 eventually
payload1 := []byte("callback test 1")
msgID1 := MessageID("cb-msg-1")
wrappedMsg1, err := WrapOutgoingMessage(handle, payload1, msgID1)
if err != nil {
t.Fatalf("WrapOutgoingMessage (1) failed: %v", err)
}
// 2. Receive msg1 (triggers OnMessageReady for msg1, OnMessageSent for msg1 via causal history)
wg.Add(1) // Expect OnMessageReady for msg1
_, _, err = UnwrapReceivedMessage(handle, wrappedMsg1)
if err != nil {
t.Fatalf("UnwrapReceivedMessage (1) failed: %v", err)
}
// 3. Send msg2 (depends on msg1)
wg.Add(1) // Expect OnMessageSent for msg2 eventually
payload2 := []byte("callback test 2")
msgID2 := MessageID("cb-msg-2")
wrappedMsg2, err := WrapOutgoingMessage(handle, payload2, msgID2)
if err != nil {
t.Fatalf("WrapOutgoingMessage (2) failed: %v", err)
}
// 4. Receive msg2 (triggers OnMessageReady for msg2, OnMessageSent for msg2)
wg.Add(1) // Expect OnMessageReady for msg2
_, _, err = UnwrapReceivedMessage(handle, wrappedMsg2)
if err != nil {
t.Fatalf("UnwrapReceivedMessage (2) failed: %v", err)
}
// --- Verification ---
// Wait for expected callbacks with a timeout
waitTimeout(&wg, 5*time.Second, t)
// Wait for the periodic sync callback with a timeout (needs to be longer than sync interval)
// Default sync interval is 30s, which is too long for a unit test.
// We rely on the periodic tasks starting quickly and triggering the callback soon.
select {
case <-syncChan:
// Success
case <-time.After(10 * time.Second):
t.Errorf("Timed out waiting for OnPeriodicSync callback")
}
cbMutex.Lock()
defer cbMutex.Unlock()
if !receivedReady[msgID1] {
t.Errorf("OnMessageReady not called for %s", msgID1)
}
if !receivedReady[msgID2] {
t.Errorf("OnMessageReady not called for %s", msgID2)
}
if !receivedSent[msgID1] {
t.Errorf("OnMessageSent not called for %s", msgID1)
}
if !receivedSent[msgID2] {
t.Errorf("OnMessageSent not called for %s", msgID2)
}
// We didn't explicitly test missing deps in this path
if len(receivedMissing) > 0 {
t.Errorf("Unexpected OnMissingDependencies calls: %v", receivedMissing)
}
// Periodic sync is harder to guarantee in a short test, just check if it was ever true
if !syncRequested {
if !syncCalled {
// This might happen if the timeout was too short
t.Logf("Warning: OnPeriodicSync might not have been called within the test timeout")
}
}

View File

@ -4,8 +4,7 @@ import strutils
import results
import private/probabilities
type
BloomFilter* = object
type BloomFilter* = object
capacity*: int
errorRate*: float
kHashes*: int
@ -31,20 +30,24 @@ proc hashN(item: string, n: int, maxValue: int): int =
{.pop.}
proc getMOverNBitsForK*(k: int, targetError: float,
probabilityTable = kErrors): Result[int, string] =
proc getMOverNBitsForK*(
k: int, targetError: float, probabilityTable = kErrors
): Result[int, string] =
## Returns the optimal number of m/n bits for a given k.
if k notin 0..12:
if k notin 0 .. 12:
return err("K must be <= 12 if forceNBitsPerElem is not also specified.")
for mOverN in 2..probabilityTable[k].high:
for mOverN in 2 .. probabilityTable[k].high:
if probabilityTable[k][mOverN] < targetError:
return ok(mOverN)
err("Specified value of k and error rate not achievable using less than 4 bytes / element.")
err(
"Specified value of k and error rate not achievable using less than 4 bytes / element."
)
proc initializeBloomFilter*(capacity: int, errorRate: float, k = 0,
forceNBitsPerElem = 0): Result[BloomFilter, string] =
proc initializeBloomFilter*(
capacity: int, errorRate: float, k = 0, forceNBitsPerElem = 0
): Result[BloomFilter, string] =
## Initializes a Bloom filter with specified parameters.
##
## Parameters:
@ -76,25 +79,29 @@ proc initializeBloomFilter*(capacity: int, errorRate: float, k = 0,
mBits = capacity * nBitsPerElem
mInts = 1 + mBits div (sizeof(int) * 8)
ok(BloomFilter(
ok(
BloomFilter(
capacity: capacity,
errorRate: errorRate,
kHashes: kHashes,
mBits: mBits,
intArray: newSeq[int](mInts)
))
intArray: newSeq[int](mInts),
)
)
proc `$`*(bf: BloomFilter): string =
## Prints the configuration of the Bloom filter.
"Bloom filter with $1 capacity, $2 error rate, $3 hash functions, and requiring $4 bits of memory." %
[$bf.capacity,
[
$bf.capacity,
formatFloat(bf.errorRate, format = ffScientific, precision = 1),
$bf.kHashes,
$(bf.mBits div bf.capacity)]
$(bf.mBits div bf.capacity),
]
proc computeHashes(bf: BloomFilter, item: string): seq[int] =
var hashes = newSeq[int](bf.kHashes)
for i in 0..<bf.kHashes:
for i in 0 ..< bf.kHashes:
hashes[i] = hashN(item, i, bf.mBits)
hashes

View File

@ -7,94 +7,97 @@
type
TErrorForK = seq[float]
TAllErrorRates* = array[0..12, TErrorForK]
TAllErrorRates* = array[0 .. 12, TErrorForK]
var kErrors* {.threadvar.}: TAllErrorRates
kErrors = [
@[1.0],
@[1.0, 1.0, 0.3930000000, 0.2830000000, 0.2210000000, 0.1810000000,
0.1540000000, 0.1330000000, 0.1180000000, 0.1050000000, 0.0952000000,
0.0869000000, 0.0800000000, 0.0740000000, 0.0689000000, 0.0645000000,
0.0606000000, 0.0571000000, 0.0540000000, 0.0513000000, 0.0488000000,
0.0465000000, 0.0444000000, 0.0425000000, 0.0408000000, 0.0392000000,
0.0377000000, 0.0364000000, 0.0351000000, 0.0339000000, 0.0328000000,
0.0317000000, 0.0308000000],
@[1.0, 1.0, 0.4000000000, 0.2370000000, 0.1550000000, 0.1090000000,
0.0804000000, 0.0618000000, 0.0489000000, 0.0397000000, 0.0329000000,
0.0276000000, 0.0236000000, 0.0203000000, 0.0177000000, 0.0156000000,
0.0138000000, 0.0123000000, 0.0111000000, 0.0099800000, 0.0090600000,
0.0082500000, 0.0075500000, 0.0069400000, 0.0063900000, 0.0059100000,
0.0054800000, 0.0051000000, 0.0047500000, 0.0044400000, 0.0041600000,
0.0039000000, 0.0036700000],
@[1.0, 1.0, 1.0, 0.2530000000, 0.1470000000, 0.0920000000, 0.0609000000,
0.0423000000, 0.0306000000, 0.0228000000, 0.0174000000, 0.0136000000,
0.0108000000, 0.0087500000, 0.0071800000, 0.0059600000, 0.0050000000,
0.0042300000, 0.0036200000, 0.0031200000, 0.0027000000, 0.0023600000,
0.0020700000, 0.0018300000, 0.0016200000, 0.0014500000, 0.0012900000,
0.0011600000, 0.0010500000, 0.0009490000, 0.0008620000, 0.0007850000,
0.0007170000],
@[1.0, 1.0, 1.0, 1.0, 0.1600000000, 0.0920000000, 0.0561000000, 0.0359000000,
0.0240000000, 0.0166000000, 0.0118000000, 0.0086400000, 0.0064600000,
0.0049200000, 0.0038100000, 0.0030000000, 0.0023900000, 0.0019300000,
0.0015800000, 0.0013000000, 0.0010800000, 0.0009050000, 0.0007640000,
0.0006490000, 0.0005550000, 0.0004780000, 0.0004130000, 0.0003590000,
0.0003140000, 0.0002760000, 0.0002430000, 0.0002150000, 0.0001910000],
@[1.0, 1.0, 1.0, 1.0, 1.0, 0.1010000000, 0.0578000000, 0.0347000000,
0.0217000000, 0.0141000000, 0.0094300000, 0.0065000000, 0.0045900000,
0.0033200000, 0.0024400000, 0.0018300000, 0.0013900000, 0.0010700000,
0.0008390000, 0.0006630000, 0.0005300000, 0.0004270000, 0.0003470000,
0.0002850000, 0.0002350000, 0.0001960000, 0.0001640000, 0.0001380000,
0.0001170000, 0.0000996000, 0.0000853000, 0.0000733000, 0.0000633000],
@[1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0638000000, 0.0364000000, 0.0216000000,
0.0133000000, 0.0084400000, 0.0055200000, 0.0037100000, 0.0025500000,
0.0017900000, 0.0012800000, 0.0009350000, 0.0006920000, 0.0005190000,
0.0003940000, 0.0003030000, 0.0002360000, 0.0001850000, 0.0001470000,
0.0001170000, 0.0000944000, 0.0000766000, 0.0000626000, 0.0000515000,
0.0000426000, 0.0000355000, 0.0000297000, 0.0000250000],
@[1.0, 1.0, 1.0,
1.0, 1.0, 1.0, 1.0, 1.0, 0.0229000000, 0.0135000000, 0.0081900000,
0.0051300000, 0.0032900000, 0.0021700000, 0.0014600000, 0.0010000000,
0.0007020000, 0.0004990000, 0.0003600000, 0.0002640000, 0.0001960000,
0.0001470000, 0.0001120000, 0.0000856000, 0.0000663000, 0.0000518000,
0.0000408000, 0.0000324000, 0.0000259000, 0.0000209000, 0.0000169000,
0.0000138000, 0.0000113000],
@[1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0,
1.0, 0.0145000000, 0.0084600000, 0.0050900000, 0.0031400000, 0.0019900000,
0.0012900000, 0.0008520000, 0.0005740000, 0.0003940000, 0.0002750000,
0.0001940000, 0.0001400000, 0.0001010000, 0.0000746000, 0.0000555000,
0.0000417000, 0.0000316000, 0.0000242000, 0.0000187000, 0.0000146000,
0.0000114000, 0.0000090100, 0.0000071600, 0.0000057300],
@[1.0, 1.0, 1.0,
1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0053100000, 0.0031700000,
0.0019400000, 0.0012100000, 0.0007750000, 0.0005050000, 0.0003350000,
0.0002260000, 0.0001550000, 0.0001080000, 0.0000759000, 0.0000542000,
0.0000392000, 0.0000286000, 0.0000211000, 0.0000157000, 0.0000118000,
0.0000089600, 0.0000068500, 0.0000052800, 0.0000041000, 0.0000032000],
@[1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0033400000,
0.0019800000, 0.0012000000, 0.0007440000, 0.0004700000, 0.0003020000,
0.0001980000, 0.0001320000, 0.0000889000, 0.0000609000, 0.0000423000,
0.0000297000, 0.0000211000, 0.0000152000, 0.0000110000, 0.0000080700,
0.0000059700, 0.0000044500, 0.0000033500, 0.0000025400, 0.0000019400],
@[1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0,
0.0021000000, 0.0012400000, 0.0007470000, 0.0004590000, 0.0002870000,
0.0001830000, 0.0001180000, 0.0000777000, 0.0000518000, 0.0000350000,
0.0000240000, 0.0000166000, 0.0000116000, 0.0000082300, 0.0000058900,
0.0000042500, 0.0000031000, 0.0000022800, 0.0000016900, 0.0000012600],
@[1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0,
0.0007780000, 0.0004660000, 0.0002840000, 0.0001760000, 0.0001110000,
0.0000712000, 0.0000463000, 0.0000305000, 0.0000204000, 0.0000138000,
0.0000094200, 0.0000065200, 0.0000045600, 0.0000032200, 0.0000022900,
0.0000016500, 0.0000012000, 0.0000008740]
@[
1.0, 1.0, 0.3930000000, 0.2830000000, 0.2210000000, 0.1810000000, 0.1540000000,
0.1330000000, 0.1180000000, 0.1050000000, 0.0952000000, 0.0869000000, 0.0800000000,
0.0740000000, 0.0689000000, 0.0645000000, 0.0606000000, 0.0571000000, 0.0540000000,
0.0513000000, 0.0488000000, 0.0465000000, 0.0444000000, 0.0425000000, 0.0408000000,
0.0392000000, 0.0377000000, 0.0364000000, 0.0351000000, 0.0339000000, 0.0328000000,
0.0317000000, 0.0308000000,
],
@[
1.0, 1.0, 0.4000000000, 0.2370000000, 0.1550000000, 0.1090000000, 0.0804000000,
0.0618000000, 0.0489000000, 0.0397000000, 0.0329000000, 0.0276000000, 0.0236000000,
0.0203000000, 0.0177000000, 0.0156000000, 0.0138000000, 0.0123000000, 0.0111000000,
0.0099800000, 0.0090600000, 0.0082500000, 0.0075500000, 0.0069400000, 0.0063900000,
0.0059100000, 0.0054800000, 0.0051000000, 0.0047500000, 0.0044400000, 0.0041600000,
0.0039000000, 0.0036700000,
],
@[
1.0, 1.0, 1.0, 0.2530000000, 0.1470000000, 0.0920000000, 0.0609000000, 0.0423000000,
0.0306000000, 0.0228000000, 0.0174000000, 0.0136000000, 0.0108000000, 0.0087500000,
0.0071800000, 0.0059600000, 0.0050000000, 0.0042300000, 0.0036200000, 0.0031200000,
0.0027000000, 0.0023600000, 0.0020700000, 0.0018300000, 0.0016200000, 0.0014500000,
0.0012900000, 0.0011600000, 0.0010500000, 0.0009490000, 0.0008620000, 0.0007850000,
0.0007170000,
],
@[
1.0, 1.0, 1.0, 1.0, 0.1600000000, 0.0920000000, 0.0561000000, 0.0359000000,
0.0240000000, 0.0166000000, 0.0118000000, 0.0086400000, 0.0064600000, 0.0049200000,
0.0038100000, 0.0030000000, 0.0023900000, 0.0019300000, 0.0015800000, 0.0013000000,
0.0010800000, 0.0009050000, 0.0007640000, 0.0006490000, 0.0005550000, 0.0004780000,
0.0004130000, 0.0003590000, 0.0003140000, 0.0002760000, 0.0002430000, 0.0002150000,
0.0001910000,
],
@[
1.0, 1.0, 1.0, 1.0, 1.0, 0.1010000000, 0.0578000000, 0.0347000000, 0.0217000000,
0.0141000000, 0.0094300000, 0.0065000000, 0.0045900000, 0.0033200000, 0.0024400000,
0.0018300000, 0.0013900000, 0.0010700000, 0.0008390000, 0.0006630000, 0.0005300000,
0.0004270000, 0.0003470000, 0.0002850000, 0.0002350000, 0.0001960000, 0.0001640000,
0.0001380000, 0.0001170000, 0.0000996000, 0.0000853000, 0.0000733000, 0.0000633000,
],
@[
1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0638000000, 0.0364000000, 0.0216000000,
0.0133000000, 0.0084400000, 0.0055200000, 0.0037100000, 0.0025500000, 0.0017900000,
0.0012800000, 0.0009350000, 0.0006920000, 0.0005190000, 0.0003940000, 0.0003030000,
0.0002360000, 0.0001850000, 0.0001470000, 0.0001170000, 0.0000944000, 0.0000766000,
0.0000626000, 0.0000515000, 0.0000426000, 0.0000355000, 0.0000297000, 0.0000250000,
],
@[
1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0229000000, 0.0135000000, 0.0081900000,
0.0051300000, 0.0032900000, 0.0021700000, 0.0014600000, 0.0010000000, 0.0007020000,
0.0004990000, 0.0003600000, 0.0002640000, 0.0001960000, 0.0001470000, 0.0001120000,
0.0000856000, 0.0000663000, 0.0000518000, 0.0000408000, 0.0000324000, 0.0000259000,
0.0000209000, 0.0000169000, 0.0000138000, 0.0000113000,
],
@[
1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0145000000, 0.0084600000,
0.0050900000, 0.0031400000, 0.0019900000, 0.0012900000, 0.0008520000, 0.0005740000,
0.0003940000, 0.0002750000, 0.0001940000, 0.0001400000, 0.0001010000, 0.0000746000,
0.0000555000, 0.0000417000, 0.0000316000, 0.0000242000, 0.0000187000, 0.0000146000,
0.0000114000, 0.0000090100, 0.0000071600, 0.0000057300,
],
@[
1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0053100000, 0.0031700000,
0.0019400000, 0.0012100000, 0.0007750000, 0.0005050000, 0.0003350000, 0.0002260000,
0.0001550000, 0.0001080000, 0.0000759000, 0.0000542000, 0.0000392000, 0.0000286000,
0.0000211000, 0.0000157000, 0.0000118000, 0.0000089600, 0.0000068500, 0.0000052800,
0.0000041000, 0.0000032000,
],
@[
1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0033400000,
0.0019800000, 0.0012000000, 0.0007440000, 0.0004700000, 0.0003020000, 0.0001980000,
0.0001320000, 0.0000889000, 0.0000609000, 0.0000423000, 0.0000297000, 0.0000211000,
0.0000152000, 0.0000110000, 0.0000080700, 0.0000059700, 0.0000044500, 0.0000033500,
0.0000025400, 0.0000019400,
],
@[
1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0021000000,
0.0012400000, 0.0007470000, 0.0004590000, 0.0002870000, 0.0001830000, 0.0001180000,
0.0000777000, 0.0000518000, 0.0000350000, 0.0000240000, 0.0000166000, 0.0000116000,
0.0000082300, 0.0000058900, 0.0000042500, 0.0000031000, 0.0000022800, 0.0000016900,
0.0000012600,
],
@[
1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0,
0.0007780000, 0.0004660000, 0.0002840000, 0.0001760000, 0.0001110000, 0.0000712000,
0.0000463000, 0.0000305000, 0.0000204000, 0.0000138000, 0.0000094200, 0.0000065200,
0.0000045600, 0.0000032200, 0.0000022900, 0.0000016500, 0.0000012000, 0.0000008740,
],
]

View File

@ -98,10 +98,8 @@ proc deserializeBloomFilter*(data: seq[byte]): Result[BloomFilter, ReliabilityEr
var bytes: seq[byte]
var cap, errRate, kHashes, mBits: uint64
if not pb.getField(1, bytes).get() or
not pb.getField(2, cap).get() or
not pb.getField(3, errRate).get() or
not pb.getField(4, kHashes).get() or
if not pb.getField(1, bytes).get() or not pb.getField(2, cap).get() or
not pb.getField(3, errRate).get() or not pb.getField(4, kHashes).get() or
not pb.getField(5, mBits).get():
return err(reDeserializationError)
@ -111,12 +109,14 @@ proc deserializeBloomFilter*(data: seq[byte]): Result[BloomFilter, ReliabilityEr
let start = i * sizeof(int)
copyMem(addr intArray[i], unsafeAddr bytes[start], sizeof(int))
ok(BloomFilter(
ok(
BloomFilter(
intArray: intArray,
capacity: int(cap),
errorRate: float(errRate) / 1_000_000,
kHashes: int(kHashes),
mBits: int(mBits)
))
mBits: int(mBits),
)
)
except:
err(reDeserializationError)

View File

@ -2,7 +2,9 @@ import std/[times, locks, tables, sets]
import chronos, results
import ../src/[message, protobuf, reliability_utils, rolling_bloom_filter]
proc newReliabilityManager*(channelId: string, config: ReliabilityConfig = defaultConfig()): Result[ReliabilityManager, ReliabilityError] =
proc newReliabilityManager*(
channelId: string, config: ReliabilityConfig = defaultConfig()
): Result[ReliabilityManager, ReliabilityError] =
## Creates a new ReliabilityManager with the specified channel ID and configuration.
##
## Parameters:
@ -16,9 +18,7 @@ proc newReliabilityManager*(channelId: string, config: ReliabilityConfig = defau
try:
let bloomFilter = newRollingBloomFilter(
config.bloomFilterCapacity,
config.bloomFilterErrorRate,
config.bloomFilterWindow
config.bloomFilterCapacity, config.bloomFilterErrorRate, config.bloomFilterWindow
)
let rm = ReliabilityManager(
@ -28,7 +28,7 @@ proc newReliabilityManager*(channelId: string, config: ReliabilityConfig = defau
outgoingBuffer: @[],
incomingBuffer: @[],
channelId: channelId,
config: config
config: config,
)
initLock(rm.lock)
return ok(rm)
@ -52,9 +52,7 @@ proc reviewAckStatus(rm: ReliabilityManager, msg: Message) =
let bfResult = deserializeBloomFilter(msg.bloomFilter)
if bfResult.isOk:
var rbf = RollingBloomFilter(
filter: bfResult.get(),
window: rm.bloomFilter.window,
messages: @[]
filter: bfResult.get(), window: rm.bloomFilter.window, messages: @[]
)
if rbf.contains(outMsg.message.messageId):
acknowledged = true
@ -62,13 +60,22 @@ proc reviewAckStatus(rm: ReliabilityManager, msg: Message) =
logError("Failed to deserialize bloom filter")
if acknowledged:
echo "[Nim Core] reviewAckStatus: Message acknowledged: ",
outMsg.message.messageId
if rm.onMessageSent != nil:
rm.onMessageSent(outMsg.message.messageId)
echo "[Nim Core] reviewAckStatus: Calling onMessageSent for: ",
outMsg.message.messageId
rm.onMessageSent(rm, outMsg.message.messageId) # Pass rm
else:
echo "[Nim Core] reviewAckStatus: rm.onMessageSent is nil, cannot call callback for: ",
outMsg.message.messageId
rm.outgoingBuffer.delete(i)
else:
inc i
proc wrapOutgoingMessage*(rm: ReliabilityManager, message: seq[byte], messageId: MessageID): Result[seq[byte], ReliabilityError] =
proc wrapOutgoingMessage*(
rm: ReliabilityManager, message: seq[byte], messageId: MessageID
): Result[seq[byte], ReliabilityError] =
## Wraps an outgoing message with reliability metadata.
##
## Parameters:
@ -100,15 +107,13 @@ proc wrapOutgoingMessage*(rm: ReliabilityManager, message: seq[byte], messageId:
causalHistory: rm.getRecentMessageIDs(rm.config.maxCausalHistory),
channelId: rm.channelId,
content: message,
bloomFilter: bloomBytes
bloomFilter: bloomBytes,
)
# Add to outgoing buffer
rm.outgoingBuffer.add(UnacknowledgedMessage(
message: msg,
sendTime: getTime(),
resendAttempts: 0
))
rm.outgoingBuffer.add(
UnacknowledgedMessage(message: msg, sendTime: getTime(), resendAttempts: 0)
)
# Add to causal history and bloom filter
rm.bloomFilter.add(msg.messageId)
@ -153,10 +158,15 @@ proc processIncomingBuffer(rm: ReliabilityManager) =
for msg in rm.incomingBuffer:
if msg.messageId == msgId:
rm.addToHistory(msg.messageId)
echo "[Nim Core] processIncomingBuffer: Message ready: ", msg.messageId
if rm.onMessageReady != nil:
rm.onMessageReady(msg.messageId)
echo "[Nim Core] processIncomingBuffer: Calling onMessageReady for: ",
msg.messageId
rm.onMessageReady(rm, msg.messageId) # Pass rm
else:
echo "[Nim Core] processIncomingBuffer: rm.onMessageReady is nil, cannot call callback for: ",
msg.messageId
processed.incl(msgId)
# Add any dependent messages that might now be ready
if msgId in dependencies:
for dependentId in dependencies[msgId]:
@ -170,7 +180,9 @@ proc processIncomingBuffer(rm: ReliabilityManager) =
rm.incomingBuffer = newIncomingBuffer
proc unwrapReceivedMessage*(rm: ReliabilityManager, message: seq[byte]): Result[tuple[message: seq[byte], missingDeps: seq[MessageID]], ReliabilityError] =
proc unwrapReceivedMessage*(
rm: ReliabilityManager, message: seq[byte]
): Result[tuple[message: seq[byte], missingDeps: seq[MessageID]], ReliabilityError] =
## Unwraps a received message and processes its reliability metadata.
##
## Parameters:
@ -185,9 +197,11 @@ proc unwrapReceivedMessage*(rm: ReliabilityManager, message: seq[byte]): Result[
let msg = msgResult.get
if rm.bloomFilter.contains(msg.messageId):
echo "[Nim Core] unwrapReceivedMessage: Duplicate message detected (in bloom filter): ",
msg.messageId # Add this log
return ok((msg.content, @[]))
rm.bloomFilter.add(msg.messageId)
rm.bloomFilter.add(msg.messageId) # Add to receiver's bloom filter
# Update Lamport timestamp
rm.updateLamportTimestamp(msg.lamportTimestamp)
@ -212,20 +226,46 @@ proc unwrapReceivedMessage*(rm: ReliabilityManager, message: seq[byte]): Result[
else:
# All dependencies met, add to history
rm.addToHistory(msg.messageId)
rm.processIncomingBuffer()
rm.processIncomingBuffer() # This might trigger onMessageReady internally
# If processIncomingBuffer didn't handle it (e.g., buffer was empty), handle it now.
# We know deps are met, so it should be ready.
# NOTE: Need to ensure addToHistory isn't called twice if processIncomingBuffer also adds it.
# Let's assume processIncomingBuffer handles adding to history if it processes the message.
# We only call the callback here if it wasn't handled by processIncomingBuffer.
# A more robust check would involve seeing if msgId was added to 'processed' set in processIncomingBuffer,
# but let's try simply calling the callback if the condition is met.
# We already added to history on line 222.
echo "[Nim Core] unwrapReceivedMessage: Message ready (direct): ", msg.messageId
# rm.addToHistory(msg.messageId) # Removed potential duplicate add
if rm.onMessageReady != nil:
rm.onMessageReady(msg.messageId)
echo "[Nim Core] unwrapReceivedMessage: Calling onMessageReady for: ",
msg.messageId
rm.onMessageReady(rm, msg.messageId) # Pass rm
else:
echo "[Nim Core] unwrapReceivedMessage: rm.onMessageReady is nil, cannot call callback for: ",
msg.messageId
else:
# Buffer message and request missing dependencies
echo "[Nim Core] unwrapReceivedMessage: Buffering message due to missing deps: ",
msg.messageId
rm.incomingBuffer.add(msg)
echo "[Nim Core] unwrapReceivedMessage: Checking onMissingDependencies callback for: ",
msg.messageId
if rm.onMissingDependencies != nil:
rm.onMissingDependencies(msg.messageId, missingDeps)
echo "[Nim Core] unwrapReceivedMessage: Calling onMissingDependencies for: ",
msg.messageId
rm.onMissingDependencies(rm, msg.messageId, missingDeps) # Pass rm
else:
echo "[Nim Core] unwrapReceivedMessage: rm.onMissingDependencies is nil, cannot call callback for: ",
msg.messageId
return ok((msg.content, missingDeps))
except:
return err(reInternalError)
proc markDependenciesMet*(rm: ReliabilityManager, messageIds: seq[MessageID]): Result[void, ReliabilityError] =
proc markDependenciesMet*(
rm: ReliabilityManager, messageIds: seq[MessageID]
): Result[void, ReliabilityError] =
## Marks the specified message dependencies as met.
##
## Parameters:
@ -239,17 +279,24 @@ proc markDependenciesMet*(rm: ReliabilityManager, messageIds: seq[MessageID]): R
if not rm.bloomFilter.contains(msgId):
rm.bloomFilter.add(msgId)
# rm.addToHistory(msgId) -- not needed as this proc usually called when msg in long-term storage of application?
echo "[Nim Core] markDependenciesMet: Calling processIncomingBuffer after marking deps"
rm.processIncomingBuffer()
return ok()
except:
return err(reInternalError)
proc setCallbacks*(rm: ReliabilityManager,
onMessageReady: proc(messageId: MessageID) {.gcsafe.},
onMessageSent: proc(messageId: MessageID) {.gcsafe.},
onMissingDependencies: proc(messageId: MessageID, missingDeps: seq[MessageID]) {.gcsafe.},
onPeriodicSync: PeriodicSyncCallback = nil) =
proc setCallbacks*(
rm: ReliabilityManager,
onMessageReady: proc(rm: ReliabilityManager, messageId: MessageID) {.gcsafe.},
# Add rm
onMessageSent: proc(rm: ReliabilityManager, messageId: MessageID) {.gcsafe.},
# Add rm
onMissingDependencies: proc(
rm: ReliabilityManager, messageId: MessageID, missingDeps: seq[MessageID]
) {.gcsafe.}, # Add rm
onPeriodicSync: proc(rm: ReliabilityManager) {.gcsafe.} = nil,
) = # Add rm, make type explicit
## Sets the callback functions for various events in the ReliabilityManager.
##
## Parameters:
@ -281,8 +328,15 @@ proc checkUnacknowledgedMessages*(rm: ReliabilityManager) {.raises: [].} =
newOutgoingBuffer.add(updatedMsg)
else:
if rm.onMessageSent != nil:
rm.onMessageSent(unackMsg.message.messageId)
# Assuming message timeout means it's considered "sent" or "failed"
echo "[Nim Core] checkUnacknowledgedMessages: Calling onMessageSent for timed out message: ",
unackMsg.message.messageId
rm.onMessageSent(rm, unackMsg.message.messageId) # Pass rm
else:
echo "[Nim Core] checkUnacknowledgedMessages: rm.onMessageSent is nil for timed out message: ",
unackMsg.message.messageId
else:
# Dedent this else to match `if elapsed > rm.config.resendInterval:` (line 296)
newOutgoingBuffer.add(unackMsg)
rm.outgoingBuffer = newOutgoingBuffer
@ -306,8 +360,12 @@ proc periodicSyncMessage(rm: ReliabilityManager) {.async: (raises: [CancelledErr
while true:
{.gcsafe.}:
try:
echo "[Nim Core] periodicSyncMessage: Checking onPeriodicSync callback"
if rm.onPeriodicSync != nil:
rm.onPeriodicSync()
echo "[Nim Core] periodicSyncMessage: Calling onPeriodicSync"
rm.onPeriodicSync(rm) # Pass rm
else:
echo "[Nim Core] periodicSyncMessage: rm.onPeriodicSync is nil"
except Exception as e:
logError("Error in periodic sync: " & e.msg)
await sleepAsync(chronos.seconds(rm.config.syncMessageInterval.inSeconds))
@ -333,9 +391,8 @@ proc resetReliabilityManager*(rm: ReliabilityManager): Result[void, ReliabilityE
rm.outgoingBuffer.setLen(0)
rm.incomingBuffer.setLen(0)
rm.bloomFilter = newRollingBloomFilter(
rm.config.bloomFilterCapacity,
rm.config.bloomFilterErrorRate,
rm.config.bloomFilterWindow
rm.config.bloomFilterCapacity, rm.config.bloomFilterErrorRate,
rm.config.bloomFilterWindow,
)
return ok()
except:

View File

@ -2,7 +2,25 @@ import std/[times, locks]
import ./[rolling_bloom_filter, message]
type
PeriodicSyncCallback* = proc() {.gcsafe, raises: [].}
# Forward declare C types needed within ReliabilityManager definition
# Ideally, these would be imported from a shared header/module if possible,
# but defining them here avoids circular dependencies for now.
CEventType* {.importc: "CEventType", header: "../bindings/bindings.h", pure.} = enum
# Use relative path
EVENT_MESSAGE_READY = 1
EVENT_MESSAGE_SENT = 2
EVENT_MISSING_DEPENDENCIES = 3
EVENT_PERIODIC_SYNC = 4
CEventCallback* = proc(
handle: pointer,
eventType: CEventType,
data1: pointer,
data2: pointer,
data3: csize_t,
) {.cdecl, gcsafe.}
PeriodicSyncCallback* = proc() {.gcsafe, raises: [].} # This is the Nim internal type
ReliabilityConfig* = object
bloomFilterCapacity*: int
@ -24,10 +42,19 @@ type
channelId*: string
config*: ReliabilityConfig
lock*: Lock
onMessageReady*: proc(messageId: MessageID)
onMessageSent*: proc(messageId: MessageID)
onMissingDependencies*: proc(messageId: MessageID, missingDeps: seq[MessageID])
onPeriodicSync*: proc()
# Nim internal callbacks (assigned in bindings/bindings.nim)
onMessageReady*: proc(rm: ReliabilityManager, messageId: MessageID) {.gcsafe.}
# Pass rm
onMessageSent*: proc(rm: ReliabilityManager, messageId: MessageID) {.gcsafe.}
# Pass rm
onMissingDependencies*: proc(
rm: ReliabilityManager, messageId: MessageID, missingDeps: seq[MessageID]
) {.gcsafe.} # Pass rm
onPeriodicSync*: proc(rm: ReliabilityManager) {.gcsafe.} # Pass rm
# C callback info (set via RegisterCallback)
cCallback*: CEventCallback
cUserData*: pointer
ReliabilityError* = enum
reInvalidArgument
@ -51,7 +78,7 @@ proc defaultConfig*(): ReliabilityConfig =
resendInterval: DefaultResendInterval,
maxResendAttempts: DefaultMaxResendAttempts,
syncMessageInterval: DefaultSyncMessageInterval,
bufferSweepInterval: DefaultBufferSweepInterval
bufferSweepInterval: DefaultBufferSweepInterval,
)
proc cleanup*(rm: ReliabilityManager) {.raises: [].} =
@ -76,7 +103,9 @@ proc addToHistory*(rm: ReliabilityManager, msgId: MessageID) {.gcsafe, raises: [
if rm.messageHistory.len > rm.config.maxMessageHistory:
rm.messageHistory.delete(0)
proc updateLamportTimestamp*(rm: ReliabilityManager, msgTs: int64) {.gcsafe, raises: [].} =
proc updateLamportTimestamp*(
rm: ReliabilityManager, msgTs: int64
) {.gcsafe, raises: [].} =
rm.lamportTimestamp = max(msgTs, rm.lamportTimestamp) + 1
proc getRecentMessageIDs*(rm: ReliabilityManager, n: int): seq[MessageID] =

View File

@ -3,8 +3,7 @@ import chronos
import chronicles
import ./[bloom, message]
type
RollingBloomFilter* = object
type RollingBloomFilter* = object
filter*: BloomFilter
window*: times.Duration
messages*: seq[TimestampedMessageID]
@ -20,7 +19,9 @@ proc logError*(msg: string) =
proc logInfo*(msg: string) =
info "ReliabilityInfo", message = msg
proc newRollingBloomFilter*(capacity: int, errorRate: float, window: times.Duration): RollingBloomFilter {.gcsafe.} =
proc newRollingBloomFilter*(
capacity: int, errorRate: float, window: times.Duration
): RollingBloomFilter {.gcsafe.} =
try:
var filterResult: Result[BloomFilter, string]
{.gcsafe.}:
@ -31,23 +32,20 @@ proc newRollingBloomFilter*(capacity: int, errorRate: float, window: times.Durat
return RollingBloomFilter(
filter: filterResult.get(), # Extract the BloomFilter from Result
window: window,
messages: @[]
messages: @[],
)
else:
logError("Failed to initialize bloom filter: " & filterResult.error)
# Fall through to default case below
except:
logError("Failed to initialize bloom filter")
# Default fallback case
let defaultResult = initializeBloomFilter(DefaultBloomFilterCapacity, DefaultBloomFilterErrorRate)
let defaultResult =
initializeBloomFilter(DefaultBloomFilterCapacity, DefaultBloomFilterErrorRate)
if defaultResult.isOk:
return RollingBloomFilter(
filter: defaultResult.get(),
window: window,
messages: @[]
)
return
RollingBloomFilter(filter: defaultResult.get(), window: window, messages: @[])
else:
# If even default initialization fails, raise an exception
logError("Failed to initialize bloom filter with default parameters")
@ -77,7 +75,8 @@ proc clean*(rbf: var RollingBloomFilter) {.gcsafe.} =
var newMessages: seq[TimestampedMessageID] = @[]
# Initialize new filter
let newFilterResult = initializeBloomFilter(rbf.filter.capacity, rbf.filter.errorRate)
let newFilterResult =
initializeBloomFilter(rbf.filter.capacity, rbf.filter.errorRate)
if newFilterResult.isErr:
logError("Failed to create new bloom filter: " & newFilterResult.error)
return

View File

@ -13,9 +13,9 @@ suite "bloom filter":
sampleChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
testElements = newSeq[string](nElementsToTest)
for i in 0..<nElementsToTest:
for i in 0 ..< nElementsToTest:
var newString = ""
for j in 0..7:
for j in 0 .. 7:
newString.add(sampleChars[rand(51)])
testElements[i] = newString
@ -41,9 +41,9 @@ suite "bloom filter":
test "error rate":
var falsePositives = 0
let testSize = nElementsToTest div 2
for i in 0..<testSize:
for i in 0 ..< testSize:
var testString = ""
for j in 0..8: # Different length than setup
for j in 0 .. 8: # Different length than setup
testString.add(sampleChars[rand(51)])
if bf.lookup(testString):
falsePositives.inc()
@ -62,12 +62,14 @@ suite "bloom filter":
# Test error case for k > 12
let errorCase = getMOverNBitsForK(k = 13, targetError = 0.01)
check errorCase.isErr
check errorCase.error == "K must be <= 12 if forceNBitsPerElem is not also specified."
check errorCase.error ==
"K must be <= 12 if forceNBitsPerElem is not also specified."
# Test error case for unachievable error rate
let errorCase2 = getMOverNBitsForK(k = 2, targetError = 0.00001)
check errorCase2.isErr
check errorCase2.error == "Specified value of k and error rate not achievable using less than 4 bytes / element."
check errorCase2.error ==
"Specified value of k and error rate not achievable using less than 4 bytes / element."
# Test success cases
let case1 = getMOverNBitsForK(k = 2, targetError = 0.1)
@ -100,12 +102,13 @@ suite "bloom filter":
suite "bloom filter special cases":
test "different patterns of strings":
const testSize = 10_000
let patterns = @[
let patterns =
@[
"shortstr",
repeat("a", 1000), # Very long string
"special@#$%^&*()", # Special characters
"unicode→★∑≈", # Unicode characters
repeat("pattern", 10) # Repeating pattern
repeat("pattern", 10), # Repeating pattern
]
let bfResult = initializeBloomFilter(testSize, 0.01)
@ -119,7 +122,7 @@ suite "bloom filter special cases":
assert bf.lookup(pattern), "failed lookup pattern: " & pattern
# Test general insertion and lookup
for i in 0..<testSize:
for i in 0 ..< testSize:
inserted[i] = $i & "test" & $rand(1000)
bf.insert(inserted[i])
@ -133,7 +136,7 @@ suite "bloom filter special cases":
# Check false positive rate
var falsePositives = 0
let fpTestSize = testSize div 2
for i in 0..<fpTestSize:
for i in 0 ..< fpTestSize:
let testItem = "notpresent" & $i & $rand(1000)
if bf.lookup(testItem):
falsePositives.inc()

View File

@ -46,7 +46,7 @@ suite "Core Operations":
causalHistory: @[],
channelId: "testChannel",
content: @[byte(1)],
bloomFilter: @[]
bloomFilter: @[],
)
let msg2 = Message(
@ -55,7 +55,7 @@ suite "Core Operations":
causalHistory: @[],
channelId: "testChannel",
content: @[byte(2)],
bloomFilter: @[]
bloomFilter: @[],
)
let serialized1 = serializeMessage(msg1)
@ -90,10 +90,16 @@ suite "Reliability Mechanisms":
var messageSentCount = 0
var missingDepsCount = 0
# Update anonymous procs to match new signature (add rm parameter)
rm.setCallbacks(
proc(messageId: MessageID) {.gcsafe.} = messageReadyCount += 1,
proc(messageId: MessageID) {.gcsafe.} = messageSentCount += 1,
proc(messageId: MessageID, missingDeps: seq[MessageID]) {.gcsafe.} = missingDepsCount += 1
onMessageReady = proc(rm: ReliabilityManager, messageId: MessageID) {.gcsafe.} =
messageReadyCount += 1,
onMessageSent = proc(rm: ReliabilityManager, messageId: MessageID) {.gcsafe.} =
messageSentCount += 1,
onMissingDependencies = proc(
rm: ReliabilityManager, messageId: MessageID, missingDeps: seq[MessageID]
) {.gcsafe.} =
missingDepsCount += 1, # onPeriodicSync is left as default nil
)
# Create dependency chain: msg3 -> msg2 -> msg1
@ -108,7 +114,7 @@ suite "Reliability Mechanisms":
causalHistory: @[id1], # msg2 depends on msg1
channelId: "testChannel",
content: @[byte(2)],
bloomFilter: @[]
bloomFilter: @[],
)
let msg3 = Message(
@ -117,7 +123,7 @@ suite "Reliability Mechanisms":
causalHistory: @[id1, id2], # msg3 depends on both msg1 and msg2
channelId: "testChannel",
content: @[byte(3)],
bloomFilter: @[]
bloomFilter: @[],
)
let serialized2 = serializeMessage(msg2)
@ -164,10 +170,16 @@ suite "Reliability Mechanisms":
var messageSentCount = 0
var missingDepsCount = 0
# Update anonymous procs to match new signature (add rm parameter)
rm.setCallbacks(
proc(messageId: MessageID) {.gcsafe.} = messageReadyCount += 1,
proc(messageId: MessageID) {.gcsafe.} = messageSentCount += 1,
proc(messageId: MessageID, missingDeps: seq[MessageID]) {.gcsafe.} = missingDepsCount += 1
onMessageReady = proc(rm: ReliabilityManager, messageId: MessageID) {.gcsafe.} =
messageReadyCount += 1,
onMessageSent = proc(rm: ReliabilityManager, messageId: MessageID) {.gcsafe.} =
messageSentCount += 1,
onMissingDependencies = proc(
rm: ReliabilityManager, messageId: MessageID, missingDeps: seq[MessageID]
) {.gcsafe.} =
missingDepsCount += 1,
)
# Send our message
@ -184,6 +196,7 @@ suite "Reliability Mechanisms":
channelId: "testChannel",
content: @[byte(2)],
bloomFilter: @[] # Test with an empty bloom filter
,
)
let serializedMsg2 = serializeMessage(msg2)
@ -200,10 +213,16 @@ suite "Reliability Mechanisms":
test "acknowledgment via bloom filter":
var messageSentCount = 0
# Update anonymous procs to match new signature (add rm parameter)
rm.setCallbacks(
proc(messageId: MessageID) {.gcsafe.} = discard,
proc(messageId: MessageID) {.gcsafe.} = messageSentCount += 1,
proc(messageId: MessageID, missingDeps: seq[MessageID]) {.gcsafe.} = discard
onMessageReady = proc(rm: ReliabilityManager, messageId: MessageID) {.gcsafe.} =
discard,
onMessageSent = proc(rm: ReliabilityManager, messageId: MessageID) {.gcsafe.} =
messageSentCount += 1,
onMissingDependencies = proc(
rm: ReliabilityManager, messageId: MessageID, missingDeps: seq[MessageID]
) {.gcsafe.} =
discard,
)
# Send our message
@ -214,9 +233,7 @@ suite "Reliability Mechanisms":
# Create a message with bloom filter containing our message
var otherPartyBloomFilter = newRollingBloomFilter(
DefaultBloomFilterCapacity,
DefaultBloomFilterErrorRate,
DefaultBloomFilterWindow
DefaultBloomFilterCapacity, DefaultBloomFilterErrorRate, DefaultBloomFilterWindow
)
otherPartyBloomFilter.add(id1)
@ -229,7 +246,7 @@ suite "Reliability Mechanisms":
causalHistory: @[], # Empty causal history as we're using bloom filter
channelId: "testChannel",
content: @[byte(2)],
bloomFilter: bfResult.get()
bloomFilter: bfResult.get(),
)
let serializedMsg2 = serializeMessage(msg2)
@ -256,14 +273,20 @@ suite "Periodic Tasks & Buffer Management":
test "outgoing buffer management":
var messageSentCount = 0
# Update anonymous procs to match new signature (add rm parameter)
rm.setCallbacks(
proc(messageId: MessageID) {.gcsafe.} = discard,
proc(messageId: MessageID) {.gcsafe.} = messageSentCount += 1,
proc(messageId: MessageID, missingDeps: seq[MessageID]) {.gcsafe.} = discard
onMessageReady = proc(rm: ReliabilityManager, messageId: MessageID) {.gcsafe.} =
discard,
onMessageSent = proc(rm: ReliabilityManager, messageId: MessageID) {.gcsafe.} =
messageSentCount += 1,
onMissingDependencies = proc(
rm: ReliabilityManager, messageId: MessageID, missingDeps: seq[MessageID]
) {.gcsafe.} =
discard,
)
# Add multiple messages
for i in 0..5:
for i in 0 .. 5:
let msg = @[byte(i)]
let id = "msg" & $i
let wrap = rm.wrapOutgoingMessage(msg, id)
@ -279,7 +302,7 @@ suite "Periodic Tasks & Buffer Management":
causalHistory: @["msg0", "msg2", "msg4"],
channelId: "testChannel",
content: @[byte(100)],
bloomFilter: @[]
bloomFilter: @[],
)
let serializedAck = serializeMessage(ackMsg)
@ -291,7 +314,8 @@ suite "Periodic Tasks & Buffer Management":
let finalBuffer = rm.getOutgoingBuffer()
check:
finalBuffer.len == 3 # Should have removed acknowledged messages
messageSentCount == 3 # Should have triggered sent callback for acknowledged messages
messageSentCount == 3
# Should have triggered sent callback for acknowledged messages
test "periodic buffer sweep and bloom clean":
var messageSentCount = 0
@ -306,10 +330,16 @@ suite "Periodic Tasks & Buffer Management":
check rmResultP.isOk()
let rm = rmResultP.get()
# Update anonymous procs to match new signature (add rm parameter)
rm.setCallbacks(
proc(messageId: MessageID) {.gcsafe.} = discard,
proc(messageId: MessageID) {.gcsafe.} = messageSentCount += 1,
proc(messageId: MessageID, missingDeps: seq[MessageID]) {.gcsafe.} = discard
onMessageReady = proc(rm: ReliabilityManager, messageId: MessageID) {.gcsafe.} =
discard,
onMessageSent = proc(rm: ReliabilityManager, messageId: MessageID) {.gcsafe.} =
messageSentCount += 1,
onMissingDependencies = proc(
rm: ReliabilityManager, messageId: MessageID, missingDeps: seq[MessageID]
) {.gcsafe.} =
discard,
)
# First message - should be cleaned from bloom filter later
@ -336,7 +366,8 @@ suite "Periodic Tasks & Buffer Management":
let finalBuffer = rm.getOutgoingBuffer()
check:
finalBuffer.len == 1 # Only msg2 should be in buffer, msg1 should be removed after max retries
finalBuffer.len == 1
# Only msg2 should be in buffer, msg1 should be removed after max retries
finalBuffer[0].message.messageId == id2 # Verify it's the second message
finalBuffer[0].resendAttempts == 0 # New message should have 0 attempts
not rm.bloomFilter.contains(id1) # Bloom filter cleaning check
@ -346,11 +377,18 @@ suite "Periodic Tasks & Buffer Management":
test "periodic sync callback":
var syncCallCount = 0
# Update anonymous procs to match new signature (add rm parameter)
rm.setCallbacks(
proc(messageId: MessageID) {.gcsafe.} = discard,
proc(messageId: MessageID) {.gcsafe.} = discard,
proc(messageId: MessageID, missingDeps: seq[MessageID]) {.gcsafe.} = discard,
proc() {.gcsafe.} = syncCallCount += 1
onMessageReady = proc(rm: ReliabilityManager, messageId: MessageID) {.gcsafe.} =
discard,
onMessageSent = proc(rm: ReliabilityManager, messageId: MessageID) {.gcsafe.} =
discard,
onMissingDependencies = proc(
rm: ReliabilityManager, messageId: MessageID, missingDeps: seq[MessageID]
) {.gcsafe.} =
discard,
onPeriodicSync = proc(rm: ReliabilityManager) {.gcsafe.} =
syncCallCount += 1, # Already correct from previous partial apply
)
rm.startPeriodicTasks()
@ -374,7 +412,7 @@ suite "Special Cases Handling":
test "message history limits":
# Add messages up to max history size
for i in 0..rm.config.maxMessageHistory + 5:
for i in 0 .. rm.config.maxMessageHistory + 5:
let msg = @[byte(i)]
let id = "msg" & $i
let wrap = rm.wrapOutgoingMessage(msg, id)
@ -393,6 +431,7 @@ suite "Special Cases Handling":
channelId: "testChannel",
content: @[byte(1)],
bloomFilter: @[1.byte, 2.byte, 3.byte] # Invalid filter data
,
)
let serializedInvalid = serializeMessage(msgInvalid)
@ -406,10 +445,16 @@ suite "Special Cases Handling":
test "duplicate message handling":
var messageReadyCount = 0
# Update anonymous procs to match new signature (add rm parameter)
rm.setCallbacks(
proc(messageId: MessageID) {.gcsafe.} = messageReadyCount += 1,
proc(messageId: MessageID) {.gcsafe.} = discard,
proc(messageId: MessageID, missingDeps: seq[MessageID]) {.gcsafe.} = discard
onMessageReady = proc(rm: ReliabilityManager, messageId: MessageID) {.gcsafe.} =
messageReadyCount += 1, # Already correct from previous partial apply
onMessageSent = proc(rm: ReliabilityManager, messageId: MessageID) {.gcsafe.} =
discard, # Already correct from previous partial apply
onMissingDependencies = proc(
rm: ReliabilityManager, messageId: MessageID, missingDeps: seq[MessageID]
) {.gcsafe.} =
discard, # Already correct from previous partial apply
)
# Create and process a message
@ -419,7 +464,7 @@ suite "Special Cases Handling":
causalHistory: @[],
channelId: "testChannel",
content: @[byte(1)],
bloomFilter: @[]
bloomFilter: @[],
)
let serialized = serializeMessage(msg)