feat(worker): add MinIO storage_bytes scanner - #4
Merged
Conversation
Closes the documented "MinIO storage_bytes tracking not implemented" gap in CLAUDE.md. UpdateStorageBytesWorker now updates resources.storage_bytes for resource_type='storage' rows by listing objects under the tenant's prefix in the shared MinIO bucket, alongside the existing postgres/redis/mongodb path that calls through the gRPC provisioner. - New MinIOStorageScanner interface + minio-go/v7 implementation; reuses the same MINIO_ENDPOINT / MINIO_ROOT_USER / MINIO_ROOT_PASSWORD env vars the worker already loads for IAM cleanup - Prefix derivation matches api/internal/providers/storage/local.go (first 8 chars of token + "/") and the provisioner-side scanner, so worker-reported usage stays consistent with what the API allocated - Counts committed objects (incl. versions, skipping delete markers and zero-byte dir placeholders) plus incomplete multipart uploads - Wired into workers.StartWorkers — nil scanner when MINIO_ENDPOINT is unset (fail-open, storage rows are skipped each run with a warn log) - MINIO_BUCKET_NAME added to Config (default "instant-shared") - Unit tests with a fake minioObjectLister cover total summing, multipart inclusion, delete-marker / dir-placeholder exclusion, bucket-missing, provider_resource_id override, plus end-to-end sqlmock'd worker pipeline asserting storage_bytes = X is persisted Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
mastermanas805
added a commit
that referenced
this pull request
Jun 4, 2026
…#3) (#85) * fix(quota): re-measure suspended resources so they can auto-unsuspend Finding #3 (SWEEP-BACKLOG-2026-06-04, P1). UpdateStorageBytesWorker scanned only status='active' resources, so a quota-suspended resource's storage_bytes was frozen at the over-cap value forever. EnforceStorageQuotaWorker's runUnsuspendLoop reads that persisted column (readStorageBytes) to decide whether usage has dropped below the hysteresis threshold — with the value frozen it never could, so a suspended resource stayed suspended permanently. The suspend email promises "access restored automatically once usage drops"; that was a no-op. Fix: the scanner now selects status IN ('active','suspended') so suspended rows keep being measured. Suspend-trigger behaviour for active rows is unchanged — runSuspendLoop independently scans status='active'; this worker only writes the storage_bytes column both loops read. Since this touched a previously-unbounded scan (ORDER BY created_at, no LIMIT), it is now keyset-paginated (id::text > cursor ORDER BY id::text, batch 1000), mirroring the quota.go reconciler scans. Scan errors stay fail-open (CLAUDE.md #1): log + stop paginating this run (cursor can't advance without a valid id), re-run next tick. The api half (ElevateResourceTiersByTeam tier-upgrade rescue, finding #4) is fixed separately in the api repo. Tests: - TestUpdateStorageBytesWorker_RemeasuresSuspendedRow — asserts the scan status args are exactly ('active','suspended') and a suspended row is re-measured + its storage_bytes updated (measurement half). - TestEnforceStorageQuotaWorker_UnderQuota_UnsuspendsResource (existing) — once storage_bytes drops, the row is unsuspended (release half). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(quota): scan suspended resources so they can auto-unsuspend (sweep #3) The storage_bytes scanner queried only status='active', so a quota-suspended resource's usage was never re-measured — runUnsuspendLoop never saw it drop under cap and the resource stayed suspended forever, breaking the suspend email's promise that "access is restored automatically once usage drops". Fix: scan `status IN ('active', 'suspended')`. Minimal one-line change — the broader keyset-pagination rewrite was dropped from this PR (the other reconciler scanners already got keyset in #81/#82; this scanner's pagination is a separate concern and is left for its own PR to keep this fix small and fully covered). Test: TestUpdateStorageBytesWorker_RemeasuresSuspendedRow pins the WHERE clause to ('active','suspended') so dropping 'suspended' reds the build, and asserts a suspended row is re-measured + its storage_bytes updated. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes the "MinIO storage_bytes tracking not implemented" gap flagged in
CLAUDE.md. TheUpdateStorageBytesWorkernow populatesresources.storage_bytesforresource_type='storage'rows by listing objects under the tenant's prefix in the shared MinIO bucket — bringing MinIO resources into the same quota-enforcement pipeline that already covers postgres/redis/mongodb.MinIOStorageScannerinterface with agithub.com/minio/minio-go/v7implementation, reusing the worker's existingMINIO_ENDPOINT/MINIO_ROOT_USER/MINIO_ROOT_PASSWORDconfig that's already loaded for IAM cleanup.api/internal/providers/storage/local.goand the provisioner-side scanner (first 8 chars of token +/), so worker-reported usage stays consistent with what the API allocated.provisioner/internal/backend/storage/minio.go.workers.StartWorkers; nil scanner whenMINIO_ENDPOINTis unset (fail-open — storage rows are skipped each run with a warn log, postgres/redis/mongo continue via the gRPC provisioner path).MINIO_BUCKET_NAMEadded tointernal/config/config.Config(defaultinstant-shared).Test plan
go build ./...from worker rootgo vet ./...from worker rootgo test ./...from worker root — all existing tests still passTestUpdateStorageBytesWorker_MinIOResource_PersistsTotalfeeds in three objects totaling 7168 bytes via a fakeminioObjectListerand asserts the worker writesstorage_bytes = 7168to a sqlmock'd DBprovider_resource_idoverride of the token-derived prefixnil(no UPDATE), nil scanner skips storage rows (no UPDATE)MINIO_ENDPOINT/MINIO_ROOT_USER/MINIO_ROOT_PASSWORD/MINIO_BUCKET_NAMEtoinfra/k8s/worker/deployment.yaml(lives in a separate repo — follow-up wiring change)Notes
infra/k8s/worker/deployment.yamlcurrently has no MinIO env vars, so the scanner will run as a no-op in production until that infra PR lands. The fail-open path (minio_scanner_unavailablewarn log per storage row) keeps this safe.GetStorageBytesover gRPC. Adding the direct path in the worker avoids a gRPC roundtrip per storage row and keeps the worker self-contained for the credentials it already holds.🤖 Generated with Claude Code