@@ -433,6 +433,12 @@ kj::Promise<void> ActorSqlite::commitImpl(ActorSqlite::PrecommitAlarmState preco
433433 // pending commit so that the next commitImpl() invocation starts its own set of precommit alarm
434434 // updates and db commit.
435435 auto alarmStateForCommit = metadata.getAlarm ();
436+
437+ // Capture the alarm version before going async to detect concurrent alarm changes. If the
438+ // alarmVersion changes while we are in-flight, we should skip attempting any move-later alarm
439+ // update.
440+ auto alarmVersionBeforeAsync = alarmVersion;
441+
436442 auto commitCallbackPromise = commitCallback ();
437443 pendingCommit = kj::none;
438444
@@ -449,27 +455,36 @@ kj::Promise<void> ActorSqlite::commitImpl(ActorSqlite::PrecommitAlarmState preco
449455 // Notify any merged commitImpl() requests that the db persistence completed.
450456 fulfiller->fulfill ();
451457
452- // If the db state is now later than the in-flight scheduled alarms, issue a request to update
453- // it to match the db state.
454- if (willFireEarlier (alarmScheduledNoLaterThan, alarmStateForCommit)) {
455- if (debugAlarmSync) {
456- KJ_LOG (WARNING, " NOSENTRY DEBUG_ALARM: Moving alarm later" , " sqlite_has" ,
457- logDate (alarmStateForCommit), " alarmScheduledNoLaterThan" ,
458- logDate (alarmScheduledNoLaterThan));
458+ // If another commit modified the alarm while we were async, skip post-commit alarm sync.
459+ //
460+ // We do this for a few reasons:
461+ // 1. The other commit will handle its own alarm sync
462+ // 2. Post-commit syncs are inherently optional (the alarm will self-correct)
463+ // 3. This coalesces redundant alarm updates for better performance
464+ // 4. This avoids race conditions where a later commit moved the alarm earlier, requiring a
465+ // pre-commit alarm update, and this update may have already been made before we get here.
466+ if (alarmVersion == alarmVersionBeforeAsync) {
467+ // No intervening alarm changes, it is safe to schedule a move-later alarm update if needed.
468+ if (willFireEarlier (alarmScheduledNoLaterThan, alarmStateForCommit)) {
469+ if (debugAlarmSync) {
470+ KJ_LOG (WARNING, " NOSENTRY DEBUG_ALARM: Moving alarm later" , " sqlite_has" ,
471+ logDate (alarmStateForCommit), " alarmScheduledNoLaterThan" ,
472+ logDate (alarmScheduledNoLaterThan));
473+ }
474+ // We need to extend our alarmLaterChain now that we're adding a new "move-later" alarm task.
475+ //
476+ // Technically, we don't need serialize our "move-later" alarms since SQLite has the later
477+ // time committed locally. We could just set the `alarmLaterChain` and pass a `kj::READY_NOW`
478+ // to requestScheduledAlarm, and so if we have a partial failure we would just recover when
479+ // the alarm runs early. That said, it doesn't hurt to serialize on the client-side.
480+ alarmLaterChain = requestScheduledAlarm (alarmStateForCommit, alarmLaterChain.addBranch ())
481+ .catch_ ([](kj::Exception&& e) {
482+ // If an exception occurs when scheduling the alarm later, it's OK -- the alarm will
483+ // eventually fire at the earlier time, and the rescheduling will be retried.
484+ // We catch here to prevent the chain from breaking on errors.
485+ LOG_WARNING_PERIODICALLY (" NOSENTRY SQLite reschedule later alarm failed" , e);
486+ }).fork ();
459487 }
460- // We need to extend our alarmLaterChain now that we're adding a new "move-later" alarm task.
461- //
462- // Technically, we don't need serialize our "move-later" alarms since SQLite has the later
463- // time committed locally. We could just set the `alarmLaterChain` and pass a `kj::READY_NOW`
464- // to requestScheduledAlarm, and so if we have a partial failure we would just recover when
465- // the alarm runs early. That said, it doesn't hurt to serialize on the client-side.
466- alarmLaterChain = requestScheduledAlarm (alarmStateForCommit, alarmLaterChain.addBranch ())
467- .catch_ ([](kj::Exception&& e) {
468- // If an exception occurs when scheduling the alarm later, it's OK -- the alarm will
469- // eventually fire at the earlier time, and the rescheduling will be retried.
470- // We catch here to prevent the chain from breaking on errors.
471- LOG_WARNING_PERIODICALLY (" NOSENTRY SQLite reschedule later alarm failed" , e);
472- }).fork ();
473488 }
474489}
475490
@@ -642,6 +657,9 @@ kj::Maybe<kj::Promise<void>> ActorSqlite::setAlarm(
642657 kj::Maybe<kj::Date> newAlarmTime, WriteOptions options) {
643658 requireNotBroken ();
644659
660+ // Increment version counter on any alarm modification
661+ ++alarmVersion;
662+
645663 // TODO(someday): When deleting alarm data in an otherwise empty database, clear the database to
646664 // free up resources?
647665
0 commit comments