mirror of
https://github.com/hanzoai/commerce.git
synced 2026-10-30 15:08:42 +00:00
Refunds and disputes were recorded under an in-process mutex, and the renewal guard was a read then a write under another. Two processes over one store, or a webhook beside the billing cycle, could each read the invoice and each write it back, the second wiping the first's refund. A swap writes only while the stored entity's updatedAt is still the one read (insert-if-absent for a new or deleted entity) and otherwise reads again: db.Swapper on SQLite and Postgres as one statement, datastore.Swap, mixin.Model.Swap. Refund and dispute recording swap the invoice or payment record in, and idempotencykey.Begin swaps its first sighting and its stale re-claim, so a guarded money move runs once across replicas. The returns and lockPeriod mutexes are gone. lockFunds stays: api/transaction mounts only in standalone commerce, whose store has one writer. The tests open one store file through two handles, each with its own write lock: 24 refunds delivered twice to both, with dispute reports between them, land once each; 32 sightings of one key proceed once. A blind write in the refund recorder fails the first test every run.
42 lines
1.2 KiB
Go
42 lines
1.2 KiB
Go
package datastore
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/hanzoai/commerce/db"
|
|
"github.com/hanzoai/commerce/mintauth"
|
|
)
|
|
|
|
// Swap writes val under keyOrKind only while the stored entity is still the one
|
|
// read at prev, its updatedAt (zero for an entity never stored): db.Swapper. It
|
|
// reports whether it wrote; false means another writer changed the entity since
|
|
// the read, and the caller reads again. A store that cannot swap refuses rather
|
|
// than fall back to a blind put.
|
|
func (d *Datastore) Swap(keyOrKind interface{}, prev time.Time, val interface{}) (bool, error) {
|
|
if d.database == nil {
|
|
return false, errors.New("datastore: database not initialized")
|
|
}
|
|
if err := mintauth.Enforce(d.Context, val); err != nil {
|
|
d.warn("Refused unauthorized mint (%v, %#v): %v", convertKeyOrKind(d, keyOrKind), val, err, d.Context)
|
|
return false, err
|
|
}
|
|
s, ok := d.database.(db.Swapper)
|
|
if !ok {
|
|
return false, fmt.Errorf("datastore: %T cannot swap", d.database)
|
|
}
|
|
stamp := ""
|
|
if !prev.IsZero() {
|
|
b, err := json.Marshal(prev)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
stamp = strings.Trim(string(b), `"`)
|
|
}
|
|
dskey := convertKeyOrKind(d, keyOrKind)
|
|
return s.Swap(d.Context, dskey.ToDBKey(d.database), stamp, val)
|
|
}
|