package 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") } }