Skip to content

feat(worker): add MinIO storage_bytes scanner - #4

Merged
mastermanas805 merged 1 commit into
masterfrom
feat/minio-storage-bytes-tracking
May 11, 2026
Merged

feat(worker): add MinIO storage_bytes scanner#4
mastermanas805 merged 1 commit into
masterfrom
feat/minio-storage-bytes-tracking

Conversation

@mastermanas805

Copy link
Copy Markdown
Member

Summary

Closes the "MinIO storage_bytes tracking not implemented" gap flagged in CLAUDE.md. The UpdateStorageBytesWorker now populates resources.storage_bytes for resource_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.

  • New MinIOStorageScanner interface with a github.com/minio/minio-go/v7 implementation, reusing the worker's existing MINIO_ENDPOINT / MINIO_ROOT_USER / MINIO_ROOT_PASSWORD config that's already loaded for IAM cleanup.
  • Prefix derivation matches api/internal/providers/storage/local.go and the provisioner-side scanner (first 8 chars of token + /), so worker-reported usage stays consistent with what the API allocated.
  • Counts committed objects (including versions, skipping delete markers and zero-byte directory placeholders) plus incomplete multipart uploads — same accounting model as provisioner/internal/backend/storage/minio.go.
  • Wired into workers.StartWorkers; nil scanner when MINIO_ENDPOINT is unset (fail-open — storage rows are skipped each run with a warn log, postgres/redis/mongo continue via the gRPC provisioner path).
  • MINIO_BUCKET_NAME added to internal/config/config.Config (default instant-shared).

Test plan

  • go build ./... from worker root
  • go vet ./... from worker root
  • go test ./... from worker root — all existing tests still pass
  • New unit test TestUpdateStorageBytesWorker_MinIOResource_PersistsTotal feeds in three objects totaling 7168 bytes via a fake minioObjectLister and asserts the worker writes storage_bytes = 7168 to a sqlmock'd DB
  • Five MinIO-scanner unit tests cover: object total summing, incomplete-multipart inclusion, delete-marker / dir-placeholder exclusion, bucket-missing, provider_resource_id override of the token-derived prefix
  • Two pipeline fail-open tests cover: MinIO listing error returns nil (no UPDATE), nil scanner skips storage rows (no UPDATE)
  • Out of scope for this PR: add MINIO_ENDPOINT / MINIO_ROOT_USER / MINIO_ROOT_PASSWORD / MINIO_BUCKET_NAME to infra/k8s/worker/deployment.yaml (lives in a separate repo — follow-up wiring change)

Notes

  • Worker deployment manifest at infra/k8s/worker/deployment.yaml currently 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_unavailable warn log per storage row) keeps this safe.
  • The provisioner already has a MinIO scanner used by GetStorageBytes over 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

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
mastermanas805 merged commit 0e9de08 into master May 11, 2026
@mastermanas805
mastermanas805 deleted the feat/minio-storage-bytes-tracking branch May 11, 2026 13:03
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant