## Run each scenario in its own ThreadSanitizer-instrumented process. ## Success means no race was reported; a crash is not required to expose UB. import std/[atomics, locks, os] import ../src/namespaced_logging const iterations = 1_000 var start: Atomic[bool] var cloneLock: Lock proc awaitStart() = while not start.load(): discard proc serviceWorker(service: LogService) {.thread.} = awaitStart() for i in 0 ..< iterations: let local = service.threadLocalRef() let logger = local.getLogger("worker") doAssert logger.isEnabled(lvlInfo) proc independentWorker() {.thread.} = awaitStart() var service = initLogService() service.setThreshold("worker", lvlInfo) for i in 0 ..< iterations: let local = service.threadLocalRef() let logger = local.getLogger("worker") doAssert logger.isEnabled(lvlInfo) type CustomState = ref object value: int StatefulAppender = CustomLogAppender[CustomState] proc ignoreMessage(state: CustomState, msg: LogMessage) {.gcsafe, noconv.} = discard proc stateWorker(source: ptr StatefulAppender) {.thread.} = awaitStart() for i in 0 ..< iterations: var copied: StatefulAppender withLock cloneLock: copied = StatefulAppender(clone(source[])) doAssert copied.state.value == 42 proc makeFormatter(prefix: string): LogMessageFormatter = result = proc(msg: LogMessage): string {.gcsafe.} = prefix & msg.msg proc formatterWorker(source: ptr ConsoleLogAppender) {.thread.} = awaitStart() # This formatter only needs msg. Avoid clock/timezone initialization so # sanitizer diagnostics identify closure ownership rather than libc state. let msg = LogMessage(msg: "message") for i in 0 ..< iterations: var copied: ConsoleLogAppender withLock cloneLock: copied = ConsoleLogAppender(clone(source[])) doAssert copied.formatter(msg) == "prefix: message" proc main() = doAssert paramCount() == 1 initLock(cloneLock) defer: deinitLock(cloneLock) case paramStr(1) of "independent": var workers: array[4, Thread[void]] for i in 0 ..< workers.len: createThread(workers[i], independentWorker) start.store(true) joinThreads(workers) of "service": var service = initLogService() service.setThreshold("worker", lvlInfo) service.addAppender(initConsoleLogAppender()) var workers: array[4, Thread[LogService]] for i in 0 ..< workers.len: createThread(workers[i], serviceWorker, service) # No parent-side copying/configuration changes overlap the workers. # The parent retains service and all thread argument storage until join. start.store(true) joinThreads(workers) of "custom-state": var source = initCustomLogAppender(CustomState(value: 42), ignoreMessage) var workers: array[4, Thread[ptr StatefulAppender]] for i in 0 ..< workers.len: createThread(workers[i], stateWorker, addr source) start.store(true) joinThreads(workers) of "formatter": var source = initConsoleLogAppender() source.formatter = makeFormatter("prefix: ") var workers: array[4, Thread[ptr ConsoleLogAppender]] for i in 0 ..< workers.len: createThread(workers[i], formatterWorker, addr source) start.store(true) joinThreads(workers) else: raise newException(ValueError, "unknown thread safety scenario") # The pointer-based scenarios borrow immutable source appenders whose owner # outlives all workers. This isolates clone/destruction races from LogService # copying and from ordinary unsynchronized application-state mutation. main()