dani-garcia/vaultwarden · error

Unable to update emergency access notification date

Error message

Unable to update emergency access notification date

What it means

emergency_notification_reminder_job: one day before a recovery activates, it records the notification via update_last_notification_date_and_save(&now, &conn).expect("Unable to update emergency access notification date"). A DB failure panics the reminder job task. The code deliberately updates a narrow column to avoid races with the timeout job, so the realistic cause is DB unavailability rather than row conflicts.

Source

Thrown at src/api/core/emergency_access.rs:807

        let now = Utc::now().naive_utc();
        for mut emer in emergency_access_list {
            // The find_all_recoveries_initiated already checks if the recovery_initiated_at is not null (None)
            // Calculate the day before the recovery will become active
            let final_recovery_reminder_at =
                emer.recovery_initiated_at.unwrap() + TimeDelta::try_days(i64::from(emer.wait_time_days - 1)).unwrap();
            // Calculate if a day has passed since the previous notification, else no notification has been sent before
            let next_recovery_reminder_at = if let Some(last_notification_at) = emer.last_notification_at {
                last_notification_at + TimeDelta::try_days(1).unwrap()
            } else {
                now
            };
            if final_recovery_reminder_at.le(&now) && next_recovery_reminder_at.le(&now) {
                // Only update the last notification date
                // Updating the whole record could cause issues when the emergency_request_timeout_job is also active
                emer.update_last_notification_date_and_save(&now, &conn)
                    .await
                    .expect("Unable to update emergency access notification date");

                if CONFIG.mail_enabled() {
                    // get grantor user to send Accepted email
                    let grantor_user =
                        User::find_by_uuid(&emer.grantor_uuid, &conn).await.expect("Grantor user not found");

                    // get grantee user to send Accepted email
                    let grantee_user =
                        User::find_by_uuid(&emer.grantee_uuid.clone().expect("Grantee user invalid"), &conn)
                            .await
                            .expect("Grantee user not found");

                    mail::send_emergency_access_recovery_reminder(
                        &grantor_user.email,
                        &grantee_user.name,
                        emer.get_type_as_str(),
                        "1", // This notification is only triggered one day before the activation
                    )

View on GitHub (pinned to 0cefa4cca7)

Solutions

  1. Restore DB health and restart the service to respawn the job
  2. Reduce write contention (WAL for SQLite, fewer concurrent jobs)
  3. Verify last_notification_at afterwards to avoid duplicate reminder mails on rerun
  4. Code fix: log-and-continue per row instead of expect

Example fix

// before
emer.update_last_notification_date_and_save(&now, &conn).await.expect("Unable to update emergency access notification date");
// after
if let Err(e) = emer.update_last_notification_date_and_save(&now, &conn).await {
    error!("Failed to record reminder for emergency access {}: {e}", emer.uuid);
    continue;
}
Defensive patterns

Strategy: try-catch

Validate before calling

-- Rows the reminder job will process; confirm they look healthy first
SELECT uuid, grantor_uuid, grantee_uuid, wait_time_days, recovery_initiated_at, last_notification_at
FROM emergency_access
WHERE recovery_initiated_at IS NOT NULL;

Try / catch

if let Err(e) = emer.update_last_notification_date_and_save(&now, &conn).await {
    error!("reminder job: failed to record notification for {}: {e}", emer.uuid);
    continue;
}

Prevention

When it happens

Trigger: The reminder job firing during DB unavailability or lock contention while updating last_notification_at for a due recovery.

Common situations: SQLite lock contention; DB failover at job time; disk full — the same conditions that break the other job writes.

Related errors


AI-assisted analysis of dani-garcia/vaultwarden@0cefa4cca7 (2026-08-16). Data as JSON: /api/errors/097d37c5f8ae9845. Report an issue: GitHub.