Serialize writer submission and shutdown, enqueue explicit stop messages after accepted records, and finish the final file batch before joining. Keep drained process-wide channel storage until exit to avoid the Nim 2.2.10 Channel.close mutex teardown defect. Cover empty records, multiple destinations, joined producers, and concurrent shutdown. Include writer lifecycle sanitizer tests in nimble test and update usage documentation. Validation: all 32 unit tests, eight writer exit scenarios, and writer lifecycle ThreadSanitizer tests pass. The full suite retains the known configuration refresh, timestamp, and default allocator failures.
65 lines
2.5 KiB
Nim
65 lines
2.5 KiB
Nim
import std/[os, strutils]
|
|
import ../src/namespaced_logging
|
|
|
|
when not defined(multithreaded):
|
|
{.error: "Compile this fixture with -d:multithreaded to exercise writer threads".}
|
|
|
|
proc formatLine(msg: LogMessage): string {.gcsafe.} = msg.msg & "\n"
|
|
|
|
proc formatOptionalLine(msg: LogMessage): string {.gcsafe.} =
|
|
if msg.msg.len > 0: msg.msg & "\n" else: ""
|
|
|
|
proc writeMessages(args: tuple[service: LogService, worker: int]) {.thread.} =
|
|
let logger = args.service.threadLocalRef().getLogger("shutdown")
|
|
for i in 0 ..< 2_500:
|
|
# Keep the timestamp race out of this writer lifecycle test.
|
|
logger.log(LogMessage(scope: "shutdown", level: lvlInfo,
|
|
msg: "message " & $(args.worker * 2_500 + i)))
|
|
|
|
proc main() =
|
|
doAssert paramCount() == 2
|
|
let destination = paramStr(1)
|
|
var service = initLogService()
|
|
case destination
|
|
of "file", "file-parallel", "file-empty":
|
|
service.addAppender(initFileLogAppender(paramStr(2)))
|
|
of "files":
|
|
service.addAppender(initFileLogAppender(paramStr(2), namespace = "first"))
|
|
service.addAppender(initFileLogAppender(paramStr(2) & ".second", namespace = "second"))
|
|
of "console", "console-parallel", "console-empty":
|
|
# Keep stdout open through the library's exit hook. Redirecting it to a
|
|
# file also prevents a full parent/child pipe from blocking the writer.
|
|
doAssert open(stdout, paramStr(2), fmWrite)
|
|
if destination == "console-empty":
|
|
service.addAppender(initConsoleLogAppender(formatter = formatOptionalLine))
|
|
else:
|
|
service.addAppender(initConsoleLogAppender(formatter = formatLine))
|
|
of "stderr":
|
|
doAssert open(stderr, paramStr(2), fmWrite)
|
|
service.addAppender(initConsoleLogAppender(useStderr = true, formatter = formatLine))
|
|
else:
|
|
raise newException(ValueError, "unknown writer destination")
|
|
|
|
if destination.endsWith("-parallel"):
|
|
var workers: array[4, Thread[tuple[service: LogService, worker: int]]]
|
|
for i in 0 ..< workers.len:
|
|
createThread(workers[i], writeMessages, (service, i))
|
|
joinThreads(workers)
|
|
else:
|
|
let local = service.threadLocalRef()
|
|
if destination == "files":
|
|
let first = local.getLogger("first")
|
|
let second = local.getLogger("second")
|
|
for i in 0 ..< 10_000:
|
|
first.info("message " & $i)
|
|
second.info("message " & $i)
|
|
else:
|
|
let logger = local.getLogger("shutdown")
|
|
for i in 0 ..< 10_000:
|
|
if destination.endsWith("-empty") and i mod 128 == 0:
|
|
logger.info("")
|
|
logger.info("message " & $i)
|
|
# No sleep or explicit flush: normal exit must preserve accepted messages.
|
|
|
|
main()
|