Skip to content

refactor(flags): use time-based periodic flush instead of cancel+reschedule#3189

Merged
gh-worker-dd-mergequeue-cf854d[bot] merged 10 commits into
developfrom
typo/no-cancel-flush-schedule
Feb 23, 2026
Merged

refactor(flags): use time-based periodic flush instead of cancel+reschedule#3189
gh-worker-dd-mergequeue-cf854d[bot] merged 10 commits into
developfrom
typo/no-cancel-flush-schedule

Conversation

@typotter

@typotter typotter commented Feb 19, 2026

Copy link
Copy Markdown
Contributor

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 lastFlushTimeMs timestamp 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 flushIntervalMs and 2 * flushIntervalMs in exchange for much simpler handling logic.

Key changes:

  • Added lastFlushTimeMs volatile field to track when events were last written
  • periodicFlushTask() checks now - lastFlushTimeMs >= flushIntervalMs before calling flush()
  • Made startPeriodicFlush() private and idempotent (only starts if not already running)
  • Removed unnecessary reschedulePeriodicFlush() call from EvaluationsFeature
  • Removed unnecessary periodicFlushEnabled property

Review checklist (to be filled by reviewers)

  • Feature or bugfix MUST have appropriate tests (unit, integration, e2e)
  • Make sure you discussed the feature or bugfix with the maintaining team in an Issue
  • Make sure each commit and the PR mention the Issue number (cf the CONTRIBUTING doc)

…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
@typotter
typotter marked this pull request as ready for review February 20, 2026 16:33
@typotter
typotter requested a review from a team as a code owner February 20, 2026 16:33
@datadog-datadog-prod-us1

This comment has been minimized.

@codecov-commenter

codecov-commenter commented Feb 20, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.23810% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 71.42%. Comparing base (699dce4) to head (6441b5d).
⚠️ Report is 248 commits behind head on develop.

Files with missing lines Patch % Lines
...ndroid/flags/internal/EvaluationEventsProcessor.kt 95.24% 1 Missing ⚠️
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     
Files with missing lines Coverage Δ
...tadog/android/flags/internal/EvaluationsFeature.kt 21.28% <ø> (+0.44%) ⬆️
...ndroid/flags/internal/EvaluationEventsProcessor.kt 98.33% <95.24%> (+3.17%) ⬆️

... and 41 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

reschedulePeriodicFlush()
}
}
private var lastFlushTimeMs: Long = 0L

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: probably it is worth to start not from 0, but from the class creation time, so System.currentTimeMillis

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added to init since we're using TimeProvider, we should be using it for the initial timestamp if not 0.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this can be even moved out of init

Suggested change
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)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +100 to +101
flushIntervalMs,
flushIntervalMs,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@typotter typotter left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. ptal

@typotter
typotter requested a review from 0xnm February 23, 2026 17:40
reschedulePeriodicFlush()
}
}
private var lastFlushTimeMs: Long = 0L

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this can be even moved out of init

Suggested change
private var lastFlushTimeMs: Long = 0L
private var lastFlushTimeMs: Long = timeProvider.getDeviceTimestampMillis()

Comment on lines +35 to +36
private var periodicFlushScheduled: Boolean = false

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
private var periodicFlushScheduled: Boolean = false

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
periodicFlushEnabled: Boolean = true
@Volatile
private var periodicFlushEnabled: Boolean = false

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think flushMutex is not needed here, because what this function does is only scheduling, it doesn't do the actual flush.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

right. now that we don't need to protect the scheduledFuture with the lock we can drop it here.

@typotter
typotter requested a review from 0xnm February 23, 2026 18:21
@gh-worker-dd-mergequeue-cf854d
gh-worker-dd-mergequeue-cf854d Bot merged commit 63dfac8 into develop Feb 23, 2026
27 checks passed
@gh-worker-dd-mergequeue-cf854d
gh-worker-dd-mergequeue-cf854d Bot deleted the typo/no-cancel-flush-schedule branch February 23, 2026 21:02
@ncreated
ncreated restored the typo/no-cancel-flush-schedule branch April 9, 2026 09:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants