Files
nim-namespaced-logging/tests/thread_safety.nim
T

104 lines
3.5 KiB
Nim

## 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()