fix(alarms): give up after three runs without delivery (FLAW-12)
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m34s
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m34s
New column alarm_failures counts ticker runs in which no channel delivered; at three the alarm is marked as done and a warning is logged instead of retrying every minute forever. Success, a new due date or reopening the task starts over with three attempts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
b5f926bcf7
commit
d1982816c1
8 files changed
+80
-13
No files matched your search
@@ -82,7 +82,7 @@ Hinweis: `DATABASE_URL` mit `postgres…` führt dazu, dass der Standardpfad in
|
|||||||
|
|
||||||
## Hintergrundjob: Alarme
|
## Hintergrundjob: Alarme
|
||||||
|
|
||||||
`startAlarmTicker` prüft jede Minute alle offenen, noch nicht gemeldeten Aufgaben mit Fälligkeit. Ist der Alarmzeitpunkt erreicht (Fälligkeit mit Uhrzeit bzw. 9:00 bei ganztägigen Aufgaben, minus Vorlaufzeit des Benutzers, Zeitzone `TZ`), sendet er über die Kanäle des Benutzers (`email`, `discord`, `webhook`) und setzt `alarm_sent`, sobald mindestens ein Kanal erfolgreich war. Details und Einschränkungen: [modules/tasks.md](modules/tasks.md), [FLAW-12](flaws.md).
|
`startAlarmTicker` prüft jede Minute alle offenen, noch nicht gemeldeten Aufgaben mit Fälligkeit. Ist der Alarmzeitpunkt erreicht (Fälligkeit mit Uhrzeit bzw. 9:00 bei ganztägigen Aufgaben, minus Vorlaufzeit des Benutzers, Zeitzone `TZ`), sendet er über die Kanäle des Benutzers (`email`, `discord`, `webhook`) und setzt `alarm_sent`, sobald mindestens ein Kanal erfolgreich war. Nach 3 Durchläufen ohne Erfolg gibt er auf (`alarm_failures`). Details und Einschränkungen: [modules/tasks.md](modules/tasks.md), [FLAW-12](flaws.md).
|
||||||
|
|
||||||
## Hintergrundjob: Verdichtung der Notiz-Synchronisation
|
## Hintergrundjob: Verdichtung der Notiz-Synchronisation
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -20,7 +20,7 @@ Die Datei `migrations/001_init.sql` wird vom Code **nicht** gelesen und ist vera
|
|||||||
| Tabelle | Zweck | Wichtige Spalten |
|
| Tabelle | Zweck | Wichtige Spalten |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `password_reset_tokens` | Links aus „Passwort vergessen?“ | `token_hash` (SHA-256), `user_id`, `created_at`, `expires_at` (1 h), `used_at` |
|
| `password_reset_tokens` | Links aus „Passwort vergessen?“ | `token_hash` (SHA-256), `user_id`, `created_at`, `expires_at` (1 h), `used_at` |
|
||||||
| `thoughts` | Gedanken **und** Aufgaben in einer Tabelle | `status` (`inbox`/`kept`/`converted`/`discarded`), `converted_type` (`task`/`document`/`list_item`/`document_block`), `converted_ref_id`, `priority`, `is_completed`, `due_date` (`YYYY-MM-DD` ganztägig oder `YYYY-MM-DDTHH:MM`, Zeitzone des Servers), `task_list_id`, `kanban_status`, `notes`, `subtasks` (JSON), `tags` (JSON), `alarm_sent`, `sort_order`, `source` (`manual`, `voice`, `webhook` …) |
|
| `thoughts` | Gedanken **und** Aufgaben in einer Tabelle | `status` (`inbox`/`kept`/`converted`/`discarded`), `converted_type` (`task`/`document`/`list_item`/`document_block`), `converted_ref_id`, `priority`, `is_completed`, `due_date` (`YYYY-MM-DD` ganztägig oder `YYYY-MM-DDTHH:MM`, Zeitzone des Servers), `task_list_id`, `kanban_status`, `notes`, `subtasks` (JSON), `tags` (JSON), `alarm_sent`, `alarm_failures` (erfolglose Alarm-Durchläufe, ab 3 aufgegeben), `sort_order`, `source` (`manual`, `voice`, `webhook` …) |
|
||||||
| `task_assignees` | Zuweisungen von Aufgaben | `task_id` (FK `thoughts`, `ON DELETE CASCADE`), `user_id`, `created_at`; nur Personen mit Zugriff auf die Liste, bereinigt durch `PruneTaskAssignees` |
|
| `task_assignees` | Zuweisungen von Aufgaben | `task_id` (FK `thoughts`, `ON DELETE CASCADE`), `user_id`, `created_at`; nur Personen mit Zugriff auf die Liste, bereinigt durch `PruneTaskAssignees` |
|
||||||
| `task_lists` | Aufgabenlisten | `user_id` (Eigentümer), `title`, `icon`, `color`, `columns` (JSON, Kanban-Spalten) |
|
| `task_lists` | Aufgabenlisten | `user_id` (Eigentümer), `title`, `icon`, `color`, `columns` (JSON, Kanban-Spalten) |
|
||||||
| `task_list_shares` | Freigaben von Aufgabenlisten | `target_type` (`team`/`user`), `target_id`, **`permission`** (`editor`/`viewer`, Standard `editor`) |
|
| `task_list_shares` | Freigaben von Aufgabenlisten | `target_type` (`team`/`user`), `target_id`, **`permission`** (`editor`/`viewer`, Standard `editor`) |
|
||||||
|
|||||||
+2
-2
@@ -21,7 +21,7 @@ Schweregrade: **Hoch** (Datenabfluss oder Rechteumgehung möglich), **Mittel** (
|
|||||||
| FLAW-09 | Auth | Kein Rate-Limit, keine Sperre bei Fehlversuchen; ~~Mindestpasswort 4 Zeichen~~ | Mittel |
|
| FLAW-09 | Auth | Kein Rate-Limit, keine Sperre bei Fehlversuchen; ~~Mindestpasswort 4 Zeichen~~ | Mittel |
|
||||||
| FLAW-10 | Freigaben | ~~`sharing_mode` wird bei Freigaben nicht durchgesetzt; Benutzerverzeichnis leakt alle Konten~~ | **Behoben** |
|
| FLAW-10 | Freigaben | ~~`sharing_mode` wird bei Freigaben nicht durchgesetzt; Benutzerverzeichnis leakt alle Konten~~ | **Behoben** |
|
||||||
| FLAW-11 | Workspace | ~~Block-Upsert (`ON CONFLICT(id)`) prüft das Dokument nicht~~ | **Behoben** |
|
| FLAW-11 | Workspace | ~~Block-Upsert (`ON CONFLICT(id)`) prüft das Dokument nicht~~ | **Behoben** |
|
||||||
| FLAW-12 | Benachrichtigungen | ~~`notify_before_minutes` wirkungslos; Alarm feuert nach Umplanung nie wieder~~; kein Backoff bei Fehlern | **Teilweise behoben** |
|
| FLAW-12 | Benachrichtigungen | ~~`notify_before_minutes` wirkungslos; Alarm feuert nach Umplanung nie wieder; endlose Wiederholung bei Fehlern~~ | **Behoben** |
|
||||||
| FLAW-13 | Datenbank | Keine Schema-Versionierung; Migrationsfehler werden verschluckt; `migrations/001_init.sql` ist toter Code | Mittel |
|
| FLAW-13 | Datenbank | Keine Schema-Versionierung; Migrationsfehler werden verschluckt; `migrations/001_init.sql` ist toter Code | Mittel |
|
||||||
| FLAW-14 | Backend | Fehler von Schreiboperationen werden häufig ignoriert (`_, _ =`) | Mittel |
|
| FLAW-14 | Backend | Fehler von Schreiboperationen werden häufig ignoriert (`_, _ =`) | Mittel |
|
||||||
| FLAW-15 | Realtime | ~~`WriteTimeout` von 30 s beendet SSE-Streams~~ | **Behoben** |
|
| FLAW-15 | Realtime | ~~`WriteTimeout` von 30 s beendet SSE-Streams~~ | **Behoben** |
|
||||||
@@ -111,7 +111,7 @@ Schweregrade: **Hoch** (Datenabfluss oder Rechteumgehung möglich), **Mittel** (
|
|||||||
- **Maßnahme:** `DO UPDATE … WHERE blocks.document_id = excluded.document_id`; fremde IDs ablehnen.
|
- **Maßnahme:** `DO UPDATE … WHERE blocks.document_id = excluded.document_id`; fremde IDs ablehnen.
|
||||||
|
|
||||||
### FLAW-12: Alarm-Logik
|
### FLAW-12: Alarm-Logik
|
||||||
- **Teilweise behoben:** Fälligkeiten haben eine optionale Uhrzeit; der Alarm kommt zur Uhrzeit (ganztägig 9:00) minus `notify_before_minutes`, in der Zeitzone `TZ` (Standard Europe/Berlin, Zeitzonendaten eingebettet). Neue Fälligkeit oder Wiederöffnen setzt `alarm_sent` zurück. Tests: `internal/domain/due_test.go`, `internal/api/alarm_test.go`, `TestAlarmResetsOnNewDueDateOrReopen`. Offen bleibt der letzte Punkt (kein Backoff).
|
- **Status: Behoben.** Fälligkeiten haben eine optionale Uhrzeit; der Alarm kommt zur Uhrzeit (ganztägig 9:00) minus `notify_before_minutes`, in der Zeitzone `TZ` (Standard Europe/Berlin, Zeitzonendaten eingebettet). Neue Fälligkeit oder Wiederöffnen setzt `alarm_sent` und `alarm_failures` zurück. Nach 3 Durchläufen, in denen kein Kanal zustellt, wird der Alarm aufgegeben (Warnung im Log). Tests: `internal/domain/due_test.go`, `internal/api/alarm_test.go`, `TestAlarmResetsOnNewDueDateOrReopen`, `TestAlarmGivesUpAfterThreeFailedRuns`.
|
||||||
- **Fundstellen:** `startAlarmTicker`, `GetDueTasksForAlarm`, `UpdateThoughtDetails`, `UserSettingsView`.
|
- **Fundstellen:** `startAlarmTicker`, `GetDueTasksForAlarm`, `UpdateThoughtDetails`, `UserSettingsView`.
|
||||||
- **Probleme:**
|
- **Probleme:**
|
||||||
- `notify_before_minutes` (Einstellungen: „15 Minuten vorher“ usw.) wird nirgends gelesen. Fälligkeiten haben nur ein Datum (`YYYY-MM-DD`), eine Vorlaufzeit ist damit nicht abbildbar.
|
- `notify_before_minutes` (Einstellungen: „15 Minuten vorher“ usw.) wird nirgends gelesen. Fälligkeiten haben nur ein Datum (`YYYY-MM-DD`), eine Vorlaufzeit ist damit nicht abbildbar.
|
||||||
|
|||||||
@@ -57,6 +57,6 @@ Danach wird `alarm_sent = 1` gesetzt, sobald mindestens ein Kanal erfolgreich wa
|
|||||||
- `SMTP_SKIP_VERIFY` / Schalter „Zertifikatsprüfung deaktivieren“ akzeptiert selbstsignierte Zertifikate (Verbindung bleibt verschlüsselt).
|
- `SMTP_SKIP_VERIFY` / Schalter „Zertifikatsprüfung deaktivieren“ akzeptiert selbstsignierte Zertifikate (Verbindung bleibt verschlüsselt).
|
||||||
- Fehler nennen den Schritt, an dem der Server abgelehnt hat (z. B. `SMTP-Schritt RCPT TO fehlgeschlagen: 451 …`), im Log als `smtp_step`. Test: `internal/api/mail_test.go` mit einem Fake-Server (STARTTLS, 451).
|
- Fehler nennen den Schritt, an dem der Server abgelehnt hat (z. B. `SMTP-Schritt RCPT TO fehlgeschlagen: 451 …`), im Log als `smtp_step`. Test: `internal/api/mail_test.go` mit einem Fake-Server (STARTTLS, 451).
|
||||||
|
|
||||||
Eine neue Fälligkeit oder das Wiederöffnen einer Aufgabe setzt `alarm_sent` zurück, der Alarm kommt dann erneut. Offen ([FLAW-12](../flaws.md)): Schlagen alle Kanäle fehl, wird jede Minute erneut versucht. URLs werden geprüft ([FLAW-05](../flaws.md)).
|
Eine neue Fälligkeit oder das Wiederöffnen einer Aufgabe setzt `alarm_sent` zurück, der Alarm kommt dann erneut. Schlagen alle Kanäle fehl, versucht der Ticker es in den nächsten Minuten erneut und gibt nach 3 erfolglosen Durchläufen auf (`alarm_failures`, Warnung im Server-Log); eine neue Fälligkeit oder das Wiederöffnen gibt wieder 3 Versuche ([FLAW-12](../flaws.md)). URLs werden geprüft ([FLAW-05](../flaws.md)).
|
||||||
|
|
||||||
Die Oberfläche zeigt Fälligkeiten relativ („Heute, 14:30“, „Morgen“, „Fr., 30.10.“); überfällige offene Aufgaben (mit Uhrzeit minutengenau) sind rot. Gruppieren und Sortieren nach Fälligkeit berücksichtigen die Uhrzeit, ganztägige Aufgaben stehen vor denen mit Uhrzeit am selben Tag.
|
Die Oberfläche zeigt Fälligkeiten relativ („Heute, 14:30“, „Morgen“, „Fr., 30.10.“); überfällige offene Aufgaben (mit Uhrzeit minutengenau) sind rot. Gruppieren und Sortieren nach Fälligkeit berücksichtigen die Uhrzeit, ganztägige Aufgaben stehen vor denen mit Uhrzeit am selben Tag.
|
||||||
@@ -151,6 +151,8 @@ func (s *Server) startAlarmTicker() {
|
|||||||
|
|
||||||
if sentAny {
|
if sentAny {
|
||||||
_ = s.store.MarkTaskAlarmSent(a.TaskID)
|
_ = s.store.MarkTaskAlarmSent(a.TaskID)
|
||||||
|
} else if gaveUp, _ := s.store.RecordTaskAlarmFailure(a.TaskID); gaveUp {
|
||||||
|
slog.Warn("Aufgaben-Alarm nach 3 Fehlversuchen aufgegeben", "task_id", a.TaskID, "title", a.Title)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -138,3 +138,50 @@ func TestAlarmResetsOnNewDueDateOrReopen(t *testing.T) {
|
|||||||
t.Errorf("reopening must re-arm the alarm")
|
t.Errorf("reopening must re-arm the alarm")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAlarmGivesUpAfterThreeFailedRuns(t *testing.T) {
|
||||||
|
store, err := NewStore(filepath.Join(t.TempDir(), "alarmfail.db"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewStore: %v", err)
|
||||||
|
}
|
||||||
|
defer store.Close()
|
||||||
|
u, _ := store.CreateUser("anna@example.com", "hash", "Anna", "member")
|
||||||
|
task, _ := store.CreateThoughtWithDetails(u.ID, "Webhook kaputt", "normal", "manual", "2026-10-30", "", "todo", "")
|
||||||
|
_, _ = store.UpdateThoughtTriage(u.ID, task.ID, "keep", "task", "", "", "")
|
||||||
|
pending := func() bool {
|
||||||
|
alarms, _ := store.GetDueTasksForAlarm()
|
||||||
|
for _, a := range alarms {
|
||||||
|
if a.TaskID == task.ID {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for run := 1; run <= MaxAlarmFailures; run++ {
|
||||||
|
gaveUp, err := store.RecordTaskAlarmFailure(task.ID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("run %d: %v", run, err)
|
||||||
|
}
|
||||||
|
if gaveUp != (run == MaxAlarmFailures) || pending() == (run == MaxAlarmFailures) {
|
||||||
|
t.Fatalf("run %d: gaveUp=%v pending=%v", run, gaveUp, pending())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A new due date gets three fresh attempts
|
||||||
|
later := "2026-11-02"
|
||||||
|
_, _ = store.UpdateThoughtDetails(u.ID, task.ID, nil, nil, &later, nil, nil, nil, nil, nil, nil)
|
||||||
|
if !pending() {
|
||||||
|
t.Fatalf("new due date must re-arm the alarm")
|
||||||
|
}
|
||||||
|
if gaveUp, _ := store.RecordTaskAlarmFailure(task.ID); gaveUp {
|
||||||
|
t.Errorf("failure counter must start over after a new due date")
|
||||||
|
}
|
||||||
|
// Success clears the counter
|
||||||
|
_ = store.MarkTaskAlarmSent(task.ID)
|
||||||
|
var failures int
|
||||||
|
_ = store.db.QueryRow(`SELECT alarm_failures FROM thoughts WHERE id = ?`, task.ID).Scan(&failures)
|
||||||
|
if failures != 0 {
|
||||||
|
t.Errorf("successful alarm should reset failures, got %d", failures)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -375,6 +375,7 @@ func (s *Store) migrate() error {
|
|||||||
|
|
||||||
// Idempotent column migrations for existing databases
|
// Idempotent column migrations for existing databases
|
||||||
columnMigrations := []string{
|
columnMigrations := []string{
|
||||||
|
"ALTER TABLE thoughts ADD COLUMN alarm_failures INTEGER NOT NULL DEFAULT 0;",
|
||||||
"ALTER TABLE documents ADD COLUMN archived_at TEXT NOT NULL DEFAULT '';",
|
"ALTER TABLE documents ADD COLUMN archived_at TEXT NOT NULL DEFAULT '';",
|
||||||
"ALTER TABLE documents ADD COLUMN archived_by TEXT NOT NULL DEFAULT '';",
|
"ALTER TABLE documents ADD COLUMN archived_by TEXT NOT NULL DEFAULT '';",
|
||||||
"ALTER TABLE users ADD COLUMN username TEXT NOT NULL DEFAULT '';",
|
"ALTER TABLE users ADD COLUMN username TEXT NOT NULL DEFAULT '';",
|
||||||
|
|||||||
@@ -251,8 +251,9 @@ func (s *Store) ReorderTasks(taskListID string, items []domain.ThoughtReorderIte
|
|||||||
}
|
}
|
||||||
_, err = tx.Exec(
|
_, err = tx.Exec(
|
||||||
`UPDATE thoughts SET sort_order = ?, kanban_status = ?, is_completed = ?,
|
`UPDATE thoughts SET sort_order = ?, kanban_status = ?, is_completed = ?,
|
||||||
alarm_sent = CASE WHEN ? = 0 THEN 0 ELSE alarm_sent END WHERE id = ?`,
|
alarm_sent = CASE WHEN ? = 0 THEN 0 ELSE alarm_sent END,
|
||||||
it.SortOrder, *it.KanbanStatus, isCompleted, isCompleted, it.ID,
|
alarm_failures = CASE WHEN ? = 0 THEN 0 ELSE alarm_failures END WHERE id = ?`,
|
||||||
|
it.SortOrder, *it.KanbanStatus, isCompleted, isCompleted, isCompleted, it.ID,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
_, err = tx.Exec(
|
_, err = tx.Exec(
|
||||||
@@ -388,9 +389,10 @@ func (s *Store) UpdateThoughtTriage(userID, thoughtID, action, convertTo, target
|
|||||||
_, err = s.db.Exec(
|
_, err = s.db.Exec(
|
||||||
`UPDATE thoughts
|
`UPDATE thoughts
|
||||||
SET status = ?, converted_type = ?, converted_ref_id = ?, due_date = ?, task_list_id = ?, triaged_at = ?,
|
SET status = ?, converted_type = ?, converted_ref_id = ?, due_date = ?, task_list_id = ?, triaged_at = ?,
|
||||||
alarm_sent = CASE WHEN due_date != ? THEN 0 ELSE alarm_sent END
|
alarm_sent = CASE WHEN due_date != ? THEN 0 ELSE alarm_sent END,
|
||||||
|
alarm_failures = CASE WHEN due_date != ? THEN 0 ELSE alarm_failures END
|
||||||
WHERE id = ? AND user_id = ?`,
|
WHERE id = ? AND user_id = ?`,
|
||||||
string(newStatus), convertedType, convertedRefID, dueDate, taskListID, now, dueDate, thoughtID, userID,
|
string(newStatus), convertedType, convertedRefID, dueDate, taskListID, now, dueDate, dueDate, thoughtID, userID,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -479,7 +481,7 @@ func (s *Store) UpdateThoughtDetails(userID, thoughtID string, content *string,
|
|||||||
}
|
}
|
||||||
// A new due date or a reopened task gets its alarm again
|
// A new due date or a reopened task gets its alarm again
|
||||||
if thought.DueDate != oldDue || (wasCompleted && !thought.IsCompleted) {
|
if thought.DueDate != oldDue || (wasCompleted && !thought.IsCompleted) {
|
||||||
_, _ = s.db.Exec(`UPDATE thoughts SET alarm_sent = 0 WHERE id = ?`, thoughtID)
|
_, _ = s.db.Exec(`UPDATE thoughts SET alarm_sent = 0, alarm_failures = 0 WHERE id = ?`, thoughtID)
|
||||||
thought.AlarmSent = false
|
thought.AlarmSent = false
|
||||||
}
|
}
|
||||||
if taskListID != nil {
|
if taskListID != nil {
|
||||||
@@ -572,8 +574,9 @@ func (s *Store) SyncTaskFromBlockCheck(userID, taskID string, checked bool) {
|
|||||||
doneInt = 1
|
doneInt = 1
|
||||||
kanban = "done"
|
kanban = "done"
|
||||||
}
|
}
|
||||||
res, _ := s.db.Exec(`UPDATE thoughts SET is_completed = ?, kanban_status = ?, alarm_sent = CASE WHEN ? = 0 THEN 0 ELSE alarm_sent END
|
res, _ := s.db.Exec(`UPDATE thoughts SET is_completed = ?, kanban_status = ?, alarm_sent = CASE WHEN ? = 0 THEN 0 ELSE alarm_sent END,
|
||||||
WHERE id = ? AND user_id = ? AND is_completed != ?`, doneInt, kanban, doneInt, taskID, userID, doneInt)
|
alarm_failures = CASE WHEN ? = 0 THEN 0 ELSE alarm_failures END
|
||||||
|
WHERE id = ? AND user_id = ? AND is_completed != ?`, doneInt, kanban, doneInt, doneInt, taskID, userID, doneInt)
|
||||||
if checked && res != nil {
|
if checked && res != nil {
|
||||||
if n, _ := res.RowsAffected(); n > 0 {
|
if n, _ := res.RowsAffected(); n > 0 {
|
||||||
s.spawnNextOccurrence(taskID)
|
s.spawnNextOccurrence(taskID)
|
||||||
@@ -634,6 +637,20 @@ func (s *Store) GetDueTasksForAlarm() ([]DueTaskAlarm, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) MarkTaskAlarmSent(taskID string) error {
|
func (s *Store) MarkTaskAlarmSent(taskID string) error {
|
||||||
_, err := s.db.Exec(`UPDATE thoughts SET alarm_sent = 1 WHERE id = ?`, taskID)
|
_, err := s.db.Exec(`UPDATE thoughts SET alarm_sent = 1, alarm_failures = 0 WHERE id = ?`, taskID)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MaxAlarmFailures: after this many runs in which no channel delivered, the alarm is given up instead of
|
||||||
|
// being retried every minute. A new due date or reopening the task starts over.
|
||||||
|
const MaxAlarmFailures = 3
|
||||||
|
|
||||||
|
// RecordTaskAlarmFailure counts a run without any successful channel and reports whether the alarm was
|
||||||
|
// given up now.
|
||||||
|
func (s *Store) RecordTaskAlarmFailure(taskID string) (gaveUp bool, err error) {
|
||||||
|
var failures int
|
||||||
|
err = s.db.QueryRow(`UPDATE thoughts SET alarm_failures = alarm_failures + 1,
|
||||||
|
alarm_sent = CASE WHEN alarm_failures + 1 >= ? THEN 1 ELSE alarm_sent END
|
||||||
|
WHERE id = ? RETURNING alarm_failures`, MaxAlarmFailures, taskID).Scan(&failures)
|
||||||
|
return err == nil && failures >= MaxAlarmFailures, err
|
||||||
|
}
|
||||||
Reference in new issue
Block a user