Something went wrong. Try again.
Monorepo for Tangled tangled.org
Something went wrong. Try again.
Go
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057package db
import ( "context" "database/sql" "errors" "path/filepath" "sync" "testing"
"tangled.org/core/spindle/quota")
func TestRepoDedup(t *testing.T) { ctx := context.Background() database, err := Make(ctx, filepath.Join(t.TempDir(), "spindle.db")) if err != nil { t.Fatal(err) } t.Cleanup(func() { database.Close() })
qs := NewQuotaStore(database, quota.Defaults{ quota.ScopeRepo: quota.Resources{ quota.ResourceCacheStorageBytes: 100, }, })
id1 := quota.Identity{OwnerDID: "did:web:alice", RepoDID: "did:web:alice/repo1"}
res, err := qs.Reserve(ctx, quota.ReserveRequest{ Kind: quota.KindNixCache, Key: "hash123", Resources: quota.Resources{ quota.ResourceCacheStorageBytes: 60, }, Identity: id1, }) if err != nil { t.Fatal(err) } if !res.Allowed || res.ID == "" { t.Fatalf("expected allowed reservation, got %v", res) }
err = qs.BeginCommit(ctx, res.ID) if err != nil { t.Fatal(err) } err = qs.Commit(ctx, res.ID) if err != nil { t.Fatal(err) }
res2, err := qs.Reserve(ctx, quota.ReserveRequest{ Kind: quota.KindNixCache, Key: "hash123", Resources: quota.Resources{ quota.ResourceCacheStorageBytes: 60, }, Identity: id1, }) if err != nil { t.Fatal(err) } if !res2.Allowed { t.Fatal("expected reservation to be allowed due to repo dedup") } if res2.ID != "" { t.Fatalf("expected empty reservation ID for already committed object, got %q", res2.ID) }
res3, err := qs.Reserve(ctx, quota.ReserveRequest{ Kind: quota.KindNixCache, Key: "hash456", Resources: quota.Resources{ quota.ResourceCacheStorageBytes: 60, }, Identity: id1, }) if err != nil { t.Fatal(err) } if res3.Allowed { t.Fatal("expected reservation to be rejected (exceeds limit)") }}
func TestOwnerUnionDedup(t *testing.T) { ctx := context.Background() database, err := Make(ctx, filepath.Join(t.TempDir(), "spindle.db")) if err != nil { t.Fatal(err) } t.Cleanup(func() { database.Close() })
qs := NewQuotaStore(database, quota.Defaults{ quota.ScopeUser: quota.Resources{ quota.ResourceCacheStorageBytes: 100, }, })
id1 := quota.Identity{OwnerDID: "did:web:alice", RepoDID: "did:web:alice/repo1"} id2 := quota.Identity{OwnerDID: "did:web:alice", RepoDID: "did:web:alice/repo2"}
res1, err := qs.Reserve(ctx, quota.ReserveRequest{ Kind: quota.KindNixCache, Key: "hash123", Resources: quota.Resources{ quota.ResourceCacheStorageBytes: 60, }, Identity: id1, }) if err != nil { t.Fatal(err) } if !res1.Allowed { t.Fatal("expected res1 to be allowed") }
if err := qs.BeginCommit(ctx, res1.ID); err != nil { t.Fatal(err) } if err := qs.Commit(ctx, res1.ID); err != nil { t.Fatal(err) }
res2, err := qs.Reserve(ctx, quota.ReserveRequest{ Kind: quota.KindNixCache, Key: "hash123", Resources: quota.Resources{ quota.ResourceCacheStorageBytes: 60, }, Identity: id2, }) if err != nil { t.Fatal(err) } if !res2.Allowed { t.Fatal("expected res2 to be allowed due to owner union dedup") }
res3, err := qs.Reserve(ctx, quota.ReserveRequest{ Kind: quota.KindNixCache, Key: "hash456", Resources: quota.Resources{ quota.ResourceCacheStorageBytes: 50, }, Identity: id2, }) if err != nil { t.Fatal(err) } if res3.Allowed { t.Fatal("expected res3 to be rejected") }}
func TestDifferentOwnerLogicalCharging(t *testing.T) { ctx := context.Background() database, err := Make(ctx, filepath.Join(t.TempDir(), "spindle.db")) if err != nil { t.Fatal(err) } t.Cleanup(func() { database.Close() })
qs := NewQuotaStore(database, quota.Defaults{ quota.ScopeUser: quota.Resources{ quota.ResourceCacheStorageBytes: 100, }, })
id1 := quota.Identity{OwnerDID: "did:web:alice", RepoDID: "did:web:alice/repo1"} id2 := quota.Identity{OwnerDID: "did:web:bob", RepoDID: "did:web:bob/repo1"}
res1, err := qs.Reserve(ctx, quota.ReserveRequest{ Kind: quota.KindNixCache, Key: "hash123", Resources: quota.Resources{ quota.ResourceCacheStorageBytes: 60, }, Identity: id1, }) if err != nil { t.Fatal(err) } if !res1.Allowed { t.Fatal("expected res1 to be allowed") }
res2, err := qs.Reserve(ctx, quota.ReserveRequest{ Kind: quota.KindNixCache, Key: "hash123", Resources: quota.Resources{ quota.ResourceCacheStorageBytes: 60, }, Identity: id2, }) if err != nil { t.Fatal(err) } if !res2.Allowed { t.Fatal("expected res2 to be allowed") }}
func TestBothLimits(t *testing.T) { ctx := context.Background() database, err := Make(ctx, filepath.Join(t.TempDir(), "spindle.db")) if err != nil { t.Fatal(err) } t.Cleanup(func() { database.Close() })
qs := NewQuotaStore(database, quota.Defaults{ quota.ScopeUser: quota.Resources{ quota.ResourceCacheStorageBytes: 100, }, quota.ScopeRepo: quota.Resources{ quota.ResourceCacheStorageBytes: 50, }, })
id1 := quota.Identity{OwnerDID: "did:web:alice", RepoDID: "did:web:alice/repo1"}
res, err := qs.Reserve(ctx, quota.ReserveRequest{ Kind: quota.KindNixCache, Key: "hash123", Resources: quota.Resources{ quota.ResourceCacheStorageBytes: 60, }, Identity: id1, }) if err != nil { t.Fatal(err) } if res.Allowed || res.Reason != string(quota.ReasonRepoLimit) { t.Fatalf("expected rejection due to repo limit, got allowed=%t reason=%s", res.Allowed, res.Reason) }}
func TestDefaultOverrideUnlimitedPrecedence(t *testing.T) { ctx := context.Background() database, err := Make(ctx, filepath.Join(t.TempDir(), "spindle.db")) if err != nil { t.Fatal(err) } t.Cleanup(func() { database.Close() })
qs := NewQuotaStore(database, quota.Defaults{ quota.ScopeUser: quota.Resources{ quota.ResourceCacheStorageBytes: 100, }, quota.ScopeRepo: quota.Resources{ quota.ResourceCacheStorageBytes: 50, }, })
id1 := quota.Identity{OwnerDID: "did:web:alice", RepoDID: "did:web:alice/repo1"}
res, err := qs.Reserve(ctx, quota.ReserveRequest{ Kind: quota.KindNixCache, Key: "hash123", Resources: quota.Resources{ quota.ResourceCacheStorageBytes: 60, }, Identity: id1, }) if err != nil { t.Fatal(err) } if res.Allowed { t.Fatal("expected rejection") }
if err := qs.SetLimit(ctx, id1.RepoDID, quota.ResourceCacheStorageBytes, 200); err != nil { t.Fatal(err) }
res2, err := qs.Reserve(ctx, quota.ReserveRequest{ Kind: quota.KindNixCache, Key: "hash123", Resources: quota.Resources{ quota.ResourceCacheStorageBytes: 60, }, Identity: id1, }) if err != nil { t.Fatal(err) } if !res2.Allowed { t.Fatal("expected allowed after repo override") }
res3, err := qs.Reserve(ctx, quota.ReserveRequest{ Kind: quota.KindNixCache, Key: "hash456", Resources: quota.Resources{ quota.ResourceCacheStorageBytes: 120, }, Identity: id1, }) if err != nil { t.Fatal(err) } if res3.Allowed { t.Fatal("expected rejection due to user limit") }
if err := qs.SetLimit(ctx, id1.OwnerDID, quota.ResourceCacheStorageBytes, 0); err != nil { t.Fatal(err) } if err := qs.SetLimit(ctx, id1.RepoDID, quota.ResourceCacheStorageBytes, 0); err != nil { t.Fatal(err) }
res4, err := qs.Reserve(ctx, quota.ReserveRequest{ Kind: quota.KindNixCache, Key: "hash456", Resources: quota.Resources{ quota.ResourceCacheStorageBytes: 120, }, Identity: id1, }) if err != nil { t.Fatal(err) } if !res4.Allowed { t.Fatal("expected zero override to make user quota unlimited") } if res4.Reason != quota.ReasonUnlimited { t.Fatalf("reason = %q, want %q", res4.Reason, quota.ReasonUnlimited) }}
func TestIdempotence(t *testing.T) { ctx := context.Background() database, err := Make(ctx, filepath.Join(t.TempDir(), "spindle.db")) if err != nil { t.Fatal(err) } t.Cleanup(func() { database.Close() })
qs := NewQuotaStore(database, quota.Defaults{ quota.ScopeUser: quota.Resources{ quota.ResourceCacheStorageBytes: 100, }, quota.ScopeRepo: quota.Resources{ quota.ResourceCacheStorageBytes: 100, }, }) id1 := quota.Identity{OwnerDID: "did:web:alice", RepoDID: "did:web:alice/repo1"}
res1, err := qs.Reserve(ctx, quota.ReserveRequest{ Kind: quota.KindNixCache, Key: "hash123", Resources: quota.Resources{ quota.ResourceCacheStorageBytes: 40, }, Identity: id1, }) if err != nil { t.Fatal(err) }
res2, err := qs.Reserve(ctx, quota.ReserveRequest{ Kind: quota.KindNixCache, Key: "hash123", Resources: quota.Resources{ quota.ResourceCacheStorageBytes: 40, }, Identity: id1, }) if err != nil { t.Fatal(err) } if res1.ID != res2.ID { t.Fatalf("expected identical reservation IDs, got %q and %q", res1.ID, res2.ID) }
if err := qs.BeginCommit(ctx, res1.ID); err != nil { t.Fatal(err) } if err := qs.BeginCommit(ctx, res1.ID); err != nil { t.Fatal(err) }
if err := qs.Commit(ctx, res1.ID); err != nil { t.Fatal(err) } if err := qs.Commit(ctx, res1.ID); err != nil { t.Fatal(err) }
if err := qs.Release(ctx, res1.ID); err != nil { t.Fatal(err) } if err := qs.Release(ctx, res1.ID); err != nil { t.Fatal(err) }
if err := qs.BeginCommit(ctx, ""); err != nil { t.Fatal(err) } if err := qs.Commit(ctx, ""); err != nil { t.Fatal(err) } if err := qs.Release(ctx, ""); err != nil { t.Fatal(err) }}
func TestQuotaTransactionRollsBackAfterContextCancellation(t *testing.T) { ctx := context.Background() database, err := Make(ctx, filepath.Join(t.TempDir(), "spindle.db")) if err != nil { t.Fatal(err) } t.Cleanup(func() { database.Close() }) qs := NewQuotaStore(database, nil)
txCtx, cancel := context.WithCancel(ctx) err = qs.withTx(txCtx, func(*sql.Conn) error { cancel() return txCtx.Err() }) if !errors.Is(err, context.Canceled) { t.Fatalf("transaction error = %v, want context canceled", err) }
if err := qs.SetLimit(ctx, "did:web:alice", quota.ResourceWorkflows, 1); err != nil { t.Fatalf("write after canceled transaction failed: %v", err) }}func TestCommitUpdatesChangedAmountForSameKey(t *testing.T) { ctx := context.Background() database, err := Make(ctx, filepath.Join(t.TempDir(), "spindle.db")) if err != nil { t.Fatal(err) } t.Cleanup(func() { database.Close() })
qs := NewQuotaStore(database, nil) identity := quota.Identity{OwnerDID: "did:web:alice", RepoDID: "did:web:alice/repo"} reserveAndCommit := func(amount int64) { t.Helper() res, err := qs.Reserve(ctx, quota.ReserveRequest{ Kind: quota.KindNixCache, Key: "nar/object.nar", Identity: identity, Resources: quota.Resources{quota.ResourceCacheStorageBytes: amount}, }) if err != nil { t.Fatal(err) } if !res.Allowed || res.ID == "" { t.Fatalf("reservation = %+v, want a new allowed reservation", res) } if err := qs.BeginCommit(ctx, res.ID); err != nil { t.Fatal(err) } if err := qs.Commit(ctx, res.ID); err != nil { t.Fatal(err) } }
reserveAndCommit(60) reserveAndCommit(80)
var amount int64 if err := database.QueryRow(` SELECT amount FROM quota_allocations WHERE repo_did = ? AND resource = ? AND kind = ? AND key = ? `, identity.RepoDID, quota.ResourceCacheStorageBytes, quota.KindNixCache, "nar/object.nar").Scan(&amount); err != nil { t.Fatal(err) } if amount != 80 { t.Fatalf("committed amount = %d, want 80", amount) }}
func TestRecoverUpdatesChangedAmountForPublishingKey(t *testing.T) { ctx := context.Background() database, err := Make(ctx, filepath.Join(t.TempDir(), "spindle.db")) if err != nil { t.Fatal(err) } t.Cleanup(func() { database.Close() }) qs := NewQuotaStore(database, nil) identity := quota.Identity{OwnerDID: "did:web:alice", RepoDID: "did:web:alice/repo"} reserve := func(amount int64) quota.Reservation { t.Helper() res, err := qs.Reserve(ctx, quota.ReserveRequest{ Kind: quota.KindNixCache, Key: "nar/object.nar", Identity: identity, Resources: quota.Resources{quota.ResourceCacheStorageBytes: amount}, }) if err != nil { t.Fatal(err) } if !res.Allowed || res.ID == "" { t.Fatalf("reservation = %+v, want a new allowed reservation", res) } if err := qs.BeginCommit(ctx, res.ID); err != nil { t.Fatal(err) } return res }
first := reserve(60) if err := qs.Commit(ctx, first.ID); err != nil { t.Fatal(err) } reserve(80) if err := qs.Recover(ctx, nil); err != nil { t.Fatal(err) }
var amount int64 if err := database.QueryRow(` SELECT amount FROM quota_allocations WHERE repo_did = ? AND resource = ? AND kind = ? AND key = ? `, identity.RepoDID, quota.ResourceCacheStorageBytes, quota.KindNixCache, "nar/object.nar").Scan(&amount); err != nil { t.Fatal(err) } if amount != 80 { t.Fatalf("recovered amount = %d, want 80", amount) }}
func TestRecoveryPhases(t *testing.T) { ctx := context.Background() database, err := Make(ctx, filepath.Join(t.TempDir(), "spindle.db")) if err != nil { t.Fatal(err) } t.Cleanup(func() { database.Close() })
const resource = "gpu_slices" qs := NewQuotaStore(database, quota.Defaults{ quota.ScopeUser: quota.Resources{ quota.ResourceCacheStorageBytes: 100, resource: 10, }, quota.ScopeRepo: quota.Resources{ quota.ResourceCacheStorageBytes: 100, resource: 10, }, }) id1 := quota.Identity{OwnerDID: "did:web:alice", RepoDID: "did:web:alice/repo1"}
res1, err := qs.Reserve(ctx, quota.ReserveRequest{ Kind: quota.KindNixCache, Key: "hash_reserved", Resources: quota.Resources{ quota.ResourceCacheStorageBytes: 10, }, Identity: id1, }) if err != nil { t.Fatal(err) }
res2, err := qs.Reserve(ctx, quota.ReserveRequest{ Kind: quota.KindNixCache, Key: "hash_publishing", Resources: quota.Resources{ quota.ResourceCacheStorageBytes: 20, }, Identity: id1, }) if err != nil { t.Fatal(err) } if err := qs.BeginCommit(ctx, res2.ID); err != nil { t.Fatal(err) }
res3, err := qs.Reserve(ctx, quota.ReserveRequest{ Kind: quota.KindWorkflow, Key: "workflow_preserved", Resources: quota.Resources{ resource: 1, }, Identity: id1, }) if err != nil { t.Fatal(err) } if err := qs.Recover(ctx, []string{res3.ID}); err != nil { t.Fatal(err) }
var count int err = database.QueryRowContext(ctx, ` SELECT COUNT(*) FROM quota_allocations WHERE key = 'hash_reserved' `).Scan(&count) if err != nil { t.Fatal(err) } if count != 0 { t.Fatal("expected reserved object to NOT be committed") }
err = database.QueryRowContext(ctx, ` SELECT COUNT(*) FROM quota_allocations WHERE key = 'hash_publishing' `).Scan(&count) if err != nil { t.Fatal(err) } if count != 1 { t.Fatal("expected publishing object to be committed") }
err = database.QueryRowContext(ctx, ` SELECT COUNT(*) FROM quota_reservations WHERE id = ? `, res3.ID).Scan(&count) if err != nil { t.Fatal(err) } if count != 1 { t.Fatal("expected preserved reservation to remain in reservations table") }
err = database.QueryRowContext(ctx, ` SELECT COUNT(*) FROM quota_reservations WHERE id IN (?, ?) `, res1.ID, res2.ID).Scan(&count) if err != nil { t.Fatal(err) } if count != 0 { t.Fatal("expected unpreserved reservations to be cleaned up") }}
func TestConcurrency(t *testing.T) { ctx := context.Background() database, err := Make(ctx, filepath.Join(t.TempDir(), "spindle.db")) if err != nil { t.Fatal(err) } t.Cleanup(func() { database.Close() })
qs := NewQuotaStore(database, quota.Defaults{ quota.ScopeUser: quota.Resources{ quota.ResourceCacheStorageBytes: 100, }, quota.ScopeRepo: quota.Resources{ quota.ResourceCacheStorageBytes: 100, }, }) id1 := quota.Identity{OwnerDID: "did:web:alice", RepoDID: "did:web:alice/repo1"}
var wg sync.WaitGroup const numGoroutines = 10 results := make([]quota.Reservation, numGoroutines) errorsList := make([]error, numGoroutines)
for i := range numGoroutines { wg.Add(1) go func(idx int) { defer wg.Done() res, err := qs.Reserve(ctx, quota.ReserveRequest{ Kind: quota.KindNixCache, Key: "hash_" + string(rune('a'+idx)), Resources: quota.Resources{ quota.ResourceCacheStorageBytes: 15, }, Identity: id1, }) results[idx] = res errorsList[idx] = err }(i) } wg.Wait()
allowedCount := 0 rejectedCount := 0 for i := range numGoroutines { if errorsList[i] != nil { t.Fatalf("goroutine %d failed with error: %v", i, errorsList[i]) } if results[i].Allowed { allowedCount++ } else { rejectedCount++ } }
if allowedCount > 6 { t.Fatalf("expected at most 6 allowed reservations, got %d", allowedCount) } if allowedCount == 0 { t.Fatal("expected at least one reservation to be allowed") }}
func TestMetricsSnapshot(t *testing.T) { ctx := context.Background() database, err := Make(ctx, filepath.Join(t.TempDir(), "spindle.db")) if err != nil { t.Fatal(err) } t.Cleanup(func() { database.Close() })
qs := NewQuotaStore(database, quota.Defaults{ quota.ScopeRepo: quota.Resources{ quota.ResourceCacheStorageBytes: 100, }, })
repos := []struct { repo string owner string obj string bytes int64 override int64 }{ {"did:web:alice/repo1", "did:web:alice", "obj1", 50, -2}, {"did:web:alice/repo2", "did:web:alice", "obj2", 80, -2}, {"did:web:alice/repo3", "did:web:alice", "obj3", 90, -2}, {"did:web:alice/repo4", "did:web:alice", "obj4", 100, -2}, {"did:web:alice/repo5", "did:web:alice", "obj5", 120, -2}, {"did:web:alice/repo6", "did:web:alice", "obj6", 200, -1}, }
for _, r := range repos { _, err = database.ExecContext(ctx, ` INSERT INTO quota_repo_owners (repo_did, owner_did) VALUES (?, ?) `, r.repo, r.owner) if err != nil { t.Fatal(err) } _, err = database.ExecContext(ctx, ` INSERT INTO quota_allocations (repo_did, resource, kind, key, amount) VALUES (?, 'cache_storage_bytes', 'nix_cache', ?, ?) `, r.repo, r.obj, r.bytes) if err != nil { t.Fatal(err) } if r.override != -2 { var val any = nil if r.override >= 0 { val = r.override } _, err = database.ExecContext(ctx, ` INSERT INTO quota_limits (did, resource, max_amount) VALUES (?, 'cache_storage_bytes', ?) `, r.repo, val) if err != nil { t.Fatal(err) } } } _, err = database.ExecContext(ctx, ` INSERT INTO quota_repo_owners (repo_did, owner_did) VALUES (?, ?) `, "did:web:bob/idle", "did:web:bob") if err != nil { t.Fatal(err) }
snapshot, err := qs.MetricsSnapshot(ctx) if err != nil { t.Fatal(err) }
repoCounts := snapshot.Subjects[quota.ScopeRepo][quota.ResourceCacheStorageBytes] if repoCounts["under_limit"] != 2 { t.Errorf("expected 2 under_limit repos including the idle default-limited repo, got %d", repoCounts["under_limit"]) } if repoCounts["near_limit"] != 2 { t.Errorf("expected 2 near_limit repos, got %d", repoCounts["near_limit"]) } if repoCounts["at_limit"] != 1 { t.Errorf("expected 1 at_limit repo, got %d", repoCounts["at_limit"]) } if repoCounts["over_limit"] != 1 { t.Errorf("expected 1 over_limit repo, got %d", repoCounts["over_limit"]) } if repoCounts["unlimited"] != 1 { t.Errorf("expected 1 unlimited repo, got %d", repoCounts["unlimited"]) }}
func TestUserLimitAllowZeroAdditional(t *testing.T) { ctx := context.Background() database, err := Make(ctx, filepath.Join(t.TempDir(), "spindle.db")) if err != nil { t.Fatal(err) } t.Cleanup(func() { database.Close() })
qs := NewQuotaStore(database, quota.Defaults{ quota.ScopeUser: quota.Resources{ quota.ResourceCacheStorageBytes: 100, }, }) id1 := quota.Identity{OwnerDID: "did:web:alice", RepoDID: "did:web:alice/repo1"}
res1, err := qs.Reserve(ctx, quota.ReserveRequest{ Kind: quota.KindNixCache, Key: "hash123", Resources: quota.Resources{ quota.ResourceCacheStorageBytes: 80, }, Identity: id1, }) if err != nil { t.Fatal(err) } if !res1.Allowed { t.Fatal("expected reservation to be allowed") }
if err := qs.SetLimit(ctx, id1.OwnerDID, quota.ResourceCacheStorageBytes, 50); err != nil { t.Fatal(err) }
res2, err := qs.Reserve(ctx, quota.ReserveRequest{ Kind: quota.KindNixCache, Key: "hash123", Resources: quota.Resources{ quota.ResourceCacheStorageBytes: 80, }, Identity: id1, }) if err != nil { t.Fatal(err) } if !res2.Allowed { t.Fatal("expected deduplicated reservation to be allowed even when over limit") }}
func TestUnequalOwnerClaimsRejected(t *testing.T) { ctx := context.Background() database, err := Make(ctx, filepath.Join(t.TempDir(), "spindle.db")) if err != nil { t.Fatal(err) } t.Cleanup(func() { database.Close() })
qs := NewQuotaStore(database, quota.Defaults{ quota.ScopeUser: quota.Resources{ quota.ResourceCacheStorageBytes: 100, }, })
id1 := quota.Identity{OwnerDID: "did:web:alice", RepoDID: "did:web:alice/repo1"} id2 := quota.Identity{OwnerDID: "did:web:alice", RepoDID: "did:web:alice/repo2"}
res1, err := qs.Reserve(ctx, quota.ReserveRequest{ Kind: quota.KindNixCache, Key: "hash123", Resources: quota.Resources{ quota.ResourceCacheStorageBytes: 60, }, Identity: id1, }) if err != nil { t.Fatal(err) } if !res1.Allowed || res1.ID == "" { t.Fatalf("expected allowed reservation, got %v", res1) }
if err := qs.BeginCommit(ctx, res1.ID); err != nil { t.Fatal(err) } if err := qs.Commit(ctx, res1.ID); err != nil { t.Fatal(err) }
res2, err := qs.Reserve(ctx, quota.ReserveRequest{ Kind: quota.KindNixCache, Key: "hash123", Resources: quota.Resources{ quota.ResourceCacheStorageBytes: 80, }, Identity: id2, }) if err != nil { t.Fatal(err) } if res2.Allowed { t.Fatal("expected unequal larger claim to be rejected at user limit") }
res3, err := qs.Reserve(ctx, quota.ReserveRequest{ Kind: quota.KindNixCache, Key: "hash123", Resources: quota.Resources{ quota.ResourceCacheStorageBytes: 60, }, Identity: id2, }) if err != nil { t.Fatal(err) } if !res3.Allowed { t.Fatal("expected equal-amount cross-repo dedup claim to be allowed") }
res4, err := qs.Reserve(ctx, quota.ReserveRequest{ Kind: quota.KindNixCache, Key: "hash123", Resources: quota.Resources{ quota.ResourceCacheStorageBytes: 30, }, Identity: id2, }) if err != nil { t.Fatal(err) } if !res4.Allowed { t.Fatal("expected smaller unequal claim to be allowed since total is within limit") }}
func TestUnequalOwnerClaimsCharged(t *testing.T) { ctx := context.Background() database, err := Make(ctx, filepath.Join(t.TempDir(), "spindle.db")) if err != nil { t.Fatal(err) } t.Cleanup(func() { database.Close() })
qs := NewQuotaStore(database, quota.Defaults{ quota.ScopeUser: quota.Resources{ quota.ResourceCacheStorageBytes: 200, }, })
id1 := quota.Identity{OwnerDID: "did:web:alice", RepoDID: "did:web:alice/repo1"} id2 := quota.Identity{OwnerDID: "did:web:alice", RepoDID: "did:web:alice/repo2"}
res1, err := qs.Reserve(ctx, quota.ReserveRequest{ Kind: quota.KindNixCache, Key: "hash123", Resources: quota.Resources{ quota.ResourceCacheStorageBytes: 60, }, Identity: id1, }) if err != nil { t.Fatal(err) } if !res1.Allowed || res1.ID == "" { t.Fatalf("expected allowed reservation, got %v", res1) }
if err := qs.BeginCommit(ctx, res1.ID); err != nil { t.Fatal(err) } if err := qs.Commit(ctx, res1.ID); err != nil { t.Fatal(err) }
res2, err := qs.Reserve(ctx, quota.ReserveRequest{ Kind: quota.KindNixCache, Key: "hash123", Resources: quota.Resources{ quota.ResourceCacheStorageBytes: 80, }, Identity: id2, }) if err != nil { t.Fatal(err) } if !res2.Allowed || res2.ID == "" { t.Fatalf("expected allowed unequal claim, got %v", res2) }
if err := qs.BeginCommit(ctx, res2.ID); err != nil { t.Fatal(err) } if err := qs.Commit(ctx, res2.ID); err != nil { t.Fatal(err) }
res3, err := qs.Reserve(ctx, quota.ReserveRequest{ Kind: quota.KindNixCache, Key: "hash456", Resources: quota.Resources{ quota.ResourceCacheStorageBytes: 70, }, Identity: id1, }) if err != nil { t.Fatal(err) } if res3.Allowed { t.Fatal("expected new claim to be rejected since user limit has been exceeded by charged unequal claims") }}
func TestLimitArithmeticDoesNotOverflow(t *testing.T) { ctx := context.Background() database, err := Make(ctx, filepath.Join(t.TempDir(), "spindle.db")) if err != nil { t.Fatal(err) } t.Cleanup(func() { database.Close() })
qs := NewQuotaStore(database, quota.Defaults{}) id := quota.Identity{OwnerDID: "did:web:alice", RepoDID: "did:web:alice/repo"} const limit = int64(1 << 62) if err := qs.SetLimit(ctx, id.RepoDID, "capacity", limit); err != nil { t.Fatal(err) } reserve := func(key string, amount int64) bool { t.Helper() res, err := qs.Reserve(ctx, quota.ReserveRequest{ Kind: quota.KindWorkflow, Key: key, Identity: id, Resources: quota.Resources{"capacity": amount}, }) if err != nil { t.Fatal(err) } return res.Allowed } if !reserve("large", limit-1) || reserve("overflow", 2) { t.Fatal("overflow-safe limit admission produced the wrong decisions") }
first, err := qs.Reserve(ctx, quota.ReserveRequest{ Kind: quota.KindWorkflow, Key: "unlimited-1", Identity: id, Resources: quota.Resources{"unlimited": quota.MaxResourceAmount}, }) if err != nil || !first.Allowed { t.Fatalf("first unlimited reservation = %+v, %v", first, err) } second, err := qs.Reserve(ctx, quota.ReserveRequest{ Kind: quota.KindWorkflow, Key: "unlimited-2", Identity: id, Resources: quota.Resources{"unlimited": quota.MaxResourceAmount}, }) if err != nil || second.Allowed || !second.Temporary { t.Fatalf("overflowing unlimited reservation = %+v, %v", second, err) } if _, err := qs.ListUsage(ctx); err != nil { t.Fatalf("safe maximum usage failed to aggregate: %v", err) } if _, err := database.ExecContext(ctx, ` INSERT INTO quota_allocations (repo_did, resource, kind, key, amount) VALUES (?, 'unlimited', 'generic_cache', 'forced-overflow', ?) `, id.RepoDID, quota.MaxResourceAmount); err != nil { t.Fatal(err) } if _, err := qs.ListUsage(ctx); err == nil { t.Fatal("expected pre-existing overflowing usage to return an error") }}