Conversation
| context.remove | ||
| } else { | ||
| register(touchedAt) | ||
| register(current) |
There was a problem hiding this comment.
(default 10 min?) instead of every ~1s trigger, growing the uncommitted-offset window
and idle-key residency with no doc/changelog note.
There was a problem hiding this comment.
I don't think there should be a doc note here as the current doc already states for fireEvery - How often the check should be performed.. So, the doc was misleading before because with fireEvery < maxIdle (e.g. 3m and 10m) the key would be evicted immediately after maxIdle (see One behaviour change worth reviewing in the description).
As for the changelog - I'd put it into the release notes when making a GH release
|
Re-triggering required check after org policy rollout fix |
|
Re-running policy check with updated detector |
onTimer re-registered the next check as `touchedAt + fireEvery`, where touchedAt is when a record was last processed for the key. For an idle key that instant never moves, so once the clock passed it every re-registration landed in the past and the timer was due again on the very next trigger - i.e. every triggerTimersInterval (1s by default) instead of every fireEvery. Anchor the next check to `current` instead, as persistPeriodically and persistPeriodicallyAndUnloadOrphaned already do.
9610ded to
5b3c202
Compare
stasimus
left a comment
There was a problem hiding this comment.
Please fix comments according to AI policy
|
Warning Review limit reachedNext included review available in 15 minutes. View limit detailsLimit details: You’ve used the included review currently available. You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Review configuration: ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Problem
TimerFlowOf.unloadOrphanedre-firesonTimeron every timer trigger once a key has been idle longer thanfireEvery, instead of once perfireEvery.PartitionFlow.triggerTimersruns attriggerTimersInterval(1s by default) and walks every cached key, so an idle-but-not-yet-unloadable key gets checked once a second.Waste, not a correctness bug:
flushonly runs on thecanUnloadbranch and the re-registered instant is the same value each time, so no extra writes and no timer-state growth. But the cost scales with the number of in-memory keys.Root cause
onTimerre-registered the next check astouchedAt + fireEvery, wheretouchedAtis when a record was last processed:touchedAtdoesn't move while the key is idle, so it's a fixed instant in the past. Once the clock passestouchedAt + fireEvery, every re-registration lands in the past,Timers.expiredrops it immediately (it keeps only instants strictly after now), and the timer is due again on the next trigger.The spin window is
[lastTouched + fireEvery, lastTouched + maxIdle):fireEvery = maxIdle = 10.minutes) it is empty — the key becomes unloadable exactly when the check would first re-fire. Hence unnoticed.maxIdle > fireEveryit is real:fireEvery = 5.minutes, maxIdle = 1.hourspins for 55 minutes per idle key.maxOffsetDifferencebranch.Fix
Anchor the next check to
current, aspersistPeriodicallyandpersistPeriodicallyAndUnloadOrphanedalready do:Safe because the decision inputs (
expiredAt,offsetDifference) are recomputed from a freshly-readprocessedAton every fire, so when the check runs doesn't change what it decides.register's parameter is renamedtouchedAt->checkedAtso the old name stops reading as intentional.Unload latency is now bounded by
fireEvery, nottriggerTimersInterval. An idle key used to be unloaded within ~1s of crossingmaxIdle— an accident of the spin; now it waits for the next scheduled check, up tofireEverylater. That matchesfireEvery's scaladoc ("How often the check should be performed") and both sibling flows, but a largefireEveryagainst a tightmaxIdlewill hold keys in memory longer. The fourth test pins it.Tests
Five tests. A shared helper ticks the clock a minute at a time and counts actual
onTimerinvocations, by wrapping the flow beforetrigger; flush counts wouldn't show the spin, sinceunloadOrphanedonly flushes on thecanUnloadbranch. Four also runpersistPeriodicallyAndUnloadOrphanedas a live reference, so the expected numbers come from the sibling flow rather than by hand.fireEveryhas elapsedfireEverywhile idlemaxIdlefireEveryis zeroThe two that pass either way are deliberate guards. The first check is registered at
acquire, a path this doesn't touch, so that one pins that the flow doesn't become eager on startup.fireEvery = 0is a documented configuration ("perfectly fine to set these parameters to zero") that 11 pre-existing tests rely on, and both anchors degenerate to the same thing there, so the guard keeps it meaning "check on every poll". The cadence test pins the semantic choice: a record processed mid-interval must not shift the cadence — the heartbeat follows the previous check, not the last record.core94/94,journal6/6,metrics5/5,persistence-cassandra19/19,persistence-kafka19/19.scalafmtCheckclean.*-it-testsnot run locally (need Cassandra/Kafka).