175 lines
6.0 KiB
Nim
175 lines
6.0 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
|
|
var ready: Atomic[int]
|
|
|
|
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
|
|
AppenderLifetimes = object
|
|
created, destroyed: Atomic[int]
|
|
TrackedAppenderObj = object of LogAppender
|
|
lifetimes: ptr AppenderLifetimes
|
|
TrackedAppender = ref TrackedAppenderObj
|
|
|
|
proc `=destroy`(app: TrackedAppenderObj) =
|
|
if app.lifetimes != nil:
|
|
app.lifetimes.destroyed.atomicInc()
|
|
`=destroy`(app.namespace)
|
|
|
|
proc trackedAppender(lifetimes: ptr AppenderLifetimes): TrackedAppender =
|
|
lifetimes.created.atomicInc()
|
|
TrackedAppender(lifetimes: lifetimes)
|
|
|
|
method clone(app: TrackedAppender): LogAppender {.gcsafe.} =
|
|
trackedAppender(app.lifetimes)
|
|
|
|
proc lifetimeWorker(source: ptr LogService) {.thread.} =
|
|
# Borrow the original only until this thread owns a local service. The
|
|
# readiness barrier lets the parent release it before we continue.
|
|
let local = source[].threadLocalRef()
|
|
ready.atomicInc()
|
|
awaitStart()
|
|
for i in 0 ..< iterations:
|
|
var handle = local.service
|
|
var copied = handle
|
|
handle = LogService()
|
|
var moved = move(copied)
|
|
moved.setThreshold("worker", lvlInfo)
|
|
let nested = moved.threadLocalRef()
|
|
doAssert nested.getLogger("worker").isEnabled(lvlInfo)
|
|
# Local caches and the last shared owner are destroyed on worker threads.
|
|
|
|
proc reconfigureWorker(service: LogService) {.thread.} =
|
|
var handle = service
|
|
let local = service.threadLocalRef()
|
|
awaitStart()
|
|
for i in 0 ..< iterations:
|
|
handle.setThreshold("worker", lvlInfo)
|
|
local.ensureFreshness()
|
|
doAssert local.getLogger("worker").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 "service-lifetime":
|
|
var lifetimes: AppenderLifetimes
|
|
var service = initLogService()
|
|
block:
|
|
let original = trackedAppender(addr lifetimes)
|
|
service.addAppender(original)
|
|
var workers: array[4, Thread[ptr LogService]]
|
|
for i in 0 ..< workers.len:
|
|
createThread(workers[i], lifetimeWorker, addr service)
|
|
while ready.load() != workers.len:
|
|
discard
|
|
# Thread arguments contain only a borrowed pointer, so there are no hidden
|
|
# owning handles in Thread.data keeping the control block alive here.
|
|
service = LogService()
|
|
start.store(true)
|
|
joinThreads(workers)
|
|
doAssert lifetimes.created.load() > workers.len
|
|
doAssert lifetimes.destroyed.load() == lifetimes.created.load()
|
|
of "service-reconfigure":
|
|
var service = initLogService()
|
|
service.addAppender(initConsoleLogAppender())
|
|
var workers: array[4, Thread[LogService]]
|
|
for i in 0 ..< workers.len:
|
|
createThread(workers[i], reconfigureWorker, service)
|
|
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()
|