{"record":{"id":"2e0683c6993b3e23","repo":"OpenNHP/opennhp","slug":"keystore-insert-otp-w","errorCode":null,"errorMessage":"keystore: insert otp: %w","messagePattern":"keystore: insert otp: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"endpoints/server/keystore.go","lineNumber":243,"sourceCode":"\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"keystore: generate otp: %w\", err)\n\t}\n\n\tnow := time.Now().Unix()\n\texpires := time.Now().Add(p.TTL).Unix()\n\n\t// Invalidate previous unused OTPs for this user+device.\n\t_, _ = s.db.Exec(\n\t\t`UPDATE otp_records SET used = 1 WHERE usr_id = ? AND dev_id = ? AND used = 0`,\n\t\tp.UserId, p.DeviceId,\n\t)\n\n\t_, err = s.db.Exec(\n\t\t`INSERT INTO otp_records (usr_id, dev_id, otp_code, pub_key, created_at, expires_at) VALUES (?, ?, ?, ?, ?, ?)`,\n\t\tp.UserId, p.DeviceId, code, p.PublicKey, now, expires,\n\t)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"keystore: insert otp: %w\", err)\n\t}\n\n\tlog.Info(\"keystore: otp generated for user=%s device=%s\", p.UserId, p.DeviceId)\n\treturn code, nil\n}\n\n// MaxOTPAttempts is the number of consecutive incorrect OTP guesses allowed\n// before the OTP is invalidated.\nconst MaxOTPAttempts = 5\n\n// ValidateOTP checks the OTP for the given user+device. Returns nil on\n// success, or a specific error:\n//\n//\tErrOTPInvalid     — no matching OTP found (or wrong code)\n//\tErrOTPExpired     — OTP has expired\n//\tErrOTPAlreadyUsed — OTP was already used\n//\tErrOTPRateLimited — too many failed attempts; OTP has been invalidated\n//\tErrOTPPublicKeyMismatch — OTP was issued for a different public key","sourceCodeStart":225,"sourceCodeEnd":261,"githubUrl":"https://github.com/OpenNHP/opennhp/blob/6e04ca5ff03222a699c24205cd4bf8fee9af7ffe/endpoints/server/keystore.go#L225-L261","documentation":"GenerateOTP inserts a row into otp_records; an Exec failure is wrapped as 'keystore: insert otp'. Typical causes are SQLITE_BUSY under write contention, a UNIQUE/constraint violation, or database I/O errors.","triggerScenarios":"High OTP issuance rate exceeding the single-writer connection's throughput (busy_timeout 5000ms exhausted), disk full, or a corrupted/truncated otp_records table.","commonSituations":"Load tests hammering OTP generation against one SQLite file, deployments with the keystore on a network filesystem that breaks SQLite locking, container disk quota exhausted.","solutions":["Read the wrapped cause: SQLITE_BUSY means contention — serialize writes or move to a client-server DB at scale.","Never place the SQLite file on NFS/network storage; SQLite locking is local-disk only.","Check disk quota/space in the container or volume.","Ensure only one process writes; SetMaxOpenConns(1) already serializes within the process."],"exampleFix":null,"handlingStrategy":"retry","validationCode":"// keep the database on local writable disk\nif fi, err := os.Stat(dbPath); err != nil || !fi.Mode().Perm().IsRegular() { /* abort */ }","typeGuard":null,"tryCatchPattern":"if err != nil && strings.Contains(err.Error(), \"insert otp\") {\n    if strings.Contains(err.Error(), \"SQLITE_BUSY\") {\n        time.Sleep(100 * time.Millisecond) // bounded retry\n    }\n}","preventionTips":["Do not place SQLite files on NFS","Rate-limit OTP generation endpoints","Alert on disk-quota exhaustion","Keep one writer process per database"],"tags":["go","sqlite","insert","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"}