Preserve cached configuration when refresh fails
Build replacement appenders and thresholds before installing them and advancing the local version. Release partial replacements and old caches outside the configuration lock. In order to make the `ensureFreshness` version check as cheap as posible and only incur overhead when we actually need to update the local cache, isolate the refresh logic in a separate scope to prevent initialization and cleanup of temporary variables when the version has not changed Add regression coverage for repeated clone failures, cache preservation, cleanup, and retry without another configuration change. Update documentation to reflect the remaining freshness issue.
This commit is contained in:
1 parent
d5e3b9c301
commit
14e71c72ca
4 files changed
+99
-32
No files matched your search
+7
-3
@@ -84,7 +84,11 @@ the unsafe forms are tested as compilation failures.
|
||||
|
||||
The in-process regression tests are deterministic. The lifetime suite checks
|
||||
copying, self-assignment, replacement, moving, container copies, and exporting a
|
||||
handle from a local service. Shutdown tests launch
|
||||
handle from a local service. Refresh tests verify that repeated clone failures
|
||||
preserve the old appenders and thresholds, release partial replacements, and
|
||||
allow a later retry without another configuration change. A successful refresh
|
||||
releases the replaced cache and installs both appenders and thresholds.
|
||||
Shutdown tests launch
|
||||
a fresh process for each scenario and check all 10,000 numbered messages after
|
||||
normal exit, using isolated temporary paths and a 15-second deadline. Scenarios
|
||||
cover stdout, stderr, files, empty formatted messages, multiple file destinations,
|
||||
@@ -109,8 +113,8 @@ The suite provides targeted regressions; it does not exhaust thread scheduling
|
||||
or ORC cycle-collector interleavings.
|
||||
|
||||
The ownership scenarios, lifetime suite, and appender-state cleanup regression
|
||||
exercise the new ownership contracts. `isEnabled` freshness, failed-refresh
|
||||
recovery, local-time timestamp construction, and allocator reports remain for
|
||||
exercise the new ownership contracts. `isEnabled` freshness,
|
||||
local-time timestamp construction, and allocator reports remain for
|
||||
subsequent work.
|
||||
|
||||
The standalone `ownership.nim` regression suite passed AddressSanitizer and
|
||||
|
||||
+51
-2
@@ -14,6 +14,18 @@ method clone(app: FallibleAppender): LogAppender {.gcsafe.} =
|
||||
raise newException(ValueError, "injected clone failure")
|
||||
FallibleAppender(control: app.control)
|
||||
|
||||
type
|
||||
RefreshProbeObj = object of LogAppender
|
||||
destroyed: ptr int
|
||||
RefreshProbe = ref RefreshProbeObj
|
||||
|
||||
proc `=destroy`(app: RefreshProbeObj) =
|
||||
if app.destroyed != nil: inc app.destroyed[]
|
||||
`=destroy`(app.namespace)
|
||||
|
||||
method clone(app: RefreshProbe): LogAppender {.gcsafe.} =
|
||||
RefreshProbe(namespace: app.namespace, destroyed: app.destroyed)
|
||||
|
||||
type
|
||||
LifetimeProbeObj = object
|
||||
destroyed: ptr int
|
||||
@@ -26,10 +38,11 @@ proc `=destroy`(probe: LifetimeProbeObj) =
|
||||
proc ignoreMessage(state: var LifetimeProbe, msg: LogMessage) {.gcsafe, noconv.} =
|
||||
discard
|
||||
|
||||
proc watchedService(destroyed: ptr int): LogService =
|
||||
proc watchedService(destroyed: ptr int, namespace = ""): LogService =
|
||||
result = initLogService()
|
||||
result.addAppender(initCustomLogAppender(
|
||||
initSharedLogState(LifetimeProbeObj(destroyed: destroyed)), ignoreMessage))
|
||||
initSharedLogState(LifetimeProbeObj(destroyed: destroyed)), ignoreMessage,
|
||||
namespace = namespace))
|
||||
|
||||
proc createAndReleaseService(destroyed: ptr int) =
|
||||
var service = watchedService(destroyed)
|
||||
@@ -128,6 +141,42 @@ suite "ORC ownership and configuration regressions":
|
||||
local.ensureFreshness()
|
||||
check local.appenders().len == 1
|
||||
|
||||
test "failed refresh preserves the old cache and releases partial replacements":
|
||||
var oldStateDestroyed, discardedClones: int
|
||||
var control: CloneControl
|
||||
var service = watchedService(addr oldStateDestroyed, "old")
|
||||
service.setThreshold("worker", lvlError)
|
||||
let local = service.threadLocalRef()
|
||||
let logger = local.getLogger("worker")
|
||||
|
||||
# Only the local cache retains the old appender's state after this change.
|
||||
service.clearAppenders()
|
||||
service.addAppender(RefreshProbe(namespace: "new", destroyed: addr discardedClones))
|
||||
service.addAppender(FallibleAppender(control: addr control))
|
||||
service.setThreshold("worker", lvlInfo)
|
||||
let initiallyDestroyed = discardedClones
|
||||
|
||||
for attempt in 1 .. 2:
|
||||
control.failNext = true
|
||||
expect ValueError:
|
||||
local.ensureFreshness()
|
||||
check oldStateDestroyed == 0
|
||||
check discardedClones == initiallyDestroyed + attempt
|
||||
block:
|
||||
let cached = local.appenders()
|
||||
require cached.len == 1
|
||||
check cached[0].namespace == "old"
|
||||
check not logger.isEnabled(lvlInfo)
|
||||
|
||||
# No further configuration update is needed to retry and install both
|
||||
# the new appenders and the new namespace thresholds.
|
||||
local.ensureFreshness()
|
||||
check oldStateDestroyed == 1
|
||||
let cached = local.appenders()
|
||||
require cached.len == 2
|
||||
check cached[0].namespace == "new"
|
||||
check logger.isEnabled(lvlInfo)
|
||||
|
||||
test "releasing the service releases state owned by its appenders":
|
||||
var destroyed = 0
|
||||
createAndReleaseService(addr destroyed)
|
||||
|
||||
Reference in new issue
Block a user