You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
165 lines
4.8 KiB
165 lines
4.8 KiB
package inventory
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
"github.com/cloudreve/Cloudreve/v4/ent"
|
|
"github.com/cloudreve/Cloudreve/v4/pkg/logging"
|
|
)
|
|
|
|
type TxOperator interface {
|
|
SetClient(newClient *ent.Client) TxOperator
|
|
GetClient() *ent.Client
|
|
}
|
|
|
|
type (
|
|
Tx struct {
|
|
tx *ent.Tx
|
|
parent *Tx
|
|
inherited bool
|
|
finished bool
|
|
storageDiff StorageDiff
|
|
}
|
|
|
|
// TxCtx is the context key for inherited transaction
|
|
TxCtx struct{}
|
|
)
|
|
|
|
// ReserveStorage atomically adds `size` bytes to user `uid`'s storage inside
|
|
// this transaction, enforcing storage + size <= maxTotal at the database
|
|
// level (when maxTotal > 0). This is the correct API for grabbing the
|
|
// per-user storage quota:
|
|
//
|
|
// - It MUST be called as the first write of the transaction (or at least
|
|
// before any other WRITE to files/entities/etc.). That way the users-row
|
|
// X lock is acquired before any INSERT into files/entities, so
|
|
// concurrent uploads for the same owner serialize on the users row from
|
|
// the start and cannot deadlock via foreign-key S locks on the shared
|
|
// parent folder / storage-policy rows.
|
|
// - A matching compensating negative diff is appended to storageDiff so
|
|
// the pre-reservation is netted against the positive diff emitted later
|
|
// by CreateEntity / fc.Copy / etc. No amount is double-applied.
|
|
// - On failure (quota exceeded or any other DB error) the caller MUST
|
|
// Rollback the transaction: any placeholder writes done afterwards
|
|
// would otherwise be committed without a matching storage reservation.
|
|
//
|
|
// It is safe to call this from an inherited-tx wrapper (returned by
|
|
// InheritTx) — AppendStorageDiff routes to the root tx.
|
|
func (t *Tx) ReserveStorage(ctx context.Context, uc UserClient, uid int, size, maxTotal int64) error {
|
|
if size <= 0 {
|
|
return nil
|
|
}
|
|
|
|
txUc, _ := InheritTx(ctx, uc)
|
|
if err := txUc.ReserveStorage(ctx, uid, size, maxTotal); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Net out the pre-reservation against any positive diff that
|
|
// CreateEntity / fc.Copy / ... will append later in the same tx.
|
|
t.AppendStorageDiff(StorageDiff{uid: -size})
|
|
return nil
|
|
}
|
|
|
|
// AppendStorageDiff appends the given storage diff to the transaction.
|
|
func (t *Tx) AppendStorageDiff(diff StorageDiff) {
|
|
root := t
|
|
for root.inherited {
|
|
root = root.parent
|
|
}
|
|
|
|
if root.storageDiff == nil {
|
|
root.storageDiff = diff
|
|
} else {
|
|
root.storageDiff.Merge(diff)
|
|
}
|
|
}
|
|
|
|
// WithTx wraps the given inventory client with a transaction.
|
|
func WithTx[T TxOperator](ctx context.Context, c T) (T, *Tx, context.Context, error) {
|
|
var txClient *ent.Client
|
|
var txWrapper *Tx
|
|
|
|
if txInherited, ok := ctx.Value(TxCtx{}).(*Tx); ok && !txInherited.finished {
|
|
txWrapper = &Tx{inherited: true, tx: txInherited.tx, parent: txInherited}
|
|
} else {
|
|
tx, err := c.GetClient().Tx(ctx)
|
|
if err != nil {
|
|
return c, nil, ctx, fmt.Errorf("failed to create transaction: %w", err)
|
|
}
|
|
|
|
txWrapper = &Tx{inherited: false, tx: tx}
|
|
ctx = context.WithValue(ctx, TxCtx{}, txWrapper)
|
|
}
|
|
|
|
txClient = txWrapper.tx.Client()
|
|
return c.SetClient(txClient).(T), txWrapper, ctx, nil
|
|
}
|
|
|
|
// InheritTx wraps the given inventory client with a transaction.
|
|
// If the transaction is already in the context, it will be inherited.
|
|
// Otherwise, original client will be returned.
|
|
func InheritTx[T TxOperator](ctx context.Context, c T) (T, *Tx) {
|
|
var txClient *ent.Client
|
|
var txWrapper *Tx
|
|
|
|
if txInherited, ok := ctx.Value(TxCtx{}).(*Tx); ok && !txInherited.finished {
|
|
txWrapper = &Tx{inherited: true, tx: txInherited.tx, parent: txInherited}
|
|
txClient = txWrapper.tx.Client()
|
|
return c.SetClient(txClient).(T), txWrapper
|
|
}
|
|
|
|
return c, nil
|
|
}
|
|
|
|
func Rollback(tx *Tx) error {
|
|
if !tx.inherited {
|
|
tx.finished = true
|
|
return tx.tx.Rollback()
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func commit(tx *Tx) (bool, error) {
|
|
if !tx.inherited {
|
|
tx.finished = true
|
|
return true, tx.tx.Commit()
|
|
}
|
|
return false, nil
|
|
}
|
|
|
|
func Commit(tx *Tx) error {
|
|
_, err := commit(tx)
|
|
return err
|
|
}
|
|
|
|
// CommitWithStorageDiff commits the transaction and applies the accumulated
|
|
// storage diff. Only the outermost (non-inherited) transaction performs the
|
|
// commit and the storage mutation.
|
|
//
|
|
// Quota enforcement is done UPFRONT via Tx.ReserveStorage, not here. This
|
|
// function's role is to (a) commit the tx, and (b) apply any residual
|
|
// storage diff produced by CreateEntity / CapEntities / etc. Tx.ReserveStorage
|
|
// pre-emits a matching negative diff, so pre-reserved sizes are netted out
|
|
// against the positive diff CreateEntity appends later. Residual is applied
|
|
// AFTER commit via ApplyStorageDiff (auto-commit, safe to retry on
|
|
// deadlock).
|
|
func CommitWithStorageDiff(ctx context.Context, tx *Tx, l logging.Logger, uc UserClient) error {
|
|
committed, err := commit(tx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if !committed {
|
|
return nil
|
|
}
|
|
|
|
if err := uc.ApplyStorageDiff(ctx, tx.storageDiff); err != nil {
|
|
l.Error("Failed to apply storage diff", "error", err)
|
|
}
|
|
|
|
return nil
|
|
}
|