Define explicit ownership for custom appenders and formatters
Add SharedLogState for synchronized value data and factories for independently owned appender state and captured formatter closures. Reject unsafe configuration state, update the test collector to value snapshots, and document the breaking APIs in version 3.0.0. Expand ORC ownership and sanitizer coverage and reject stale test executables.
This commit is contained in:
1 parent
851caef706
commit
b2d0e9dade
11 files changed
+808
-110
No files matched your search
@@ -104,16 +104,44 @@ loggers upwards in the scope naming heirarchy. This pattern is explained in
|
|||||||
detail in the [logback documentation][effective logging level] and applies in
|
detail in the [logback documentation][effective logging level] and applies in
|
||||||
the same manner to loggers in this library.
|
the same manner to loggers in this library.
|
||||||
|
|
||||||
### LogMessageFormater
|
### LogFormatter
|
||||||
|
|
||||||
Both the [ConsoleLogAppender](#ConsoleLogAppender) and
|
Console, file, and std/logging appenders accept a *LogFormatter*. Noncapturing
|
||||||
[FileLogAppender](#FileLogAppender) can be given a *LogMessageFormatter* to
|
functions such as *formatSimpleTextLog* and *formatJsonStructuredLog* are
|
||||||
determine how a log message is formatted before being written.
|
converted automatically:
|
||||||
|
|
||||||
```nim
|
```nim
|
||||||
type LogMessageFormatter* = proc (msg: LogMessage): string {.gcsafe.}
|
let appender = initConsoleLogAppender(formatter = formatJsonStructuredLog)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
For a formatter that captures state, pass value configuration and a
|
||||||
|
noncapturing factory to *initLogFormatter*. The factory creates a separate
|
||||||
|
closure in each thread on first use:
|
||||||
|
|
||||||
|
```nim
|
||||||
|
proc makeFormatter(prefix: string): StatefulLogFormatterFunc =
|
||||||
|
var count = 0
|
||||||
|
result = proc(msg: LogMessage): string {.gcsafe.} =
|
||||||
|
inc count
|
||||||
|
prefix & $count & ": " & msg.msg
|
||||||
|
|
||||||
|
let appender = initConsoleLogAppender(
|
||||||
|
formatter = initLogFormatter("request ", makeFormatter))
|
||||||
|
```
|
||||||
|
|
||||||
|
The factory's input must contain value data, such as strings, sequences of
|
||||||
|
values, or *SharedLogState* handles. Ordinary `ref` values and closures are
|
||||||
|
rejected, including when nested inside other types. The returned closure may
|
||||||
|
capture independently created references and mutable state belonging to its
|
||||||
|
thread. It must not return a closure shared with another thread.
|
||||||
|
|
||||||
|
Each appender clone starts fresh formatter state; configuration refresh can
|
||||||
|
reate new clones. For counters or other data that must persist across
|
||||||
|
refreshes or be shared between threads, pass a *SharedLogState* handle as
|
||||||
|
factory input. To invoke a formatter directly, use
|
||||||
|
`formatter.format(message)`.
|
||||||
|
|
||||||
|
|
||||||
## Available Appenders
|
## Available Appenders
|
||||||
|
|
||||||
### ConsoleLogAppender
|
### ConsoleLogAppender
|
||||||
@@ -122,7 +150,7 @@ Used for writing logs to stdout or stderr.
|
|||||||
|
|
||||||
```nim
|
```nim
|
||||||
proc initConsoleLogAppender*(
|
proc initConsoleLogAppender*(
|
||||||
formatter = formatSimpleTextLog,
|
formatter: LogFormatter = nil, # defaults to formatSimpleTextLog
|
||||||
## formatJsonStructuredLog is another useful formatter provided
|
## formatJsonStructuredLog is another useful formatter provided
|
||||||
## or you can write your own
|
## or you can write your own
|
||||||
useStderr = false, ## stdout is used by default
|
useStderr = false, ## stdout is used by default
|
||||||
@@ -144,7 +172,7 @@ Used for writing logs to files.
|
|||||||
```nim
|
```nim
|
||||||
proc initFileLogAppender*(
|
proc initFileLogAppender*(
|
||||||
filePath: string,
|
filePath: string,
|
||||||
formatter = formatSimpleTextLog,
|
formatter: LogFormatter = nil, # defaults to formatSimpleTextLog
|
||||||
## formatJsonStructuredLog is another useful formatter provided
|
## formatJsonStructuredLog is another useful formatter provided
|
||||||
## or you can write your own
|
## or you can write your own
|
||||||
namespace = "",
|
namespace = "",
|
||||||
@@ -178,41 +206,75 @@ messages regardless of whether namespaced\_logging has other appenders by
|
|||||||
setting `fallbackOnly = false`.
|
setting `fallbackOnly = false`.
|
||||||
|
|
||||||
```nim
|
```nim
|
||||||
func initStdLoggingAppender*(
|
proc initStdLoggingAppender*(
|
||||||
fallbackOnly = true,
|
fallbackOnly = true,
|
||||||
formatter = formatForwardedLog,
|
formatter: LogFormatter = nil, # defaults to formatForwardedLog
|
||||||
namespace = "",
|
namespace = "",
|
||||||
threshold = lvlAll): StdLoggingAppender {.gcsafe.}
|
threshold = lvlAll): StdLoggingAppender {.gcsafe.}
|
||||||
```
|
```
|
||||||
|
|
||||||
### CustomLogAppender
|
### CustomLogAppender
|
||||||
|
|
||||||
Provides an extension point for custom logging implementations.
|
Provides an extension point for custom logging implementation.
|
||||||
|
|
||||||
|
Use *initCustomLogAppender* for state that can be copied by value. Its callback
|
||||||
|
receives `state: var T` and a *LogMessage*. Each clone owns a separate state
|
||||||
|
value. Ordinary `ref` values, closures, and embedded locks or atomics are
|
||||||
|
rejected, including inside containers and objects.
|
||||||
|
|
||||||
|
For mutable state shared by all workers, put value data in a *SharedLogState*:
|
||||||
|
|
||||||
```nim
|
```nim
|
||||||
func initCustomLogAppender*[T](
|
let count = initSharedLogState(0)
|
||||||
state: T, # arbitrary state needed for the appender
|
|
||||||
doLogMessage: CustomLogAppenderFunc[T],
|
proc countMessage(state: var SharedLogState[int], msg: LogMessage) {.gcsafe, noconv.} =
|
||||||
# custom log appender implementation
|
state.withState(proc(value: var int) = inc value)
|
||||||
namespace = "",
|
|
||||||
threshold = lvlAll): CustomLogAppender[T] {.gcsafe.} =
|
var service = initLogService()
|
||||||
|
service.addAppender(initCustomLogAppender(count, countMessage))
|
||||||
|
# Pass service copies to workers as described below.
|
||||||
|
# count.snapshot() reads the current count safely.
|
||||||
```
|
```
|
||||||
|
|
||||||
The `state` field allows you to explicitly pass in any data that is required
|
Copies of the handle share the same data and keep it alive. *withState* runs
|
||||||
for the custom functionality.
|
its callback synchronously with exclusive access; *snapshot* returns a value
|
||||||
|
copy made under the same lock. Neither operation allows unsynchronized access
|
||||||
|
to the stored payload. Do not retain a pointer to the callback's argument or
|
||||||
|
call back into the same handle from inside *withState*, since its lock is not
|
||||||
|
recursive.
|
||||||
|
|
||||||
> [!CAUTION]
|
The payload must contain value data without ordinary references, closures, or
|
||||||
> Custom state is currently copied by assignment when an appender is cloned.
|
embedded synchronization primitives. Strings and sequences of supported values
|
||||||
> Ordinary ORC `ref` values, including references nested in value types, must
|
are accepted. Raw pointers and types with custom copy/destruction hooks remain
|
||||||
> not be shared this way across threads. Keeping an original reference alive
|
an explicit caller responsibility: their lifetime and synchronization must be
|
||||||
> does not protect reference counts. A safe custom-state and captured-formatter
|
safe across threads. *SharedLogState* does not make a raw pointer's target
|
||||||
> ownership API is still pending; use independently owned values or an explicit
|
safe.
|
||||||
> shared-state abstraction with a synchronized lifetime and access protocol.
|
|
||||||
|
|
||||||
See [testutil][] and the unit tests in [namespaced\_logging][nsl-unit-tests]
|
The [testutil][] collector demonstrates shared state using value snapshots.
|
||||||
for a single-threaded example; the test collector's lock does not make its
|
*LoggedMessages.get()* returns `seq[RecordedLogMessage]`: timestamps are
|
||||||
reference-counted state safe to share across threads.
|
*Time* values, additional JSON data is serialized text, and exceptions retain
|
||||||
|
name, message, and stacktrace text. Returned snapshots can be inspected and
|
||||||
|
modified independently without sharing the original message's references.
|
||||||
|
|
||||||
|
### FactoryCustomLogAppender
|
||||||
|
|
||||||
|
Provides an extension point for custom logging with state created separately
|
||||||
|
in each logging thread. Use this when state needs ordinary references or
|
||||||
|
closures owned by that thread.
|
||||||
|
|
||||||
|
Call *initFactoryCustomLogAppender(seed, factory, doLogMessage)* during logging
|
||||||
|
setup. The noncapturing factory receives a copyable value seed and creates
|
||||||
|
independently owned state on the first message handled by each clone, in the
|
||||||
|
thread doing the logging. The logging callback receives `state: var T` and a
|
||||||
|
*LogMessage*.
|
||||||
|
|
||||||
|
The seed follows the same ownership restrictions as *CustomLogAppender* state;
|
||||||
|
it may include *SharedLogState* handles. The state returned by the factory may
|
||||||
|
contain independently created references and closures. State lasts for that
|
||||||
|
clone's lifetime; configuration refresh may replace it. A factory that raises
|
||||||
|
is retried on the next message, and errors go to the configured error handler.
|
||||||
|
Do not retain managed references from an incoming *LogMessage* for use in
|
||||||
|
another thread.
|
||||||
|
|
||||||
## Notes on Use in Multi-Threaded Applications
|
## Notes on Use in Multi-Threaded Applications
|
||||||
|
|
||||||
@@ -247,12 +309,13 @@ subsequent logging calls in every thread using that service, without recreating
|
|||||||
loggers or configuring each worker separately.
|
loggers or configuring each worker separately.
|
||||||
|
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> Thread-safety work is still in progress. For now, use the built-in appenders
|
> Thread-safety work is still in progress. Custom state and captured
|
||||||
> with default or stateless formatters when sharing a service across threads.
|
> formatters must follow the ownership rules above. Runtime configuration
|
||||||
> Custom appenders with shared reference-based state and formatters with
|
> refresh, concurrent local-time timestamp construction, and delivery of queued
|
||||||
> captured state are not yet generally safe. Runtime configuration refresh
|
> background output at application exit still have known failures. Nim 2.2.10
|
||||||
> and delivery of queued background output at application exit also have known
|
> also produces sanitizer reports in its default allocator during some
|
||||||
> failures. See [the regression tests](tests/README.md) for the remaining work.
|
> cross-thread allocation patterns. See
|
||||||
|
> [the regression tests](tests/README.md) for the remaining work.
|
||||||
|
|
||||||
|
|
||||||
## Architectural Design
|
## Architectural Design
|
||||||
@@ -335,10 +398,13 @@ configuration. This internal type is not exposed to library users but manages:
|
|||||||
detection
|
detection
|
||||||
|
|
||||||
Console and file writers remain process-wide. The shared control block owns
|
Console and file writers remain process-wide. The shared control block owns
|
||||||
appender prototypes; refresh borrows them under the configuration lock and calls
|
appender prototypes; refresh borrows them under the configuration lock and
|
||||||
`clone()` to create local instances. Clones must not retain ordinary managed
|
calls `clone()` to create local instances. Clones must not retain ordinary
|
||||||
references shared across threads. That custom-state and formatter contract is
|
managed references shared across threads. Built-in custom appenders copy
|
||||||
the next part of the ownership work.
|
checked value state or shared-state handles. Factory appenders and formatters
|
||||||
|
copy value seeds and create their managed state lazily within the thread that
|
||||||
|
uses each clone. The state of an already-used clone is never copied into
|
||||||
|
another clone.
|
||||||
|
|
||||||
### Thread Safety Model
|
### Thread Safety Model
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Package
|
# Package
|
||||||
|
|
||||||
version = "2.1.2"
|
version = "3.0.0"
|
||||||
author = "Jonathan Bernard"
|
author = "Jonathan Bernard"
|
||||||
description = "Wrapper around std/logging to provide namespaced logging."
|
description = "Wrapper around std/logging to provide namespaced logging."
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
@@ -15,16 +15,28 @@ requires @["nim >= 2.2.0", "zero_functional"]
|
|||||||
requires "timeutils"
|
requires "timeutils"
|
||||||
|
|
||||||
proc runUnitTests() =
|
proc runUnitTests() =
|
||||||
|
if fileExists("src/namespaced_logging.out"): rmFile("src/namespaced_logging.out")
|
||||||
exec "nimble c --mm:orc --threads:on src/namespaced_logging.nim"
|
exec "nimble c --mm:orc --threads:on src/namespaced_logging.nim"
|
||||||
|
if not fileExists("src/namespaced_logging.out"):
|
||||||
|
raise newException(OSError, "Unit test compilation produced no executable")
|
||||||
exec "src/namespaced_logging.out"
|
exec "src/namespaced_logging.out"
|
||||||
|
|
||||||
proc runRegressionTests() =
|
proc runRegressionTests() =
|
||||||
|
if fileExists("tests/bin/writer_child"): rmFile("tests/bin/writer_child")
|
||||||
|
if fileExists("tests/bin/regressions"): rmFile("tests/bin/regressions")
|
||||||
exec "nimble c --mm:orc --threads:on -d:multithreaded --out:tests/bin/writer_child tests/writer_child.nim"
|
exec "nimble c --mm:orc --threads:on -d:multithreaded --out:tests/bin/writer_child tests/writer_child.nim"
|
||||||
|
if not fileExists("tests/bin/writer_child"):
|
||||||
|
raise newException(OSError, "Writer test compilation produced no executable")
|
||||||
exec "nimble c --mm:orc --threads:on --out:tests/bin/regressions tests/regressions.nim"
|
exec "nimble c --mm:orc --threads:on --out:tests/bin/regressions tests/regressions.nim"
|
||||||
|
if not fileExists("tests/bin/regressions"):
|
||||||
|
raise newException(OSError, "Regression test compilation produced no executable")
|
||||||
exec "tests/bin/regressions"
|
exec "tests/bin/regressions"
|
||||||
|
|
||||||
proc runThreadSafetyTests() =
|
proc runThreadSafetyTests() =
|
||||||
|
if fileExists("tests/bin/thread_safety"): rmFile("tests/bin/thread_safety")
|
||||||
exec "nimble c --mm:orc --threads:on --passC:-fsanitize=thread --passC:-g --passL:-fsanitize=thread --out:tests/bin/thread_safety tests/thread_safety.nim"
|
exec "nimble c --mm:orc --threads:on --passC:-fsanitize=thread --passC:-g --passL:-fsanitize=thread --out:tests/bin/thread_safety tests/thread_safety.nim"
|
||||||
|
if not fileExists("tests/bin/thread_safety"):
|
||||||
|
raise newException(OSError, "Thread-safety test compilation produced no executable")
|
||||||
exec "bash tests/run_tsan.sh tests/bin/thread_safety"
|
exec "bash tests/run_tsan.sh tests/bin/thread_safety"
|
||||||
|
|
||||||
task test, "Run all tests, including ThreadSanitizer, and report all failing suites.":
|
task test, "Run all tests, including ThreadSanitizer, and report all failing suites.":
|
||||||
|
|||||||
+130
-24
@@ -2,6 +2,8 @@ import std/[algorithm, atomics, exitprocs, json, locks, options, os, paths,
|
|||||||
sequtils, strutils, tables, times]
|
sequtils, strutils, tables, times]
|
||||||
import timeutils
|
import timeutils
|
||||||
import std/logging as stdlog
|
import std/logging as stdlog
|
||||||
|
import namespaced_logging/ownership
|
||||||
|
export ownership
|
||||||
|
|
||||||
from std/logging import Level
|
from std/logging import Level
|
||||||
export Level
|
export Level
|
||||||
@@ -75,7 +77,20 @@ type
|
|||||||
additionalData*: JsonNode
|
additionalData*: JsonNode
|
||||||
|
|
||||||
|
|
||||||
LogMessageFormatter* = proc (msg: LogMessage): string {.gcsafe.}
|
StatefulLogFormatterFunc* = proc (msg: LogMessage): string {.gcsafe.}
|
||||||
|
|
||||||
|
StatelessLogFormatterFunc* = proc (msg: LogMessage): string {.gcsafe, nimcall.}
|
||||||
|
|
||||||
|
LogFormatter* = ref object of RootObj
|
||||||
|
## Thread-confined formatter. clone must create independently owned state.
|
||||||
|
|
||||||
|
StatelessLogFormatterImpl = ref object of LogFormatter
|
||||||
|
callback: StatelessLogFormatterFunc
|
||||||
|
|
||||||
|
FactoryLogFormatterImpl[S] = ref object of LogFormatter
|
||||||
|
seed: S
|
||||||
|
factory: proc(seed: S): StatefulLogFormatterFunc {.gcsafe, nimcall.}
|
||||||
|
local: StatefulLogFormatterFunc
|
||||||
|
|
||||||
ConsoleMessage = object
|
ConsoleMessage = object
|
||||||
msg: string
|
msg: string
|
||||||
@@ -98,7 +113,7 @@ type
|
|||||||
## Log appender that writes log messages to the console. See
|
## Log appender that writes log messages to the console. See
|
||||||
## *initConsoleLogAppender* for a convenient way to create instances of
|
## *initConsoleLogAppender* for a convenient way to create instances of
|
||||||
## this appender.
|
## this appender.
|
||||||
formatter*: LogMessageFormatter
|
formatter*: LogFormatter
|
||||||
## Formatter allows for custom formatting of log messages. The default
|
## Formatter allows for custom formatting of log messages. The default
|
||||||
## formatter uses *formatSimpleTextLog* which just writes the log message.
|
## formatter uses *formatSimpleTextLog* which just writes the log message.
|
||||||
## *formatJsonStructuredLog* is also provided to support writing logs out
|
## *formatJsonStructuredLog* is also provided to support writing logs out
|
||||||
@@ -106,14 +121,22 @@ type
|
|||||||
useStderr*: bool
|
useStderr*: bool
|
||||||
|
|
||||||
CustomLogAppenderFunc*[T] =
|
CustomLogAppenderFunc*[T] =
|
||||||
proc(state: T, msg: LogMessage) {.gcsafe, noconv.}
|
proc(state: var T, msg: LogMessage) {.gcsafe, noconv.}
|
||||||
|
|
||||||
CustomLogAppender*[T] = ref object of LogAppender
|
CustomLogAppender*[T] = ref object of LogAppender
|
||||||
doLogMessage*: CustomLogAppenderFunc[T]
|
doLogMessage*: CustomLogAppenderFunc[T]
|
||||||
state*: T
|
state*: T
|
||||||
|
|
||||||
|
FactoryCustomLogAppender*[S, T] = ref object of LogAppender
|
||||||
|
## Creates state lazily in the thread performing the first append.
|
||||||
|
seed: S
|
||||||
|
factory: proc(seed: S): T {.gcsafe, nimcall.}
|
||||||
|
doLogMessage: CustomLogAppenderFunc[T]
|
||||||
|
local: T
|
||||||
|
initialized: bool
|
||||||
|
|
||||||
FileLogAppender* = ref object of LogAppender
|
FileLogAppender* = ref object of LogAppender
|
||||||
formatter*: LogMessageFormatter
|
formatter*: LogFormatter
|
||||||
absPath*: Path
|
absPath*: Path
|
||||||
|
|
||||||
StdLoggingAppender* = ref object of LogAppender
|
StdLoggingAppender* = ref object of LogAppender
|
||||||
@@ -127,7 +150,7 @@ type
|
|||||||
## when true, only forward to std/logging where there are no appenders
|
## when true, only forward to std/logging where there are no appenders
|
||||||
## configured on the related LogService
|
## configured on the related LogService
|
||||||
|
|
||||||
formatter*: LogMessageFormatter
|
formatter*: LogFormatter
|
||||||
|
|
||||||
const UninitializedConfigVersion = low(int)
|
const UninitializedConfigVersion = low(int)
|
||||||
|
|
||||||
@@ -698,16 +721,62 @@ proc initThreadedFileLogging() =
|
|||||||
fileLogging.initialized.store(true)
|
fileLogging.initialized.store(true)
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# LogFormatter Implementation
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
method clone*(formatter: LogFormatter): LogFormatter {.base, gcsafe.} =
|
||||||
|
raise newException(CatchableError, "missing formatter clone implementation")
|
||||||
|
|
||||||
|
method format*(formatter: LogFormatter, msg: LogMessage): string {.base, gcsafe.} =
|
||||||
|
raise newException(CatchableError, "missing formatter implementation")
|
||||||
|
|
||||||
|
method clone(formatter: StatelessLogFormatterImpl): LogFormatter {.gcsafe.} =
|
||||||
|
StatelessLogFormatterImpl(callback: formatter.callback)
|
||||||
|
|
||||||
|
method format(formatter: StatelessLogFormatterImpl, msg: LogMessage): string {.gcsafe.} =
|
||||||
|
formatter.callback(msg)
|
||||||
|
|
||||||
|
proc initLogFormatter*(callback: StatelessLogFormatterFunc): LogFormatter =
|
||||||
|
if callback.isNil:
|
||||||
|
raise newException(ValueError, "Log formatter callback is nil")
|
||||||
|
StatelessLogFormatterImpl(callback: callback)
|
||||||
|
|
||||||
|
converter toLogFormatter*(callback: StatelessLogFormatterFunc): LogFormatter =
|
||||||
|
## Noncapturing formatter functions remain convenient constructor arguments.
|
||||||
|
initLogFormatter(callback)
|
||||||
|
|
||||||
|
method clone[S](formatter: FactoryLogFormatterImpl[S]): LogFormatter {.gcsafe.} =
|
||||||
|
FactoryLogFormatterImpl[S](seed: formatter.seed, factory: formatter.factory)
|
||||||
|
|
||||||
|
method format[S](formatter: FactoryLogFormatterImpl[S], msg: LogMessage): string {.gcsafe.} =
|
||||||
|
if formatter.local.isNil:
|
||||||
|
formatter.local = formatter.factory(formatter.seed)
|
||||||
|
if formatter.local.isNil:
|
||||||
|
raise newException(ValueError, "Log formatter factory returned nil")
|
||||||
|
formatter.local(msg)
|
||||||
|
|
||||||
|
proc initLogFormatter*[S](seed: S,
|
||||||
|
factory: proc(seed: S): StatefulLogFormatterFunc {.gcsafe, nimcall.}): LogFormatter =
|
||||||
|
## The seed is copied by value. The noncapturing factory creates a closure
|
||||||
|
## separately for each clone on first use; live closure state is never copied.
|
||||||
|
requireThreadValue(S)
|
||||||
|
if factory.isNil:
|
||||||
|
raise newException(ValueError, "Log formatter factory is nil")
|
||||||
|
FactoryLogFormatterImpl[S](seed: seed, factory: factory)
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# CustomLogAppender Implementation
|
# CustomLogAppender Implementation
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
func initCustomLogAppender*[T](
|
proc initCustomLogAppender*[T](
|
||||||
state: T,
|
state: T,
|
||||||
doLogMessage: CustomLogAppenderFunc[T],
|
doLogMessage: CustomLogAppenderFunc[T],
|
||||||
namespace = "",
|
namespace = "",
|
||||||
threshold = lvlAll): CustomLogAppender[T] {.gcsafe.} =
|
threshold = lvlAll): CustomLogAppender[T] {.gcsafe.} =
|
||||||
|
## Copy value state into each appender clone. For shared mutable state, pass
|
||||||
|
## a SharedLogState handle. For local refs or closures, use a factory.
|
||||||
|
requireThreadValue(T)
|
||||||
if doLogMessage.isNil:
|
if doLogMessage.isNil:
|
||||||
raise newException(ValueError, "initCustomLogAppender: doLogMessage is nil")
|
raise newException(ValueError, "initCustomLogAppender: doLogMessage is nil")
|
||||||
|
|
||||||
@@ -718,6 +787,7 @@ func initCustomLogAppender*[T](
|
|||||||
state: state)
|
state: state)
|
||||||
|
|
||||||
method clone*[T](cla: CustomLogAppender[T]): LogAppender {.gcsafe.} =
|
method clone*[T](cla: CustomLogAppender[T]): LogAppender {.gcsafe.} =
|
||||||
|
requireThreadValue(T)
|
||||||
assert not cla.doLogMessage.isNil,
|
assert not cla.doLogMessage.isNil,
|
||||||
"CustomLogAppender#clone: source doLogMessage is nil"
|
"CustomLogAppender#clone: source doLogMessage is nil"
|
||||||
|
|
||||||
@@ -741,20 +811,51 @@ method appendLogMessage[T](
|
|||||||
getCurrentException(),
|
getCurrentException(),
|
||||||
"unable to append to CustomLogAppender")
|
"unable to append to CustomLogAppender")
|
||||||
|
|
||||||
|
proc initFactoryCustomLogAppender*[S, T](seed: S,
|
||||||
|
factory: proc(seed: S): T {.gcsafe, nimcall.},
|
||||||
|
doLogMessage: CustomLogAppenderFunc[T],
|
||||||
|
namespace = "", threshold = lvlAll): FactoryCustomLogAppender[S, T] =
|
||||||
|
## Only the seed crosses threads. State returned by the factory may contain
|
||||||
|
## refs and closures, and must be independently owned by the calling thread.
|
||||||
|
requireThreadValue(S)
|
||||||
|
if factory.isNil or doLogMessage.isNil:
|
||||||
|
raise newException(ValueError, "Factory custom appender callbacks must not be nil")
|
||||||
|
FactoryCustomLogAppender[S, T](seed: seed, factory: factory,
|
||||||
|
doLogMessage: doLogMessage, namespace: namespace, threshold: threshold)
|
||||||
|
|
||||||
|
method clone[S, T](app: FactoryCustomLogAppender[S, T]): LogAppender {.gcsafe.} =
|
||||||
|
FactoryCustomLogAppender[S, T](seed: app.seed, factory: app.factory,
|
||||||
|
doLogMessage: app.doLogMessage, namespace: app.namespace, threshold: app.threshold)
|
||||||
|
|
||||||
|
method appendLogMessage[S, T](app: FactoryCustomLogAppender[S, T],
|
||||||
|
ls: ThreadLocalLogService, msg: LogMessage) {.gcsafe.} =
|
||||||
|
try:
|
||||||
|
if not app.initialized:
|
||||||
|
app.local = app.factory(app.seed)
|
||||||
|
app.initialized = true
|
||||||
|
app.doLogMessage(app.local, msg)
|
||||||
|
except Exception:
|
||||||
|
ls.shared.reportLoggingError(getCurrentException(),
|
||||||
|
"unable to append to FactoryCustomLogAppender")
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# ConsoleLogAppender Implementation
|
# ConsoleLogAppender Implementation
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
proc initConsoleLogAppender*(
|
proc initConsoleLogAppender*(
|
||||||
formatter = formatSimpleTextLog,
|
formatter: LogFormatter = nil,
|
||||||
useStderr = false,
|
useStderr = false,
|
||||||
namespace = "",
|
namespace = "",
|
||||||
threshold = lvlAll): ConsoleLogAppender {.gcsafe.} =
|
threshold = lvlAll): ConsoleLogAppender {.gcsafe.} =
|
||||||
|
## Initialize a new ConsoleLogAppender. formatter defaults to
|
||||||
|
## formatSimpleTextLog. formatJsonStructuredLog is another useful formatter
|
||||||
|
## provided (or you can write your own)
|
||||||
|
|
||||||
result = ConsoleLogAppender(
|
result = ConsoleLogAppender(
|
||||||
namespace: namespace,
|
namespace: namespace,
|
||||||
threshold: threshold,
|
threshold: threshold,
|
||||||
formatter: formatter,
|
formatter: (if formatter.isNil: initLogFormatter(formatSimpleTextLog)
|
||||||
|
else: clone(formatter)),
|
||||||
useStderr: useStderr)
|
useStderr: useStderr)
|
||||||
|
|
||||||
|
|
||||||
@@ -762,7 +863,7 @@ method clone*(cla: ConsoleLogAppender): LogAppender {.gcsafe.} =
|
|||||||
result = ConsoleLogAppender(
|
result = ConsoleLogAppender(
|
||||||
namespace: cla.namespace,
|
namespace: cla.namespace,
|
||||||
threshold: cla.threshold,
|
threshold: cla.threshold,
|
||||||
formatter: cla.formatter,
|
formatter: clone(cla.formatter),
|
||||||
useStderr: cla.useStderr)
|
useStderr: cla.useStderr)
|
||||||
|
|
||||||
|
|
||||||
@@ -776,15 +877,15 @@ proc appendLogMessageMultiThreaded(
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
consoleLogging.chan.send(ConsoleMessage(
|
consoleLogging.chan.send(ConsoleMessage(
|
||||||
msg: cla.formatter(msg),
|
msg: cla.formatter.format(msg),
|
||||||
useStderr: cla.useStderr))
|
useStderr: cla.useStderr))
|
||||||
except Exception:
|
except Exception:
|
||||||
try:
|
try:
|
||||||
let output =
|
let output =
|
||||||
if cla.useStderr: stderr
|
if cla.useStderr: stderr
|
||||||
else: stdout
|
else: stdout
|
||||||
output.writeLine(cla.formatter(msg))
|
output.writeLine(cla.formatter.format(msg))
|
||||||
output.writeLine(cla.formatter(LogMessage(
|
output.writeLine(cla.formatter.format(LogMessage(
|
||||||
scope: "namespaced_logging",
|
scope: "namespaced_logging",
|
||||||
level: lvlError,
|
level: lvlError,
|
||||||
err: some(getCurrentException()),
|
err: some(getCurrentException()),
|
||||||
@@ -804,7 +905,7 @@ proc appendLogMessageSingleThreaded(
|
|||||||
let output =
|
let output =
|
||||||
if cla.useStderr: stderr
|
if cla.useStderr: stderr
|
||||||
else: stdout
|
else: stdout
|
||||||
output.writeLine(cla.formatter(msg))
|
output.writeLine(cla.formatter.format(msg))
|
||||||
output.flushFile()
|
output.flushFile()
|
||||||
except Exception: discard
|
except Exception: discard
|
||||||
|
|
||||||
@@ -832,14 +933,18 @@ method appendLogMessage(
|
|||||||
|
|
||||||
proc initFileLogAppender*(
|
proc initFileLogAppender*(
|
||||||
filePath: string,
|
filePath: string,
|
||||||
formatter = formatSimpleTextLog,
|
formatter: LogFormatter = nil,
|
||||||
namespace = "",
|
namespace = "",
|
||||||
threshold = lvlAll): FileLogAppender {.gcsafe.} =
|
threshold = lvlAll): FileLogAppender {.gcsafe.} =
|
||||||
|
## Initialize a new FileLogAppender. formatter defaults to
|
||||||
|
## formatSimpleTextLog. formatJsonStructuredLog is another useful formatter
|
||||||
|
## provided (or you can write your own)
|
||||||
|
|
||||||
result = FileLogAppender(
|
result = FileLogAppender(
|
||||||
namespace: namespace,
|
namespace: namespace,
|
||||||
threshold: threshold,
|
threshold: threshold,
|
||||||
formatter: formatter,
|
formatter: (if formatter.isNil: initLogFormatter(formatSimpleTextLog)
|
||||||
|
else: clone(formatter)),
|
||||||
absPath: absolutePath(Path(filePath)))
|
absPath: absolutePath(Path(filePath)))
|
||||||
|
|
||||||
|
|
||||||
@@ -847,7 +952,7 @@ method clone*(fla: FileLogAppender): LogAppender {.gcsafe.} =
|
|||||||
result = FileLogAppender(
|
result = FileLogAppender(
|
||||||
namespace: fla.namespace,
|
namespace: fla.namespace,
|
||||||
threshold: fla.threshold,
|
threshold: fla.threshold,
|
||||||
formatter: fla.formatter,
|
formatter: clone(fla.formatter),
|
||||||
absPath: fla.absPath)
|
absPath: fla.absPath)
|
||||||
|
|
||||||
|
|
||||||
@@ -861,7 +966,7 @@ proc appendLogMessageMultiThreaded(
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
fileLogging.chan.send(FileMessage(
|
fileLogging.chan.send(FileMessage(
|
||||||
msg: fla.formatter(msg),
|
msg: fla.formatter.format(msg),
|
||||||
absPath: fla.absPath))
|
absPath: fla.absPath))
|
||||||
except Exception: discard
|
except Exception: discard
|
||||||
|
|
||||||
@@ -874,7 +979,7 @@ proc appendLogMessageSingleThreaded(
|
|||||||
try:
|
try:
|
||||||
var f: File
|
var f: File
|
||||||
if not open(f, $fla.absPath, fmAppend): return
|
if not open(f, $fla.absPath, fmAppend): return
|
||||||
writeLine(f, fla.formatter(msg))
|
writeLine(f, fla.formatter.format(msg))
|
||||||
flushFile(f)
|
flushFile(f)
|
||||||
close(f)
|
close(f)
|
||||||
except Exception: discard
|
except Exception: discard
|
||||||
@@ -907,9 +1012,9 @@ func formatForwardedLog*(lm: LogMessage): string =
|
|||||||
"[" & lm.scope & "] " & formatSimpleTextLog(lm)
|
"[" & lm.scope & "] " & formatSimpleTextLog(lm)
|
||||||
|
|
||||||
|
|
||||||
func initStdLoggingAppender*(
|
proc initStdLoggingAppender*(
|
||||||
fallbackOnly = true,
|
fallbackOnly = true,
|
||||||
formatter = formatForwardedLog,
|
formatter: LogFormatter = nil,
|
||||||
namespace = "",
|
namespace = "",
|
||||||
threshold = lvlAll): StdLoggingAppender {.gcsafe.} =
|
threshold = lvlAll): StdLoggingAppender {.gcsafe.} =
|
||||||
|
|
||||||
@@ -917,7 +1022,8 @@ func initStdLoggingAppender*(
|
|||||||
namespace: namespace,
|
namespace: namespace,
|
||||||
threshold: threshold,
|
threshold: threshold,
|
||||||
fallbackOnly: fallbackOnly,
|
fallbackOnly: fallbackOnly,
|
||||||
formatter: formatter)
|
formatter: (if formatter.isNil: initLogFormatter(formatForwardedLog)
|
||||||
|
else: clone(formatter)))
|
||||||
|
|
||||||
|
|
||||||
method clone*(sla: StdLoggingAppender): LogAppender {.gcsafe.} =
|
method clone*(sla: StdLoggingAppender): LogAppender {.gcsafe.} =
|
||||||
@@ -925,7 +1031,7 @@ method clone*(sla: StdLoggingAppender): LogAppender {.gcsafe.} =
|
|||||||
namespace: sla.namespace,
|
namespace: sla.namespace,
|
||||||
threshold: sla.threshold,
|
threshold: sla.threshold,
|
||||||
fallbackOnly: sla.fallbackOnly,
|
fallbackOnly: sla.fallbackOnly,
|
||||||
formatter: sla.formatter)
|
formatter: clone(sla.formatter))
|
||||||
|
|
||||||
|
|
||||||
method appendLogMessage*(
|
method appendLogMessage*(
|
||||||
@@ -935,7 +1041,7 @@ method appendLogMessage*(
|
|||||||
|
|
||||||
if sla.fallbackOnly and ls.appenders.len > 1: return
|
if sla.fallbackOnly and ls.appenders.len > 1: return
|
||||||
|
|
||||||
stdlog.log(msg.level, sla.formatter(msg))
|
stdlog.log(msg.level, sla.formatter.format(msg))
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
## Ownership for custom logging state shared between threads under ORC.
|
||||||
|
import std/[atomics, locks, typetraits]
|
||||||
|
|
||||||
|
proc checkThreadValue[T]() =
|
||||||
|
when T is Lock or T is Cond or T is Atomic:
|
||||||
|
{.error: "Do not copy synchronization primitives as logging configuration".}
|
||||||
|
elif T is ref:
|
||||||
|
{.error: "Logging configuration cannot contain ref values; use a thread-local factory or SharedLogState with value data".}
|
||||||
|
elif T is string:
|
||||||
|
discard
|
||||||
|
elif T is seq:
|
||||||
|
checkThreadValue[typeof(default(T)[0])]()
|
||||||
|
elif T is array:
|
||||||
|
checkThreadValue[typeof(default(T)[low(T)])]()
|
||||||
|
elif T is object or T is tuple:
|
||||||
|
for field in fields(default(T)):
|
||||||
|
checkThreadValue[typeof(field)]()
|
||||||
|
elif supportsCopyMem(T):
|
||||||
|
discard
|
||||||
|
else:
|
||||||
|
{.error: "Logging configuration cannot contain closures; create them with a thread-local factory".}
|
||||||
|
|
||||||
|
template requireThreadValue*(T: typedesc) =
|
||||||
|
## Reject managed references and closures, including nested ones. Strings
|
||||||
|
## and sequences have value semantics under ORC. Raw pointers and custom
|
||||||
|
## copy hooks still require the caller's lifetime and synchronization contract.
|
||||||
|
## Type-check only: inspecting fields must not construct a payload at runtime.
|
||||||
|
when not compiles(block:
|
||||||
|
checkThreadValue[T]()
|
||||||
|
):
|
||||||
|
{.error: "Logging configuration must be value data without refs, closures, or synchronization primitives; use SharedLogState or a thread-local factory".}
|
||||||
|
|
||||||
|
type
|
||||||
|
SharedLogStateData[T] = object
|
||||||
|
owners: Atomic[int]
|
||||||
|
lock: Lock
|
||||||
|
value: T
|
||||||
|
|
||||||
|
SharedLogState*[T] = object
|
||||||
|
## Copyable owner of synchronized value data. Each thread owns its own
|
||||||
|
## handle. The payload cannot contain ordinary refs or captured closures.
|
||||||
|
data: ptr SharedLogStateData[T]
|
||||||
|
|
||||||
|
proc release[T](data: ptr SharedLogStateData[T]) =
|
||||||
|
if data != nil and data.owners.fetchSub(1, moAcquireRelease) == 1:
|
||||||
|
`=destroy`(data.value)
|
||||||
|
deinitLock(data.lock)
|
||||||
|
deallocShared(data)
|
||||||
|
|
||||||
|
proc `=destroy`*[T](state: SharedLogState[T]) = release(state.data)
|
||||||
|
|
||||||
|
proc `=copy`*[T](dest: var SharedLogState[T], source: SharedLogState[T]) =
|
||||||
|
if dest.data == source.data: return
|
||||||
|
if source.data != nil:
|
||||||
|
discard source.data.owners.fetchAdd(1, moRelaxed)
|
||||||
|
release(dest.data)
|
||||||
|
dest.data = source.data
|
||||||
|
|
||||||
|
proc `=dup`*[T](source: SharedLogState[T]): SharedLogState[T] =
|
||||||
|
if source.data != nil:
|
||||||
|
discard source.data.owners.fetchAdd(1, moRelaxed)
|
||||||
|
result.data = source.data
|
||||||
|
|
||||||
|
proc `=wasMoved`*[T](state: var SharedLogState[T]) = state.data = nil
|
||||||
|
|
||||||
|
proc initSharedLogState*[T](value: sink T): SharedLogState[T] =
|
||||||
|
requireThreadValue(T)
|
||||||
|
result.data = cast[ptr SharedLogStateData[T]](
|
||||||
|
allocShared0(sizeof(SharedLogStateData[T])))
|
||||||
|
result.data.owners.store(1)
|
||||||
|
initLock(result.data.lock)
|
||||||
|
result.data.value = value
|
||||||
|
|
||||||
|
proc withState*[T](state: SharedLogState[T],
|
||||||
|
action: proc(value: var T) {.gcsafe.}) =
|
||||||
|
## Execute synchronously under the state's lock. Do not retain a pointer to
|
||||||
|
## value or re-enter this same state from action. Exceptions release the lock.
|
||||||
|
if state.data == nil:
|
||||||
|
raise newException(ValueError, "SharedLogState is not initialized")
|
||||||
|
if action.isNil:
|
||||||
|
raise newException(ValueError, "SharedLogState callback is nil")
|
||||||
|
# The callback can release its caller's handle. Retain an explicit owner
|
||||||
|
# until after the lock is released; a borrowed parameter is insufficient.
|
||||||
|
let owner = `=dup`(state)
|
||||||
|
withLock owner.data.lock:
|
||||||
|
action(owner.data.value)
|
||||||
|
|
||||||
|
proc snapshot*[T](state: SharedLogState[T]): T =
|
||||||
|
## Return an independently owned value copy, made under the state's lock.
|
||||||
|
if state.data == nil:
|
||||||
|
raise newException(ValueError, "SharedLogState is not initialized")
|
||||||
|
withLock state.data.lock:
|
||||||
|
result = state.data.value
|
||||||
@@ -1,42 +1,51 @@
|
|||||||
import std/[locks, sequtils, syncio, os, times]
|
import std/[json, options, syncio, os, times]
|
||||||
from logging import Level
|
from logging import Level
|
||||||
from ../namespaced_logging import CustomLogAppender, initCustomLogAppender, LogMessage
|
from ../namespaced_logging import CustomLogAppender, initCustomLogAppender, LogMessage
|
||||||
|
import ./ownership
|
||||||
|
|
||||||
type
|
type
|
||||||
LoggedMessages* = ref object
|
RecordedException* = object
|
||||||
messages*: seq[LogMessage]
|
name*, msg*, stacktrace*: string
|
||||||
lock: Lock
|
|
||||||
|
RecordedLogMessage* = object
|
||||||
|
## A value snapshot: JSON is serialized, exceptions retain diagnostic text,
|
||||||
|
## and timestamps retain the instant without sharing a Timezone reference.
|
||||||
|
scope*: string
|
||||||
|
level*: Level
|
||||||
|
ts*: Time
|
||||||
|
msg*: string
|
||||||
|
err*: Option[RecordedException]
|
||||||
|
additionalData*: string
|
||||||
|
|
||||||
|
LoggedMessages* = SharedLogState[seq[RecordedLogMessage]]
|
||||||
|
|
||||||
proc initLoggedMessages*(): LoggedMessages =
|
proc initLoggedMessages*(): LoggedMessages =
|
||||||
result = LoggedMessages(messages: @[])
|
initSharedLogState(newSeq[RecordedLogMessage]())
|
||||||
initLock(result.lock)
|
|
||||||
|
|
||||||
|
|
||||||
proc add*(lm: LoggedMessages, msg: LogMessage) =
|
proc add*(lm: LoggedMessages, msg: LogMessage) =
|
||||||
withLock lm.lock: lm.messages.add(msg)
|
var record = RecordedLogMessage(scope: msg.scope, level: msg.level,
|
||||||
|
ts: msg.ts.toTime, msg: msg.msg,
|
||||||
|
additionalData: (if msg.additionalData.isNil: "null" else: $msg.additionalData))
|
||||||
|
if msg.err.isSome:
|
||||||
|
let err = msg.err.get
|
||||||
|
record.err = some(RecordedException(name: $err.name, msg: err.msg,
|
||||||
|
stacktrace: $err.trace))
|
||||||
|
lm.withState(proc(messages: var seq[RecordedLogMessage]) = messages.add(record))
|
||||||
|
|
||||||
proc clear*(lm: LoggedMessages) =
|
proc clear*(lm: LoggedMessages) =
|
||||||
withLock lm.lock: lm.messages = @[]
|
lm.withState(proc(messages: var seq[RecordedLogMessage]) = messages.setLen(0))
|
||||||
|
|
||||||
|
proc get*(lm: LoggedMessages): seq[RecordedLogMessage] = lm.snapshot()
|
||||||
|
|
||||||
proc get*(lm: LoggedMessages): seq[LogMessage] =
|
proc testLogAppenderProc(state: var LoggedMessages, msg: LogMessage) {.gcsafe, noconv.} =
|
||||||
withLock lm.lock: return lm.messages.mapIt(it)
|
|
||||||
|
|
||||||
|
|
||||||
proc testLogAppenderProc(state: LoggedMessages, msg: LogMessage) {.gcsafe, noconv.} =
|
|
||||||
state.add(msg)
|
state.add(msg)
|
||||||
|
|
||||||
proc initTestLogAppender*(
|
proc initTestLogAppender*(
|
||||||
lm: LoggedMessages,
|
lm: LoggedMessages,
|
||||||
namespace = "",
|
namespace = "",
|
||||||
threshold = lvlAll): CustomLogAppender[LoggedMessages] =
|
threshold = lvlAll): CustomLogAppender[LoggedMessages] =
|
||||||
|
initCustomLogAppender(state = lm, doLogMessage = testLogAppenderProc,
|
||||||
initCustomLogAppender(
|
namespace = namespace, threshold = threshold)
|
||||||
state = lm,
|
|
||||||
doLogMessage = testLogAppenderProc,
|
|
||||||
namespace = namespace,
|
|
||||||
threshold = threshold)
|
|
||||||
|
|
||||||
|
|
||||||
proc waitForFileContent*(
|
proc waitForFileContent*(
|
||||||
|
|||||||
+50
-12
@@ -9,12 +9,15 @@ Run from the repository root:
|
|||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `nimble test` | All unit, regression, and ThreadSanitizer tests under ORC with threads enabled |
|
| `nimble test` | All unit, regression, and ThreadSanitizer tests under ORC with threads enabled |
|
||||||
| `nimble testUnit` | Existing embedded unit tests |
|
| `nimble testUnit` | Existing embedded unit tests |
|
||||||
| `nimble testRegression` | Handle lifetime, configuration freshness, refresh failure recovery, appender-state cleanup, and writer shutdown |
|
| `nimble testRegression` | Custom-state/formatter ownership, handle lifetime, configuration freshness, refresh failure recovery, appender-state cleanup, and writer shutdown |
|
||||||
| `nimble testThreadSafety` | Separate ThreadSanitizer processes for service ownership, last-owner teardown, reconfiguration, custom state, and formatter closures, plus an independent-service control |
|
| `nimble testThreadSafety` | Separate ThreadSanitizer processes for service ownership, last-owner teardown, reconfiguration, shared state, factory state, formatter closures, collector snapshots, timestamps, and the Nim allocator, plus an independent-service control |
|
||||||
|
|
||||||
`nimble test` attempts every suite even if an earlier suite fails to compile or
|
`nimble test` attempts every suite even if an earlier suite fails to compile or
|
||||||
run, summarizes the failing suites, and exits unsuccessfully if any suite fails.
|
run, summarizes the failing suites, and exits unsuccessfully if any suite fails.
|
||||||
The subset commands use the same build and execution procedures as the full run.
|
The subset commands use the same build and execution procedures as the full run.
|
||||||
|
Old executables are removed before compilation, and a new executable must exist
|
||||||
|
before a suite runs. This prevents stale binaries from hiding compilation errors
|
||||||
|
when a nested Nimble command fails to propagate its exit status.
|
||||||
|
|
||||||
Executables are generated in ignored `tests/bin/`. The shutdown child is built
|
Executables are generated in ignored `tests/bin/`. The shutdown child is built
|
||||||
with `-d:multithreaded`; `--threads:on` alone does not enable background writers.
|
with `-d:multithreaded`; `--threads:on` alone does not enable background writers.
|
||||||
@@ -39,13 +42,43 @@ Thread arguments contain borrowed pointers so no hidden owning argument keeps
|
|||||||
the service alive. Atomic construction/destruction counters verify that every
|
the service alive. Atomic construction/destruction counters verify that every
|
||||||
tracked appender is released exactly once after the workers join.
|
tracked appender is released exactly once after the workers join.
|
||||||
`service-reconfigure` exercises concurrent updates and local cache refresh.
|
`service-reconfigure` exercises concurrent updates and local cache refresh.
|
||||||
The other scenarios retain parent owners until all workers join. Custom-state
|
`shared-state-lifetime` drops the original state handle before concurrent
|
||||||
and closure tests only read application data; their concurrent writes are the
|
mutations and verifies one final destruction with all updates present.
|
||||||
|
`formatter-lifetime` drops an already-used original formatter and checks both
|
||||||
|
independent closure counters and exact cleanup on worker threads. `factory-state`
|
||||||
|
uses per-thread cyclic reference graphs, explicitly runs the ORC collector, and
|
||||||
|
checks that each worker maintains its own counter. `collector` concurrently
|
||||||
|
records messages containing JSON and exceptions and mutates returned snapshots.
|
||||||
|
These ownership scenarios use explicit timestamps where messages are needed.
|
||||||
|
|
||||||
|
The separate `timestamp` case exercises concurrent `initLogMessage()` calls. It
|
||||||
|
currently exposes a sanitizer report in libc local-time conversion reached via
|
||||||
|
Nim's `std/times.now()`; this remains a separate thread-safety issue.
|
||||||
|
|
||||||
|
`allocator` calls the standalone reproduction in `runtime_allocator.nim`, which
|
||||||
|
imports no logging code. It exercises sequence growth and clearing under a lock
|
||||||
|
alongside thread-local closure allocations. On Nim 2.2.10 it reproduces a report
|
||||||
|
in `system/alloc.nim`'s `compensateCounters` / shared-free-list path. The same
|
||||||
|
runtime report can occur in `factory-state` and other allocation-heavy cases.
|
||||||
|
The ten ownership scenarios passed debug and release diagnostic builds with
|
||||||
|
`-d:useMalloc`;
|
||||||
|
`nimble test` deliberately retains the default allocator and exposes these
|
||||||
|
reports rather than suppressing them.
|
||||||
|
|
||||||
|
The original custom-state and formatter scenarios retain parent owners until all
|
||||||
|
workers join. They only read application data; their concurrent writes are the
|
||||||
implicit ownership operations caused by cloning and destruction. The independent
|
implicit ownership operations caused by cloning and destruction. The independent
|
||||||
case creates a separate service inside each worker and should pass. Custom-state
|
case creates a separate service inside each worker and should pass. Custom-state
|
||||||
and formatter cloning is serialized with a lock, matching configuration refresh;
|
and formatter cloning is serialized with a lock, matching configuration refresh;
|
||||||
the copied appenders are used and destroyed outside that lock.
|
the copied appenders are used and destroyed outside that lock.
|
||||||
|
|
||||||
|
`ownership.nim`, imported by `regressions.nim`, checks independent snapshots,
|
||||||
|
exception recovery, factory and clone behavior, and the built-in formatter paths.
|
||||||
|
Compile-time checks reject direct and nested refs, captured formatter arguments,
|
||||||
|
and embedded synchronization primitives. The formerly unsafe custom-state and
|
||||||
|
formatter scenarios now exercise the supported shared-handle and factory APIs;
|
||||||
|
the unsafe forms are tested as compilation failures.
|
||||||
|
|
||||||
The in-process regression tests are deterministic. The lifetime suite checks
|
The in-process regression tests are deterministic. The lifetime suite checks
|
||||||
copying, self-assignment, replacement, moving, container copies, and exporting a
|
copying, self-assignment, replacement, moving, container copies, and exporting a
|
||||||
handle from a local service. Shutdown tests launch
|
handle from a local service. Shutdown tests launch
|
||||||
@@ -55,12 +88,17 @@ real exit hook without sleeps to hide queued-message loss. Like the race tests,
|
|||||||
the shutdown failure can depend on scheduling; a passing run is not proof of
|
the shutdown failure can depend on scheduling; a passing run is not proof of
|
||||||
thread safety or reliable draining.
|
thread safety or reliable draining.
|
||||||
|
|
||||||
This suite does not yet force an active producer/channel-close race, separately
|
This suite does not yet force an active producer/channel-close race or exhaust
|
||||||
exercise the shared `testutil.LoggedMessages` graph, or exhaust ORC cycle-collector
|
ORC cycle-collector interleavings. It provides targeted regressions for ownership,
|
||||||
interleavings. It provides targeted regressions for the reproduced ownership
|
configuration, cleanup, timestamps, allocator behavior, and shutdown.
|
||||||
defects and the configuration, cleanup, and shutdown findings.
|
|
||||||
|
|
||||||
After the shared-handle implementation, the service sanitizer scenarios, lifetime
|
The ownership scenarios, lifetime suite, and appender-state cleanup regression
|
||||||
suite, and appender-state cleanup regression should pass. The custom-state and
|
exercise the new ownership contracts. `isEnabled` freshness, failed-refresh
|
||||||
formatter sanitizer cases, `isEnabled` freshness, failed-refresh recovery, and
|
recovery, local-time timestamp construction, allocator reports, and writer
|
||||||
writer shutdown remain failing regressions for subsequent work.
|
shutdown remain for subsequent work.
|
||||||
|
|
||||||
|
The standalone `ownership.nim` regression suite passed AddressSanitizer and
|
||||||
|
LeakSanitizer with `--mm:orc -d:useMalloc`. An additional worker-thread build found
|
||||||
|
no invalid accesses in the shared-state, factory-state, formatter-lifetime, and collector
|
||||||
|
scenarios. LeakSanitizer reported per-thread ORC cycle-registration buffers
|
||||||
|
allocated by the Nim runtime; this is not a clean leak-check result.
|
||||||
@@ -0,0 +1,170 @@
|
|||||||
|
import std/[atomics, locks, options, tables, unittest]
|
||||||
|
import ../src/namespaced_logging
|
||||||
|
|
||||||
|
type
|
||||||
|
UnsafeState = ref object
|
||||||
|
value: int
|
||||||
|
NestedUnsafeState = object
|
||||||
|
values: seq[UnsafeState]
|
||||||
|
VariantUnsafeState = object
|
||||||
|
case hasReference: bool
|
||||||
|
of true: reference: UnsafeState
|
||||||
|
of false: value: int
|
||||||
|
EmbeddedLock = object
|
||||||
|
lock: Lock
|
||||||
|
Slot = enum firstSlot, secondSlot
|
||||||
|
|
||||||
|
proc unsafeCallback(state: var UnsafeState, msg: LogMessage) {.gcsafe, noconv.} = discard
|
||||||
|
proc nestedCallback(state: var NestedUnsafeState, msg: LogMessage) {.gcsafe, noconv.} = discard
|
||||||
|
proc makePrefix(prefix: string): StatefulLogFormatterFunc =
|
||||||
|
result = proc(msg: LogMessage): string {.gcsafe.} = prefix & msg.msg
|
||||||
|
|
||||||
|
static:
|
||||||
|
doAssert not compiles(initSharedLogState(UnsafeState()))
|
||||||
|
doAssert not compiles(initSharedLogState(NestedUnsafeState()))
|
||||||
|
doAssert not compiles(initSharedLogState(VariantUnsafeState()))
|
||||||
|
doAssert not compiles(initSharedLogState(some(UnsafeState())))
|
||||||
|
doAssert not compiles(initCustomLogAppender(UnsafeState(), unsafeCallback))
|
||||||
|
doAssert not compiles(initCustomLogAppender(NestedUnsafeState(), nestedCallback))
|
||||||
|
doAssert not compiles(initConsoleLogAppender(formatter = makePrefix("unsafe")))
|
||||||
|
doAssert not compiles(initSharedLogState(makePrefix("unsafe")))
|
||||||
|
doAssert not compiles(initSharedLogState(default(Lock)))
|
||||||
|
doAssert not compiles(initSharedLogState(default(Atomic[int])))
|
||||||
|
doAssert not compiles(initSharedLogState(default(EmbeddedLock)))
|
||||||
|
doAssert compiles(initSharedLogState(default(array[Slot, string])))
|
||||||
|
doAssert compiles(initSharedLogState(initTable[string, seq[string]]()))
|
||||||
|
|
||||||
|
proc counterFormatter(prefix: string): StatefulLogFormatterFunc =
|
||||||
|
var count = 0
|
||||||
|
result = proc(msg: LogMessage): string {.gcsafe.} =
|
||||||
|
inc count
|
||||||
|
prefix & $count & ": " & msg.msg
|
||||||
|
|
||||||
|
type
|
||||||
|
ValueCounter = object
|
||||||
|
count: int
|
||||||
|
output: SharedLogState[seq[int]]
|
||||||
|
LocalCounter = ref object
|
||||||
|
count: int
|
||||||
|
output: SharedLogState[seq[int]]
|
||||||
|
|
||||||
|
proc initCounter(output: SharedLogState[seq[int]]): LocalCounter =
|
||||||
|
LocalCounter(output: output)
|
||||||
|
|
||||||
|
proc recordCounter(state: var LocalCounter, msg: LogMessage) {.gcsafe, noconv.} =
|
||||||
|
inc state.count
|
||||||
|
let count = state.count
|
||||||
|
state.output.withState(proc(values: var seq[int]) = values.add(count))
|
||||||
|
|
||||||
|
proc recordCounter(state: var ValueCounter, msg: LogMessage) {.gcsafe, noconv.} =
|
||||||
|
inc state.count
|
||||||
|
let count = state.count
|
||||||
|
state.output.withState(proc(values: var seq[int]) = values.add(count))
|
||||||
|
|
||||||
|
proc ignoreError(err: ref Exception, msg: string) {.gcsafe, nimcall.} = discard
|
||||||
|
|
||||||
|
suite "Custom state and formatter ownership":
|
||||||
|
test "shared value snapshots are independent and updates are explicit":
|
||||||
|
let state = initSharedLogState(@[@["original"]])
|
||||||
|
var first = state.snapshot()
|
||||||
|
first[0][0] = "changed"
|
||||||
|
check state.snapshot() == @[@["original"]]
|
||||||
|
state.withState(proc(value: var seq[seq[string]]) = value[0].add("second"))
|
||||||
|
check state.snapshot() == @[@["original", "second"]]
|
||||||
|
|
||||||
|
test "callback exceptions release the shared state lock":
|
||||||
|
let state = initSharedLogState(0)
|
||||||
|
expect ValueError:
|
||||||
|
state.withState(proc(value: var int) =
|
||||||
|
inc value
|
||||||
|
raise newException(ValueError, "injected failure"))
|
||||||
|
check state.snapshot() == 1
|
||||||
|
state.withState(proc(value: var int) = inc value)
|
||||||
|
check state.snapshot() == 2
|
||||||
|
|
||||||
|
test "a callback can release its caller's last handle":
|
||||||
|
var state = initSharedLogState(@["alive"])
|
||||||
|
state.withState(proc(value: var seq[string]) =
|
||||||
|
state = default(typeof(state))
|
||||||
|
value.add("still alive")
|
||||||
|
check value == @["alive", "still alive"])
|
||||||
|
expect ValueError: discard state.snapshot()
|
||||||
|
|
||||||
|
test "empty and moved handles reject access":
|
||||||
|
var original = initSharedLogState(42)
|
||||||
|
let moved = move(original)
|
||||||
|
check moved.snapshot() == 42
|
||||||
|
expect ValueError: discard original.snapshot()
|
||||||
|
expect ValueError: original.withState(proc(value: var int) = discard)
|
||||||
|
|
||||||
|
test "appender factories retain local state and reset it on clone":
|
||||||
|
let output = initSharedLogState(newSeq[int]())
|
||||||
|
var service = initLogService()
|
||||||
|
service.addAppender(initFactoryCustomLogAppender(output, initCounter, recordCounter))
|
||||||
|
let first = service.threadLocalRef().getLogger("first")
|
||||||
|
let second = service.threadLocalRef().getLogger("second")
|
||||||
|
first.info("one")
|
||||||
|
first.info("two")
|
||||||
|
second.info("one")
|
||||||
|
check output.snapshot() == @[1, 2, 1]
|
||||||
|
|
||||||
|
test "value appender state is mutable and independently copied":
|
||||||
|
let output = initSharedLogState(newSeq[int]())
|
||||||
|
var service = initLogService()
|
||||||
|
service.addAppender(initCustomLogAppender(ValueCounter(output: output), recordCounter))
|
||||||
|
let first = service.threadLocalRef().getLogger("first")
|
||||||
|
let second = service.threadLocalRef().getLogger("second")
|
||||||
|
first.info("one")
|
||||||
|
first.info("two")
|
||||||
|
second.info("one")
|
||||||
|
check output.snapshot() == @[1, 2, 1]
|
||||||
|
|
||||||
|
test "a failed appender factory is retried on the next message":
|
||||||
|
let calls = initSharedLogState(0)
|
||||||
|
proc factory(calls: SharedLogState[int]): UnsafeState =
|
||||||
|
calls.withState(proc(count: var int) = inc count)
|
||||||
|
if calls.snapshot() == 1:
|
||||||
|
raise newException(ValueError, "injected failure")
|
||||||
|
UnsafeState()
|
||||||
|
var service = initLogService(errorHandler = ignoreError)
|
||||||
|
service.addAppender(initFactoryCustomLogAppender(calls, factory, unsafeCallback))
|
||||||
|
let logger = service.threadLocalRef().getLogger("worker")
|
||||||
|
logger.info("first attempt")
|
||||||
|
logger.info("retry")
|
||||||
|
logger.info("reuse")
|
||||||
|
check calls.snapshot() == 2
|
||||||
|
|
||||||
|
test "formatter clones create independent captured state":
|
||||||
|
let original = initLogFormatter("count ", counterFormatter)
|
||||||
|
let message = LogMessage(msg: "hello")
|
||||||
|
check original.format(message) == "count 1: hello"
|
||||||
|
let copied = clone(original)
|
||||||
|
check original.format(message) == "count 2: hello"
|
||||||
|
check copied.format(message) == "count 1: hello"
|
||||||
|
|
||||||
|
test "every built-in appender clones its formatter configuration":
|
||||||
|
let formatter = initLogFormatter("count ", counterFormatter)
|
||||||
|
let message = LogMessage(msg: "hello")
|
||||||
|
let console = initConsoleLogAppender(formatter)
|
||||||
|
let file = initFileLogAppender("unused.log", formatter)
|
||||||
|
let standard = initStdLoggingAppender(formatter = formatter)
|
||||||
|
check console.formatter.format(message) == "count 1: hello"
|
||||||
|
check file.formatter.format(message) == "count 1: hello"
|
||||||
|
check standard.formatter.format(message) == "count 1: hello"
|
||||||
|
check console.formatter.format(message) == "count 2: hello"
|
||||||
|
check ConsoleLogAppender(clone(console)).formatter.format(message) == "count 1: hello"
|
||||||
|
check FileLogAppender(clone(file)).formatter.format(message) == "count 1: hello"
|
||||||
|
check StdLoggingAppender(clone(standard)).formatter.format(message) == "count 1: hello"
|
||||||
|
|
||||||
|
test "a failed formatter factory is retried":
|
||||||
|
let calls = initSharedLogState(0)
|
||||||
|
proc factory(calls: SharedLogState[int]): StatefulLogFormatterFunc =
|
||||||
|
calls.withState(proc(count: var int) = inc count)
|
||||||
|
if calls.snapshot() == 1:
|
||||||
|
raise newException(ValueError, "injected failure")
|
||||||
|
makePrefix("ready: ")
|
||||||
|
let formatter = initLogFormatter(calls, factory)
|
||||||
|
expect ValueError: discard formatter.format(LogMessage(msg: "hello"))
|
||||||
|
check formatter.format(LogMessage(msg: "hello")) == "ready: hello"
|
||||||
|
check formatter.format(LogMessage(msg: "again")) == "ready: again"
|
||||||
|
check calls.snapshot() == 2
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import std/[os, osproc, strutils, tempfiles, unittest]
|
import std/[os, osproc, strutils, tempfiles, unittest]
|
||||||
import ../src/namespaced_logging
|
import ../src/namespaced_logging
|
||||||
|
import ./ownership
|
||||||
|
|
||||||
type
|
type
|
||||||
CloneControl = object
|
CloneControl = object
|
||||||
@@ -16,18 +17,19 @@ method clone(app: FallibleAppender): LogAppender {.gcsafe.} =
|
|||||||
type
|
type
|
||||||
LifetimeProbeObj = object
|
LifetimeProbeObj = object
|
||||||
destroyed: ptr int
|
destroyed: ptr int
|
||||||
LifetimeProbe = ref LifetimeProbeObj
|
LifetimeProbe = SharedLogState[LifetimeProbeObj]
|
||||||
|
|
||||||
proc `=destroy`(probe: LifetimeProbeObj) =
|
proc `=destroy`(probe: LifetimeProbeObj) =
|
||||||
inc probe.destroyed[]
|
if probe.destroyed != nil:
|
||||||
|
inc probe.destroyed[]
|
||||||
|
|
||||||
proc ignoreMessage(state: LifetimeProbe, msg: LogMessage) {.gcsafe, noconv.} =
|
proc ignoreMessage(state: var LifetimeProbe, msg: LogMessage) {.gcsafe, noconv.} =
|
||||||
discard
|
discard
|
||||||
|
|
||||||
proc watchedService(destroyed: ptr int): LogService =
|
proc watchedService(destroyed: ptr int): LogService =
|
||||||
result = initLogService()
|
result = initLogService()
|
||||||
result.addAppender(initCustomLogAppender(
|
result.addAppender(initCustomLogAppender(
|
||||||
LifetimeProbe(destroyed: destroyed), ignoreMessage))
|
initSharedLogState(LifetimeProbeObj(destroyed: destroyed)), ignoreMessage))
|
||||||
|
|
||||||
proc createAndReleaseService(destroyed: ptr int) =
|
proc createAndReleaseService(destroyed: ptr int) =
|
||||||
var service = watchedService(destroyed)
|
var service = watchedService(destroyed)
|
||||||
|
|||||||
+1
-1
@@ -3,7 +3,7 @@ set -u
|
|||||||
|
|
||||||
binary=${1:?Expected the ThreadSanitizer test executable}
|
binary=${1:?Expected the ThreadSanitizer test executable}
|
||||||
result=0
|
result=0
|
||||||
for scenario in independent service service-lifetime service-reconfigure custom-state formatter; do
|
for scenario in independent service service-lifetime service-reconfigure custom-state formatter shared-state-lifetime factory-state collector formatter-lifetime timestamp allocator; do
|
||||||
echo "ThreadSanitizer scenario: $scenario"
|
echo "ThreadSanitizer scenario: $scenario"
|
||||||
if TSAN_OPTIONS=halt_on_error=1:exitcode=66 timeout 30s "$binary" "$scenario"; then
|
if TSAN_OPTIONS=halt_on_error=1:exitcode=66 timeout 30s "$binary" "$scenario"; then
|
||||||
echo "PASS: $scenario"
|
echo "PASS: $scenario"
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
## Standalone reproduction of Nim 2.2.10's allocator sanitizer report.
|
||||||
|
## No logging-library imports. All accesses to the shared sequence hold a lock.
|
||||||
|
import std/[atomics, locks]
|
||||||
|
|
||||||
|
type AllocatorData = object
|
||||||
|
lock: Lock
|
||||||
|
entries: seq[int]
|
||||||
|
total: int
|
||||||
|
|
||||||
|
var start: Atomic[bool]
|
||||||
|
|
||||||
|
proc withData(data: ptr AllocatorData,
|
||||||
|
action: proc(entries: var seq[int]) {.gcsafe.}) =
|
||||||
|
withLock data.lock:
|
||||||
|
action(data.entries)
|
||||||
|
inc data.total
|
||||||
|
|
||||||
|
proc append(data: ptr AllocatorData, value: int) =
|
||||||
|
data.withData(proc(entries: var seq[int]) =
|
||||||
|
entries.add(value)
|
||||||
|
if entries.len >= 64: entries = @[])
|
||||||
|
|
||||||
|
proc worker(data: ptr AllocatorData) {.thread.} =
|
||||||
|
while not start.load(): discard
|
||||||
|
for i in 0 ..< 10_000: data.append(i)
|
||||||
|
|
||||||
|
proc runAllocatorRegression*() =
|
||||||
|
var data: AllocatorData
|
||||||
|
initLock(data.lock)
|
||||||
|
defer: deinitLock(data.lock)
|
||||||
|
var workers: array[4, Thread[ptr AllocatorData]]
|
||||||
|
for thread in workers.mitems: createThread(thread, worker, addr data)
|
||||||
|
start.store(true)
|
||||||
|
joinThreads(workers)
|
||||||
|
doAssert data.total == 40_000
|
||||||
|
|
||||||
|
when isMainModule:
|
||||||
|
runAllocatorRegression()
|
||||||
+173
-9
@@ -1,9 +1,12 @@
|
|||||||
## Run each scenario in its own ThreadSanitizer-instrumented process.
|
## Run each scenario in its own ThreadSanitizer-instrumented process.
|
||||||
## Success means no race was reported; a crash is not required to expose UB.
|
## Success means no race was reported; a crash is not required to expose UB.
|
||||||
import std/[atomics, locks, os]
|
import std/[atomics, json, locks, options, os, times]
|
||||||
import ../src/namespaced_logging
|
import ../src/namespaced_logging
|
||||||
|
import ../src/namespaced_logging/testutil
|
||||||
|
import ./runtime_allocator
|
||||||
|
|
||||||
const iterations = 1_000
|
const iterations = 1_000
|
||||||
|
const collectorIterations = 100
|
||||||
var start: Atomic[bool]
|
var start: Atomic[bool]
|
||||||
var cloneLock: Lock
|
var cloneLock: Lock
|
||||||
var ready: Atomic[int]
|
var ready: Atomic[int]
|
||||||
@@ -73,11 +76,11 @@ proc reconfigureWorker(service: LogService) {.thread.} =
|
|||||||
doAssert local.getLogger("worker").isEnabled(lvlInfo)
|
doAssert local.getLogger("worker").isEnabled(lvlInfo)
|
||||||
|
|
||||||
type
|
type
|
||||||
CustomState = ref object
|
CustomState = object
|
||||||
value: int
|
value: int
|
||||||
StatefulAppender = CustomLogAppender[CustomState]
|
StatefulAppender = CustomLogAppender[SharedLogState[CustomState]]
|
||||||
|
|
||||||
proc ignoreMessage(state: CustomState, msg: LogMessage) {.gcsafe, noconv.} =
|
proc ignoreMessage(state: var SharedLogState[CustomState], msg: LogMessage) {.gcsafe, noconv.} =
|
||||||
discard
|
discard
|
||||||
|
|
||||||
proc stateWorker(source: ptr StatefulAppender) {.thread.} =
|
proc stateWorker(source: ptr StatefulAppender) {.thread.} =
|
||||||
@@ -86,9 +89,9 @@ proc stateWorker(source: ptr StatefulAppender) {.thread.} =
|
|||||||
var copied: StatefulAppender
|
var copied: StatefulAppender
|
||||||
withLock cloneLock:
|
withLock cloneLock:
|
||||||
copied = StatefulAppender(clone(source[]))
|
copied = StatefulAppender(clone(source[]))
|
||||||
doAssert copied.state.value == 42
|
doAssert copied.state.snapshot().value == 42
|
||||||
|
|
||||||
proc makeFormatter(prefix: string): LogMessageFormatter =
|
proc makeFormatter(prefix: string): StatefulLogFormatterFunc =
|
||||||
result = proc(msg: LogMessage): string {.gcsafe.} = prefix & msg.msg
|
result = proc(msg: LogMessage): string {.gcsafe.} = prefix & msg.msg
|
||||||
|
|
||||||
proc formatterWorker(source: ptr ConsoleLogAppender) {.thread.} =
|
proc formatterWorker(source: ptr ConsoleLogAppender) {.thread.} =
|
||||||
@@ -100,7 +103,109 @@ proc formatterWorker(source: ptr ConsoleLogAppender) {.thread.} =
|
|||||||
var copied: ConsoleLogAppender
|
var copied: ConsoleLogAppender
|
||||||
withLock cloneLock:
|
withLock cloneLock:
|
||||||
copied = ConsoleLogAppender(clone(source[]))
|
copied = ConsoleLogAppender(clone(source[]))
|
||||||
doAssert copied.formatter(msg) == "prefix: message"
|
doAssert copied.formatter.format(msg) == "prefix: message"
|
||||||
|
|
||||||
|
type
|
||||||
|
SharedPayload = object
|
||||||
|
entries: seq[string]
|
||||||
|
destructions: ptr Atomic[int]
|
||||||
|
finalCount: ptr Atomic[int]
|
||||||
|
|
||||||
|
proc `=destroy`(value: SharedPayload) =
|
||||||
|
if value.destructions != nil:
|
||||||
|
value.finalCount[].store(value.entries.len)
|
||||||
|
value.destructions[].atomicInc()
|
||||||
|
`=destroy`(value.entries)
|
||||||
|
|
||||||
|
proc sharedStateWorker(source: ptr SharedLogState[SharedPayload]) {.thread.} =
|
||||||
|
let state = source[]
|
||||||
|
ready.atomicInc()
|
||||||
|
awaitStart()
|
||||||
|
for i in 0 ..< iterations:
|
||||||
|
var copied = state
|
||||||
|
let moved = move(copied)
|
||||||
|
moved.withState(proc(value: var SharedPayload) =
|
||||||
|
value.entries.add("entry")
|
||||||
|
doAssert value.entries[^1] == "entry")
|
||||||
|
|
||||||
|
type
|
||||||
|
FactoryState = ref object
|
||||||
|
count: int
|
||||||
|
self: FactoryState
|
||||||
|
output: SharedLogState[seq[int]]
|
||||||
|
|
||||||
|
proc makeState(output: SharedLogState[seq[int]]): FactoryState =
|
||||||
|
result = FactoryState(output: output)
|
||||||
|
result.self = result
|
||||||
|
|
||||||
|
proc logState(state: var FactoryState, msg: LogMessage) {.gcsafe, noconv.} =
|
||||||
|
inc state.count
|
||||||
|
doAssert state.self == state
|
||||||
|
let count = state.count
|
||||||
|
state.output.withState(proc(values: var seq[int]) = values.add(count))
|
||||||
|
|
||||||
|
proc factoryWorker(source: ptr LogService) {.thread.} =
|
||||||
|
block:
|
||||||
|
let local = source[].threadLocalRef()
|
||||||
|
ready.atomicInc()
|
||||||
|
awaitStart()
|
||||||
|
let logger = local.getLogger("worker")
|
||||||
|
let stamp = fromUnix(0).utc
|
||||||
|
for i in 0 ..< iterations:
|
||||||
|
logger.log(LogMessage(scope: "worker", level: lvlInfo, ts: stamp, msg: "message"))
|
||||||
|
GC_fullCollect()
|
||||||
|
|
||||||
|
proc timestampWorker() {.thread.} =
|
||||||
|
awaitStart()
|
||||||
|
for i in 0 ..< iterations:
|
||||||
|
let msg = initLogMessage("worker", lvlInfo, "message")
|
||||||
|
doAssert msg.msg == "message"
|
||||||
|
|
||||||
|
proc collectorWorker(collector: LoggedMessages) {.thread.} =
|
||||||
|
# Construct every managed field in this worker, including the timezone.
|
||||||
|
let stamp = fromUnix(0).utc
|
||||||
|
awaitStart()
|
||||||
|
for i in 0 ..< collectorIterations:
|
||||||
|
collector.add(LogMessage(scope: "worker", level: lvlInfo, ts: stamp,
|
||||||
|
msg: "message", err: some[ref Exception](newException(ValueError, "error")),
|
||||||
|
additionalData: %*{"number": i}))
|
||||||
|
var snapshot = collector.get()
|
||||||
|
doAssert snapshot.len > 0
|
||||||
|
doAssert snapshot[^1].err.get.msg == "error"
|
||||||
|
snapshot[0].msg = "private copy"
|
||||||
|
doAssert collector.get()[0].msg == "message"
|
||||||
|
|
||||||
|
type
|
||||||
|
FormatterSeed = object
|
||||||
|
prefix: string
|
||||||
|
destroyed: ptr Atomic[int]
|
||||||
|
FormatterProbeObj = object
|
||||||
|
count: int
|
||||||
|
destroyed: ptr Atomic[int]
|
||||||
|
FormatterProbe = ref FormatterProbeObj
|
||||||
|
|
||||||
|
proc `=destroy`(probe: FormatterProbeObj) =
|
||||||
|
if probe.destroyed != nil: probe.destroyed[].atomicInc()
|
||||||
|
|
||||||
|
proc makeCountedFormatter(seed: FormatterSeed): StatefulLogFormatterFunc =
|
||||||
|
let probe = FormatterProbe(destroyed: seed.destroyed)
|
||||||
|
result = proc(msg: LogMessage): string {.gcsafe.} =
|
||||||
|
inc probe.count
|
||||||
|
seed.prefix & $probe.count & msg.msg
|
||||||
|
|
||||||
|
proc formatterLifetimeWorker(source: ptr LogFormatter) {.thread.} =
|
||||||
|
block:
|
||||||
|
var formatter: LogFormatter
|
||||||
|
withLock cloneLock:
|
||||||
|
formatter = clone(source[])
|
||||||
|
ready.atomicInc()
|
||||||
|
awaitStart()
|
||||||
|
doAssert formatter.format(LogMessage(msg: "!")) == "count 1!"
|
||||||
|
for i in 0 ..< iterations:
|
||||||
|
let copied = clone(formatter)
|
||||||
|
doAssert copied.format(LogMessage(msg: "!")) == "count 1!"
|
||||||
|
doAssert copied.format(LogMessage(msg: "!")) == "count 2!"
|
||||||
|
GC_fullCollect()
|
||||||
|
|
||||||
proc main() =
|
proc main() =
|
||||||
doAssert paramCount() == 1
|
doAssert paramCount() == 1
|
||||||
@@ -125,7 +230,7 @@ proc main() =
|
|||||||
start.store(true)
|
start.store(true)
|
||||||
joinThreads(workers)
|
joinThreads(workers)
|
||||||
of "custom-state":
|
of "custom-state":
|
||||||
var source = initCustomLogAppender(CustomState(value: 42), ignoreMessage)
|
var source = initCustomLogAppender(initSharedLogState(CustomState(value: 42)), ignoreMessage)
|
||||||
var workers: array[4, Thread[ptr StatefulAppender]]
|
var workers: array[4, Thread[ptr StatefulAppender]]
|
||||||
for i in 0 ..< workers.len:
|
for i in 0 ..< workers.len:
|
||||||
createThread(workers[i], stateWorker, addr source)
|
createThread(workers[i], stateWorker, addr source)
|
||||||
@@ -159,12 +264,71 @@ proc main() =
|
|||||||
joinThreads(workers)
|
joinThreads(workers)
|
||||||
of "formatter":
|
of "formatter":
|
||||||
var source = initConsoleLogAppender()
|
var source = initConsoleLogAppender()
|
||||||
source.formatter = makeFormatter("prefix: ")
|
source.formatter = initLogFormatter("prefix: ", makeFormatter)
|
||||||
var workers: array[4, Thread[ptr ConsoleLogAppender]]
|
var workers: array[4, Thread[ptr ConsoleLogAppender]]
|
||||||
for i in 0 ..< workers.len:
|
for i in 0 ..< workers.len:
|
||||||
createThread(workers[i], formatterWorker, addr source)
|
createThread(workers[i], formatterWorker, addr source)
|
||||||
start.store(true)
|
start.store(true)
|
||||||
joinThreads(workers)
|
joinThreads(workers)
|
||||||
|
of "shared-state-lifetime":
|
||||||
|
var destructions, finalCount: Atomic[int]
|
||||||
|
var state = initSharedLogState(SharedPayload(
|
||||||
|
destructions: addr destructions, finalCount: addr finalCount))
|
||||||
|
var workers: array[4, Thread[ptr SharedLogState[SharedPayload]]]
|
||||||
|
for i in 0 ..< workers.len:
|
||||||
|
createThread(workers[i], sharedStateWorker, addr state)
|
||||||
|
while ready.load() != workers.len: discard
|
||||||
|
state = default(typeof(state))
|
||||||
|
start.store(true)
|
||||||
|
joinThreads(workers)
|
||||||
|
doAssert destructions.load() == 1
|
||||||
|
doAssert finalCount.load() == workers.len * iterations
|
||||||
|
of "factory-state":
|
||||||
|
let output = initSharedLogState(newSeq[int]())
|
||||||
|
var service = initLogService()
|
||||||
|
service.addAppender(initFactoryCustomLogAppender(output, makeState, logState))
|
||||||
|
var workers: array[4, Thread[ptr LogService]]
|
||||||
|
for i in 0 ..< workers.len:
|
||||||
|
createThread(workers[i], factoryWorker, addr service)
|
||||||
|
while ready.load() != workers.len: discard
|
||||||
|
service = LogService()
|
||||||
|
start.store(true)
|
||||||
|
joinThreads(workers)
|
||||||
|
let values = output.snapshot()
|
||||||
|
doAssert values.len == workers.len * iterations
|
||||||
|
var counts: array[iterations + 1, int]
|
||||||
|
for value in values: inc counts[value]
|
||||||
|
for i in 1 .. iterations: doAssert counts[i] == workers.len
|
||||||
|
of "collector":
|
||||||
|
let collector = initLoggedMessages()
|
||||||
|
var workers: array[4, Thread[LoggedMessages]]
|
||||||
|
for i in 0 ..< workers.len:
|
||||||
|
createThread(workers[i], collectorWorker, collector)
|
||||||
|
start.store(true)
|
||||||
|
joinThreads(workers)
|
||||||
|
doAssert collector.get().len == workers.len * collectorIterations
|
||||||
|
of "formatter-lifetime":
|
||||||
|
var destroyed: Atomic[int]
|
||||||
|
var formatter = initLogFormatter(
|
||||||
|
FormatterSeed(prefix: "count ", destroyed: addr destroyed), makeCountedFormatter)
|
||||||
|
doAssert formatter.format(LogMessage(msg: "!")) == "count 1!"
|
||||||
|
var workers: array[4, Thread[ptr LogFormatter]]
|
||||||
|
for i in 0 ..< workers.len:
|
||||||
|
createThread(workers[i], formatterLifetimeWorker, addr formatter)
|
||||||
|
while ready.load() != workers.len: discard
|
||||||
|
formatter = nil
|
||||||
|
start.store(true)
|
||||||
|
joinThreads(workers)
|
||||||
|
GC_fullCollect()
|
||||||
|
doAssert destroyed.load() == 1 + workers.len * (iterations + 1)
|
||||||
|
of "timestamp":
|
||||||
|
var workers: array[4, Thread[void]]
|
||||||
|
for i in 0 ..< workers.len:
|
||||||
|
createThread(workers[i], timestampWorker)
|
||||||
|
start.store(true)
|
||||||
|
joinThreads(workers)
|
||||||
|
of "allocator":
|
||||||
|
runAllocatorRegression()
|
||||||
else:
|
else:
|
||||||
raise newException(ValueError, "unknown thread safety scenario")
|
raise newException(ValueError, "unknown thread safety scenario")
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user