{"record":{"id":"d8671fe8a0a1e1a8","repo":"OpenNHP/opennhp","slug":"keystore-sweep-otp-w","errorCode":null,"errorMessage":"keystore: sweep otp: %w","messagePattern":"keystore: sweep otp: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"endpoints/server/keystore.go","lineNumber":562,"sourceCode":"\n// SweepStaleOTPs deletes OTP rows that are already used or expired and\n// were created more than retentionSeconds ago. Returns the number of rows\n// deleted. Unused, non-expired OTPs are never swept. Retention defaults\n// to 86400s (24 hours) when passed a negative value. Pass 0 to delete all\n// used or expired OTPs regardless of age.\nfunc (s *AgentKeyStore) SweepStaleOTPs(retentionSeconds int64) (int64, error) {\n\tif retentionSeconds < 0 {\n\t\tretentionSeconds = 86400\n\t}\n\tcutoff := time.Now().Unix() - retentionSeconds\n\tres, err := s.db.Exec(\n\t\t`DELETE FROM otp_records\n\t\t WHERE created_at < ?\n\t\t   AND (used = 1 OR expires_at <= ?)`,\n\t\tcutoff, time.Now().Unix(),\n\t)\n\tif err != nil {\n\t\treturn 0, fmt.Errorf(\"keystore: sweep otp: %w\", err)\n\t}\n\tn, err := res.RowsAffected()\n\tif err != nil {\n\t\treturn 0, fmt.Errorf(\"keystore: sweep otp rows affected: %w\", err)\n\t}\n\treturn n, nil\n}\n\n// ── Helpers ───────────────────────────────────────────────────────────────\n\nfunc randomDigits(n int) (string, error) {\n\tif n <= 0 {\n\t\treturn \"\", fmt.Errorf(\"invalid digit count: %d\", n)\n\t}\n\n\tbuf := make([]byte, n)\n\tfor i := range buf {\n\t\tdigit, err := rand.Int(rand.Reader, big.NewInt(10))","sourceCodeStart":544,"sourceCodeEnd":580,"githubUrl":"https://github.com/OpenNHP/opennhp/blob/6e04ca5ff03222a699c24205cd4bf8fee9af7ffe/endpoints/server/keystore.go#L544-L580","documentation":"SweepStaleOTPs wraps failures of the DELETE that removes used/expired OTP rows older than the retention cutoff. The deletion predicate itself is fixed (created_at < cutoff AND (used=1 OR expires_at <= now)); only Exec-level DB failures produce this error.","triggerScenarios":"Calling SweepStaleOTPs(retentionSeconds) when otp_records does not exist (missing migration), the DB is locked by another writer, the handle is closed, or the filesystem is full/read-only.","commonSituations":"Retention sweeper running before the OTP schema migration; lock contention with OTP creation/verification traffic; container data mount being read-only.","solutions":["Unwrap the error to identify the cause (no such table: otp_records / database is locked).","Run migrations creating otp_records before starting the OTP sweeper.","Enable WAL mode and busy_timeout to coexist with OTP traffic.","Retry the sweep with backoff if the error is transient lock contention.","Verify the process can write to the SQLite file and its containing directory."],"exampleFix":"null","handlingStrategy":"retry","validationCode":"var tables int\ndb.QueryRow(\"SELECT count(*) FROM sqlite_master WHERE type='table' AND name='otp_records'\").Scan(&tables)\nif tables == 0 { return errors.New(\"otp_records missing; migrate first\") }","typeGuard":"n, err := store.SweepStaleOTPs(retention)\nif err != nil && isBusyOrLocked(err) {\n    time.Sleep(time.Second)\n    n, err = store.SweepStaleOTPs(retention)\n}","tryCatchPattern":"n, err := store.SweepStaleOTPs(86400)\nif err != nil {\n    if strings.Contains(err.Error(), \"no such table\") {\n        return 0, errors.New(\"otp schema not migrated\")\n    }\n    return 0, err\n}","preventionTips":["Run OTP migrations before starting the retention sweeper","Use WAL mode + busy_timeout to coexist with OTP write traffic","Jitter sweep timing to avoid lock storms","Watch disk space — DELETE failures can stem from full volumes"],"tags":["go","database","sqlite","otp"],"backgroundTag":"database-write-failed","analyzedSha":"6e04ca5ff03222a699c24205cd4bf8fee9af7ffe","analyzedAt":"2026-09-07T15:44:59.941Z","contentChangedAt":"2026-09-07T15:44:59.941Z","schemaVersion":2},"datasetVersion":"2026-09-16T04:17:20.429Z"}