Skip to content

Conversation

@MellowYarker
Copy link
Contributor

Historically, if a SQLite DOs local alarm time in SQLite didn't match the AlarmManager's alarm time, we would reschedule the alarm and try to run the alarm again later.

However, if the time we are attempting to sync to is already before the current real system time, there is no point in rescheduling to run later, since we might as well run now.

This commit runs the alarm immediately if the current system time is already past the time we are trying to sync the alarm manager to, since it's better to run an alarm a little late rather than very late.

Historically, if a SQLite DOs local alarm time in SQLite didn't match
the AlarmManager's alarm time, we would reschedule the alarm and try to
run the alarm again later.

However, if the time we are attempting to sync to is already before the
current real system time, there is no point in rescheduling to run
later, since we might as well run now.

This commit runs the alarm immediately if the current system time is
already past the time we are trying to sync the alarm manager to, since
it's better to run an alarm a little late rather than very late.
@MellowYarker MellowYarker marked this pull request as ready for review January 5, 2026 14:55
@MellowYarker MellowYarker requested review from a team as code owners January 5, 2026 14:55
Copy link
Contributor

@jqmmes jqmmes left a comment

Choose a reason for hiding this comment

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

LGTM

static constexpr kj::Date fourMs = 4 * kj::MILLISECONDS + kj::UNIX_EPOCH;
static constexpr kj::Date fiveMs = 5 * kj::MILLISECONDS + kj::UNIX_EPOCH;
// Used as the "current time" parameter for armAlarmHandler in tests.
// Set to epoch (before all test alarm times) so existing tests aren't affected by
Copy link
Member

Choose a reason for hiding this comment

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

Making sure existing tests aren't affected is great, but is this new behavior worth covering with a new test case?

// If the local alarm time is already in the past, just run the handler now. This avoids
// blocking alarm execution on storage sync when storage is overloaded. The alarm will
// either delete itself on success or reschedule on failure.
if (localAlarmState != kj::none && willFireEarlier(localAlarmState, currentTime)) {
Copy link
Member

Choose a reason for hiding this comment

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

Given that this code block appears to be identical both here and below, can it just be moved up above the if (willFireEarlier(scheduledTime, localAlarmState)) { ... } else { ...} block rather than being repeated in both the if and the else?

Copy link

@Hardanish-Singh Hardanish-Singh left a comment

Choose a reason for hiding this comment

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

LGTM!

}
}

auto currentTime = kj::systemPreciseCalendarClock().now();
Copy link
Contributor

@jclee jclee Jan 8, 2026

Choose a reason for hiding this comment

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

It would probably be more consistent with other alarm operations to use context.now() here -- basically using the context's Date.now() value instead of the CPU clock. That's e.g. the value we use to force alarm times to the present, in DurableObjectStorageOperations::setAlarm(), as well as to ensure alarms do not run until after Date.now() becomes current, in handleAlarm().

KJ_ASSERT_NONNULL(localAlarmState), currentTime, actorId);
haveDeferredDelete = true;
inAlarmHandler = true;
static const DeferredAlarmDeleter disposer;
Copy link
Contributor

Choose a reason for hiding this comment

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

Might be a little neater to just have a single DeferredAlarmDeleter declaration at the top of the function.

LOG_WARNING_PERIODICALLY(
"NOSENTRY SQLite alarm overdue, running despite stale AlarmManager time",
scheduledTime, localTime, currentTime, actorId);
haveDeferredDelete = true;
Copy link
Contributor

Choose a reason for hiding this comment

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

I notice that the other existing code that sets haveDeferredDelete = true is also setting deferredAlarmSpan = kj::mv(parentSpan);... I'm not familiar with exactly how the spans are being used, but it seems possible the consuming code may expect the span to similarly be set for new cases of haveDeferredDelete = true?

// If the local alarm time is already in the past, just run the handler now. This avoids
// blocking alarm execution on storage sync when storage is overloaded. The alarm will
// either delete itself on success or reschedule on failure.
if (localAlarmState != kj::none && willFireEarlier(localAlarmState, currentTime)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Strictly speaking, it isn't necessary to check localAlarmState != kj::none, since willFireEarlier(localAlarmState, currentTime) will never be true for localAlarmState == kj::none.

Comment on lines +889 to +891
LOG_WARNING_PERIODICALLY(
"NOSENTRY SQLite alarm overdue, running despite AlarmManager mismatch", scheduledTime,
KJ_ASSERT_NONNULL(localAlarmState), currentTime, actorId);
Copy link
Contributor

@jclee jclee Jan 8, 2026

Choose a reason for hiding this comment

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

If execution reaches this point, then we know (scheduledTime < localAlarmState < currentTime). But we have code in the alarm caller that explicitly waits until Date.now() <= scheduledTime... which I think means that this clause should be theoretically unreachable, if we populate currentTime from Date.now() rather than from the CPU clock.

Ah, misread... the calling code is actually waiting until scheduledTime <= Date.now(), so should be reachable.

Comment on lines +932 to +933
// If the local alarm time is already in the past, just run the handler now. This avoids
// blocking alarm execution on storage sync when storage is overloaded.
Copy link
Contributor

Choose a reason for hiding this comment

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

The comments mention avoiding blocks on "storage sync", which I assume means being blocked on the persistence of sqlite data? In which case, it might be worth noting that this code is inside of a check for "if (localAlarmState == lastConfirmedAlarmDbState)", so should only run in the cases where persistence of alarm changes has completed -- or the unlikely case where the alarm time was changed, then quickly changed back. So might not actually help much in cases where "storage is overloaded"?

kj::Date scheduledTime, SpanParent parentSpan, bool noCache, kj::StringPtr actorId) {
kj::Date scheduledTime,
SpanParent parentSpan,
kj::Date /*currentTime -- unused*/,
Copy link
Contributor

Choose a reason for hiding this comment

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

Curious... was this required by some compiler warning/error, or is it just for notation? I'm not seeing other code using this convention.

In either case, tagging the parameter with KJ_UNUSED or [[maybe_unused]] might also work? ...although I don't see other code doing that, either. It looks like the kj style guide just recommends compiling with -Wno-unused-parameter.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants