{"record":{"id":"a3fedacb031066a4","repo":"OpenNHP/opennhp","slug":"keystore-sweep-expired-w","errorCode":null,"errorMessage":"keystore: sweep expired: %w","messagePattern":"keystore: sweep expired: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"endpoints/server/keystore.go","lineNumber":536,"sourceCode":"}\n\n// SweepExpiredDeactivates flips active=0 for any row whose expires_at has\n// elapsed. Returns the number of rows updated. NULL expires_at rows are\n// never swept (they are configured to never expire). The result of\n// FindAgentByPublicKey / IsAgentRegistered does not depend on this\n// sweeper — those functions already filter on expires_at — so this\n// method is purely a hygiene / index-utility measure.\nfunc (s *AgentKeyStore) SweepExpiredDeactivates() (int64, error) {\n\tres, err := s.db.Exec(\n\t\t`UPDATE agent_keys\n\t\t SET active = 0\n\t\t WHERE active = 1\n\t\t   AND expires_at IS NOT NULL\n\t\t   AND expires_at <= ?`,\n\t\ttime.Now().Unix(),\n\t)\n\tif err != nil {\n\t\treturn 0, fmt.Errorf(\"keystore: sweep expired: %w\", err)\n\t}\n\tn, err := res.RowsAffected()\n\tif err != nil {\n\t\treturn 0, fmt.Errorf(\"keystore: sweep rows affected: %w\", err)\n\t}\n\treturn n, nil\n}\n\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","sourceCodeStart":518,"sourceCodeEnd":554,"githubUrl":"https://github.com/OpenNHP/opennhp/blob/6e04ca5ff03222a699c24205cd4bf8fee9af7ffe/endpoints/server/keystore.go#L518-L554","documentation":"SweepExpiredDeactivates wraps failures of the UPDATE that flips active=0 on rows whose expires_at has elapsed. This is the first of two failure points in the sweep; the second is RowsAffected (error 145). Any Exec failure — locked DB, missing table, read-only file — is wrapped with this message.","triggerScenarios":"Calling SweepExpiredDeactivates() (typically from a periodic hygiene loop) when the database is locked by concurrent writers, agent_keys does not exist, the DB file is read-only, or the disk is full.","commonSituations":"Sweeper timer firing while a registration upsert holds the write lock; deploying a new server against an un-migrated DB; container with a read-only mount for the data directory.","solutions":["Read the wrapped driver error to distinguish lock contention from schema/storage problems.","Schedule the sweep at a low-traffic interval or add retry-with-backoff around lock errors.","Run migrations so agent_keys exists before the sweeper starts.","Ensure the data directory is writable by the nhp-serverd process user.","Use WAL journal mode so the UPDATE does not block on readers."],"exampleFix":"null","handlingStrategy":"retry","validationCode":"var tables int\ndb.QueryRow(\"SELECT count(*) FROM sqlite_master WHERE type='table' AND name='agent_keys'\").Scan(&tables)\nif tables == 0 { return errors.New(\"agent_keys missing; migrate first\") }","typeGuard":"n, err := store.SweepExpiredDeactivates()\nif err != nil {\n    if isBusyOrLocked(err) {\n        time.Sleep(backoff)\n        n, err = store.SweepExpiredDeactivates()\n    }\n    if err != nil { return err }\n}","tryCatchPattern":"for attempt := 0; attempt < 3; attempt++ {\n    n, err := store.SweepExpiredDeactivates()\n    if err == nil { return n, nil }\n    if !isLockError(err) { return 0, err }\n    time.Sleep(time.Duration(attempt+1) * time.Second)\n}\nreturn 0, errors.New(\"sweep: retries exhausted\")","preventionTips":["Run the sweeper off-peak or with jittered intervals","Enable WAL journal mode so UPDATE does not block on readers","Ensure the data directory is writable (check container mounts)","Confirm migrations ran before starting background sweepers"],"tags":["go","database","sqlite","maintenance"],"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"}