// idempotency.go remembers the first answer to a keyed request, so a
// caller that retries — an agent whose connection dropped, a browser
// that re-posts a form — gets the ticket it already filed instead of a
// second one. Stripe's Idempotency-Key, kept a day.
package oltp
import (
"database/sql"
"errors"
"time"
)
const idempotencySchema = `
CREATE TABLE IF NOT EXISTS idempotency (
scope TEXT NOT NULL,
key TEXT NOT NULL,
digest TEXT NOT NULL,
result TEXT NOT NULL,
at TEXT NOT NULL,
PRIMARY KEY (scope, key)
);`
// IdempotencyKeep is how long a key is remembered.
const IdempotencyKeep = 24 * time.Hour
// IdemRecall is the answer remembered for (scope, key), and the digest
// of the request that produced it, while it is younger than the keep.
func (d *DB) IdemRecall(scope, key string) (digest, result string, ok bool, err error) {
since := time.Now().UTC().Add(-IdempotencyKeep).Format(time.RFC3339)
err = d.db.QueryRow(`SELECT digest, result FROM idempotency WHERE scope = ? AND key = ? AND at >= ?`,
scope, key, since).Scan(&digest, &result)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return "", "", false, nil
}
return "", "", false, err
}
return digest, result, true, nil
}
// IdemRemember keeps the first answer for (scope, key) — a second one
// for the same key never replaces it — and forgets keys past the keep.
func (d *DB) IdemRemember(scope, key, digest, result string) error {
if _, err := d.db.Exec(`DELETE FROM idempotency WHERE at < ?`,
time.Now().UTC().Add(-IdempotencyKeep).Format(time.RFC3339)); err != nil {
return err
}
_, err := d.db.Exec(`INSERT INTO idempotency (scope, key, digest, result, at) VALUES (?, ?, ?, ?, ?)
ON CONFLICT (scope, key) DO NOTHING`, scope, key, digest, result, now())
return err
}