Skip to content
quarry Artifact [7d9a5ac2b2]
Login

Artifact [7d9a5ac2b2]

Artifact 7d9a5ac2b215497a73565c8d3cdc4846d4bf547ea65f7f4569bd21cbcd69c646:


// 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
}