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:
jdb committed 2026-09-29 08:10:12 -05:00
1 parent d5e3b9c301
commit 14e71c72ca
4 files changed
+99 -32

No files matched your search

+51 -2
View File
@@ -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)