refactor(flags): use time-based periodic flush instead of cancel+reschedule#3189
Conversation
…hedule Replace the cancel and reschedule pattern for periodic evaluation flushes with a more efficient time-tracking approach. The scheduled task now runs at a fixed rate and checks if enough time has elapsed since the last flush before executing. Changes: - Add lastFlushTimeMs to track when events were last written - Use scheduleAtFixedRate() instead of repeated schedule() calls - periodicFlushTask() checks time elapsed before calling flush() - Remove unnecessary reschedulePeriodicFlush() call from EvaluationsFeature
This comment has been minimized.
This comment has been minimized.
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## develop #3189 +/- ##
===========================================
+ Coverage 71.26% 71.42% +0.17%
===========================================
Files 929 929
Lines 34442 34447 +5
Branches 5813 5810 -3
===========================================
+ Hits 24542 24603 +61
+ Misses 8255 8229 -26
+ Partials 1645 1615 -30
🚀 New features to boost your workflow:
|
| reschedulePeriodicFlush() | ||
| } | ||
| } | ||
| private var lastFlushTimeMs: Long = 0L |
There was a problem hiding this comment.
nit: probably it is worth to start not from 0, but from the class creation time, so System.currentTimeMillis
There was a problem hiding this comment.
added to init since we're using TimeProvider, we should be using it for the initial timestamp if not 0.
There was a problem hiding this comment.
this can be even moved out of init
| private var lastFlushTimeMs: Long = 0L | |
| private var lastFlushTimeMs: Long = timeProvider.getDeviceTimestampMillis() |
| // Wait for any in-progress flush to complete, then drain any remaining events. | ||
| @Suppress("UnsafeThirdPartyFunctionCall") // safe - ReentrantLock.lock() does not throw | ||
| val events = flushMutex.withLock { | ||
| scheduledFlushFuture?.cancel(false) |
There was a problem hiding this comment.
There is no need to cancel it, because the executor is shut down above anyway. This will remove the need to store the reference to scheduledFlushFuture at all (probably). Runnable passed in the scheduleWithFixedDelay can check if it should schedule itself once again after periodicFlushTask is executed and do it if periodicFlushEnabled is enabled.
There was a problem hiding this comment.
Great simplification. The shutdown will allow the running task to complete and will also prevent the scheduleExecutor from re-scheduling the task so we don't need to even manage the rescheduling.
| flushIntervalMs, | ||
| flushIntervalMs, |
There was a problem hiding this comment.
Should it be the same value? The first one is for how long to wait until the task get executed, the second is how long to wait until the next one should be scheduled for execution. But we always have only single task anyway if I'm not wrong.
There was a problem hiding this comment.
Good question. I don't think it makes sense to immediately start a flush since this is is called on startup, so waiting 1 interval makes sense there. The executor waits for delayms after the task executes, so it is automatically rescheduled and as such, there would indeed only ever be one task scheduled at a time.
| reschedulePeriodicFlush() | ||
| } | ||
| } | ||
| private var lastFlushTimeMs: Long = 0L |
There was a problem hiding this comment.
this can be even moved out of init
| private var lastFlushTimeMs: Long = 0L | |
| private var lastFlushTimeMs: Long = timeProvider.getDeviceTimestampMillis() |
| private var periodicFlushScheduled: Boolean = false | ||
|
|
There was a problem hiding this comment.
| private var periodicFlushScheduled: Boolean = false |
There was a problem hiding this comment.
This was extra-defensive since the class, as written, will only ever call startPeriodicFlush once from init.
| @@ -31,19 +30,18 @@ internal class EvaluationEventsProcessor( | |||
| periodicFlushEnabled: Boolean = true | |||
There was a problem hiding this comment.
| periodicFlushEnabled: Boolean = true | |
| @Volatile | |
| private var periodicFlushEnabled: Boolean = false |
There was a problem hiding this comment.
We aren't storing this value - just read it once at construction and start the period flush using a mechanism that reschedules the task after each completion.
| // Cancel any scheduled before scheduling a new one. | ||
| @Suppress("UnsafeThirdPartyFunctionCall") // safe - ReentrantLock.lock() does not throw | ||
| private fun startPeriodicFlush() { | ||
| flushMutex.withLock { |
There was a problem hiding this comment.
I think flushMutex is not needed here, because what this function does is only scheduling, it doesn't do the actual flush.
There was a problem hiding this comment.
right. now that we don't need to protect the scheduledFuture with the lock we can drop it here.
What does this PR do?
Replaces the cancel and reschedule pattern for periodic evaluation flushes with a more efficient time-tracking approach. The scheduled task now checks if enough time has elapsed since the last flush before executing.
Motivation
The previous implementation would cancel and reschedule the periodic flush task every time a flush occurred (manual or automatic). This created unnecessary overhead from repeatedly creating and cancelling scheduled tasks. The new approach uses a single scheduled task that checks a
lastFlushTimeMstimestamp to determine if a flush is needed.Additional Notes
Trade-offs:
This configuration guarantees that the age of the in-memory cache will be between
flushIntervalMsand2 * flushIntervalMsin exchange for much simpler handling logic.Key changes:
lastFlushTimeMsvolatile field to track when events were last writtenperiodicFlushTask()checksnow - lastFlushTimeMs >= flushIntervalMsbefore callingflush()startPeriodicFlush()private and idempotent (only starts if not already running)reschedulePeriodicFlush()call fromEvaluationsFeatureperiodicFlushEnabledpropertyReview checklist (to be filled by reviewers)