Skip to content
quarry Artifact [eaa032a044]
Login

Artifact [eaa032a044]

Artifact eaa032a044edf36dc08036901cad16ad841a22b3327878052adf8ea64edce659:


package api

import (
	"context"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"log"
	"sync"

	"craftdesign.group/quarry/internal/ops"
)

// IdempotencyStore remembers the first answer to a keyed request
// (oltp/idempotency.go).
type IdempotencyStore interface {
	IdemRecall(scope, key string) (digest, result string, ok bool, err error)
	IdemRemember(scope, key, digest, result string) error
}

// idemLocks serializes one (scope, key) at a time, so two copies of the
// same retry arriving together file one ticket, not two.
var idemLocks sync.Map

// idempotent runs a filing op at most once per key. A caller that sends
// the same Idempotency-Key again — an agent retrying after a dropped
// connection, a form posted twice — gets the first answer back instead
// of a second ticket. The key is scoped to the op, the repository and
// the caller, so one caller's key never reaches another's answer; the
// same key with a DIFFERENT request is refused rather than guessed at,
// because answering it with the first ticket would hide that the second
// was never filed. No key, or a forge with no store, runs as before.
func idempotent[In any, Out any](ctx context.Context, d Deps, op, repo, key string, in In, run func() (Out, error)) (Out, error) {
	var zero Out
	if key == "" || d.Idem == nil {
		return run()
	}
	scope := op + "|" + repo + "|" + author(ctx)
	mu, _ := idemLocks.LoadOrStore(scope+"|"+key, &sync.Mutex{})
	mu.(*sync.Mutex).Lock()
	defer mu.(*sync.Mutex).Unlock()

	raw, _ := json.Marshal(in)
	sum := sha256.Sum256(raw)
	digest := hex.EncodeToString(sum[:])
	was, result, ok, err := d.Idem.IdemRecall(scope, key)
	if err != nil {
		return zero, err
	}
	if ok {
		if was != digest {
			return zero, fmt.Errorf("this Idempotency-Key already filed a different request; use a new key for a new ticket: %w", ops.ErrInvalid)
		}
		var out Out
		if err := json.Unmarshal([]byte(result), &out); err != nil {
			return zero, err
		}
		return out, nil
	}
	out, err := run()
	if err != nil {
		return out, err
	}
	if b, merr := json.Marshal(out); merr == nil {
		if rerr := d.Idem.IdemRemember(scope, key, digest, string(b)); rerr != nil {
			log.Printf("idempotency: %s: remembering %s: %v", scope, key, rerr)
		}
	}
	return out, nil
}