Skip to content

fix: free iceberg shards slots by catalog id, not name - #1368

Open
ferhatelmas wants to merge 2 commits into
masterfrom
ferhat/free-by-catalog-id
Open

ferhatelmas wants to merge 2 commits into
masterfrom
ferhat/free-by-catalog-id

Conversation

@ferhatelmas

@ferhatelmas ferhatelmas commented Sep 8, 2026

Copy link
Copy Markdown
Member

What kind of change does this PR introduce?

Bug fix

What is the current behavior?

Iceberg shard slots are allocated by id but freed by name so leaks.

What is the new behavior?

It's now always using id consistently.
Add admin triggered reclaim job that only does when slot identity matches tenant/resource, no local table metadata, warehouse is reachable, table is gone.

Signed-off-by: Ferhat Elmas <elmas.ferhat@gmail.com>
@ferhatelmas
ferhatelmas requested a review from a team as a code owner September 8, 2026 09:43
Copilot AI lite review requested due to automatic review settings September 8, 2026 09:43

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The change is small, aligns behavior across allocation/freeing paths, and is backed by targeted regression tests covering the id-vs-name mismatch scenario.

Pull request overview

This PR fixes an Iceberg sharding resource leak by making shard slot freeing use the same identifier as allocation (catalog id), instead of mixing catalog id for allocation and catalog name for freeing.

Changes:

  • Update TenantAwareRestCatalog to free shard resources using catalog.id instead of params.warehouse (catalog name).
  • Update IcebergCatalogReconciler to consistently use catalog.id when finding/reserving/confirming shard allocations.
  • Add/extend tests to cover the “catalog id differs from name” scenario for both mutation and reconciliation paths.
File summaries
File Description
src/storage/protocols/iceberg/catalog/tenant-catalog.ts Uses catalog.id when freeing shard resources to match the allocation resource key.
src/storage/protocols/iceberg/catalog/tenant-catalog.test.ts Adds a regression test ensuring the freed shard resource matches the allocated one when id != name.
src/storage/protocols/iceberg/catalog/reconciler.ts Switches shard resource lookups/reservations/confirmations to use catalog.id instead of catalog.name.
src/storage/protocols/iceberg/catalog/reconciler.test.ts Adds a test asserting the reconciler uses catalog IDs for shard resource matching and allocation.
Review details
  • Files reviewed: 4/4 changed files
  • Comments generated: 0
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@coveralls

coveralls commented Sep 8, 2026

Copy link
Copy Markdown

Coverage Report for CI Build 34260580560

Coverage increased (+0.6%) to 82.901%

Details

  • Coverage increased (+0.6%) from the base build.
  • Patch coverage: 3 uncovered changes across 2 files (117 of 120 lines covered, 97.5%).
  • 19 coverage regressions across 3 files.

Uncovered Changes

File Changed Covered %
src/storage/events/iceberg/reclaim-shard-slots.ts 14 12 85.71%
src/storage/events/workers.ts 1 0 0.0%
Total (9 files) 120 117 97.5%

Coverage Regressions

19 previously-covered lines in 3 files lost coverage.

File Lines Losing Coverage Coverage
src/internal/http/agent.ts 17 32.56%
src/http/routes/s3/index.ts 1 87.42%
src/storage/protocols/iceberg/catalog/rest-catalog-client.ts 1 87.04%

Coverage Stats

Coverage Status
Relevant Lines: 13752
Covered Lines: 11863
Line Coverage: 86.26%
Relevant Branches: 8203
Covered Branches: 6338
Branch Coverage: 77.26%
Branches in Coverage %: Yes
Coverage Strength: 680.28 hits per line

💛 - Coveralls

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Beyond the inline findings about migration/backfill of existing resource_id rows, I checked that this change is consistent with the rest of the file — createTable already used catalog.id for bucketName (lines 227, 271 pre-existing), so this fix just aligns dropTable and the reconciler to match, rather than introducing a new pattern.

Extended reasoning...

This is a small, targeted bugfix: reconciler.ts and tenant-catalog.ts's dropTable previously used catalog.name (or params.warehouse) instead of catalog.id when building the shard resource key passed to the sharder, causing a free/reserve mismatch after a catalog rename. I verified createTable in tenant-catalog.ts already used catalog.id at lines 227 and 271, confirming the fix brings dropTable and the reconciler into line with the existing convention rather than inventing a new one — reducing risk of the fix itself being wrong. New tests were added in both test files exercising the exact create-then-drop scenario with a catalog whose id differs from its name, asserting reserve/confirm/freeByResource are all called with the same bucketName.

No security-sensitive surface is touched (no auth, crypto, or permission logic); this is purely an internal resource-key consistency fix in shard bookkeeping.

Given the inline findings already raised (about whether existing/stale shard_slots or shard_reservation rows keyed by name need a migration/backfill), a human should weigh in on the operational rollout question, which is a legitimate concern the code review alone cannot settle.

@ferhatelmas
ferhatelmas requested a lite review from Copilot September 8, 2026 12:29
@ferhatelmas

Copy link
Copy Markdown
Member Author

@claude review always

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

It introduces a new job that can delete shard reservations/slots in production, so it warrants final human verification of operational safety and rollout expectations.

Review details
  • Files reviewed: 13/13 changed files
  • Comments generated: 1
  • Review effort level: Lite

Comment thread src/storage/protocols/iceberg/catalog/reclaim-shard-slots.ts

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.

Comment thread src/storage/protocols/iceberg/catalog/reclaim-shard-slots.ts Outdated
@ferhatelmas
ferhatelmas force-pushed the ferhat/free-by-catalog-id branch from dc03b20 to 9bb68ce Compare September 8, 2026 13:47

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.

Beyond the inline finding on batch resumability, I checked two other angles and ruled them out: the reclaimer's "local table" guard matches only on (tenant_id, namespace_id, name), not the candidate's shard_id/slot_no, but iceberg_tables has a unique index per (tenant_id, namespace_id, catalog_id, name), so a table can only have one live shard binding at a time — no masked duplicate-leak path. I also verified the reconciler's Promise.all → sequential-loop refactor doesn't drop restorations on error: an unswallowed exception just stops that pass, and the remaining missing tables are picked up on the next reconciliation run.

Extended reasoning...

This run's confirmed inline finding (admin endpoint missing afterReservationId, combined with runBatch/inspect re-throwing non-timeout upstream errors, meaning an operator has no way to resume past a permanently-failing candidate) is new and not something I raised in my prior two reviews of this PR, so it warrants surfacing. Beyond that, I independently re-examined the two candidate issues surfaced this run and was able to concretely rule both out from the code and schema: the iceberg_tables unique index on (tenant_id, namespace_id, catalog_id, name) means the reclaimer's name-only local-table check cannot mask a genuinely orphaned duplicate reservation, since only one shard binding can be live per table at a time; and the reconciler's switch from Promise.all to a sequential per-table loop with only 404/NoSuchTable swallowed doesn't lose any missing-table restoration work, it just defers it to the next reconciliation pass on error, matching prior (fail-fast) behavior. My earlier inline comment about the advisory lock being held across external REST-catalog calls during batch reclamation remains open and unaddressed by the latest commit, but since it was already posted in a prior round I am not repeating it here.

Comment thread src/http/routes/admin/iceberg-admin.ts
@ferhatelmas
ferhatelmas force-pushed the ferhat/free-by-catalog-id branch 2 times, most recently from aeefe88 to 9545253 Compare September 8, 2026 14:35

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This pull request has been reviewed before and this review found new issues. Where they share a root cause, one fix may close them together.

Comment thread src/storage/protocols/iceberg/catalog/reconciler.ts Outdated
@ferhatelmas
ferhatelmas force-pushed the ferhat/free-by-catalog-id branch 2 times, most recently from 139d5fc to 8e79b30 Compare September 8, 2026 14:59
@ferhatelmas
ferhatelmas requested a lite review from Copilot September 8, 2026 14:59

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The new reclaimer’s “allocation unchanged” guard query does not verify the reservation row’s tenant/resource match, which can allow freeing a live reservation if rows become temporarily inconsistent.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details
  • Files reviewed: 14/14 changed files
  • Comments generated: 1
  • Review effort level: Lite

Comment thread src/storage/protocols/iceberg/catalog/reclaim-shard-slots.ts
Comment thread src/storage/protocols/iceberg/catalog/reconciler.ts
@ferhatelmas
ferhatelmas force-pushed the ferhat/free-by-catalog-id branch 2 times, most recently from 839d2d1 to 9579f7a Compare September 8, 2026 15:43
@ferhatelmas
ferhatelmas requested a lite review from Copilot September 8, 2026 15:43

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

It introduces new operational behavior (admin endpoint + queue worker + locking/rollback semantics) in addition to the bug fix, which warrants final human validation in the target deployment environment.

Review details
  • Files reviewed: 14/14 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Critical reservation-collision handling and moderate reclamation retry issues must be resolved before approval.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details
  • Files reviewed: 14/14 changed files
  • Comments generated: 3
  • Review effort level: Balanced

Comment thread src/storage/protocols/iceberg/catalog/reconciler.ts
Comment thread src/storage/protocols/iceberg/catalog/reclaim-shard-slots.ts
Comment thread src/storage/protocols/iceberg/catalog/reclaim-shard-slots.ts

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.

Beyond the inline finding, this run also checked two other candidates and ruled them out: the reclamation continuation's job-id check in reclaim-shard-slots.ts (Event.send() can return null on an exactly_once policy drop, but the continuation's singletonKey is unique per batch so that path isn't actually reachable here), and the new ReconciliationRollbackError aborting reconcile() on a rollback failure in reconciler.ts (this matches the PR's stated intent to fail fast rather than silently swallow errors, not an oversight).

Extended reasoning...

This run's inline finding (namespace bucket-name property never being sent upstream by tenant-catalog.ts's createNamespace, so syncUpstreamOrphanTables' catalogName lookup always falls back to findFirstCatalog) is a genuine new issue not raised in prior rounds. Two other candidates surfaced by the hunt were investigated and ruled out rather than posted: the nextJobId === undefined check in reclaim-shard-slots.ts:57 is technically checking the wrong sentinel (pg-boss send() signals a policy drop via null, not undefined) but the continuation's singletonKey (runId + the new afterReservationId) is always fresh, so the exactly_once collision this guards against cannot actually occur on the continuation path; and reconciler.ts's new ReconciliationRollbackError aborting the whole reconcile() on a rollback failure is the PR's deliberate fail-fast design (explicitly called out in the PR description as replacing silent Promise.allSettled swallowing), not an accidental behavior change. Given the PR already carries a large number of open threads from prior rounds (several resolved only by the author, not independently), and this run adds one more confirmed, unaddressed issue, a full body reiterating "needs human review" would be redundant — the inline finding and the still-open prior threads already make that clear.

Additional findings (outside the current diff — GitHub can't attach inline comments there):

  • 🟣 src/storage/protocols/iceberg/catalog/reconciler.ts — syncUpstreamOrphanTables resolves the owning catalog via namespaceResp.properties['bucket-name'], but tenant-catalog.ts's createNamespace never sends that property upstream (commented out at tenant-catalog.ts:237), so catalogName is always undefined and it always falls back to findFirstCatalog (arbitrary catalog, no ORDER BY). For tenants with more than one catalog, an orphan table gets attributed to the wrong catalog.id/bucket_name on restore. Fix: resolve the catalog from the local iceberg_namespaces row for dbNamespaceId (source of truth) instead of an upstream property that is never populated, or actually set bucket-name when creating namespaces. Pre-existing, but this diff extends the same lookup to feed migrateLegacyAllocation/reserve with catalog.id.

    Extended reasoning...

    In syncUpstreamOrphanTables (reconciler.ts:127-172), catalogName is read from namespaceResp.properties?.['bucket-name']. tenant-catalog.ts createNamespace (lines 230-240) has the properties block that would set 'bucket-name' commented out, so no REST catalog namespace ever carries this property. Therefore catalogName is always undefined, findCatalogByName is never invoked, and every orphan-table restoration falls through to findFirstCatalog(tnx, tenantId), which does SELECT id, name FROM iceberg_catalogs WHERE tenant_id=$1 AND deleted_at IS NULL LIMIT 1 with no ORDER BY. For a tenant with 2+ catalogs, this can attribute a restored table to the wrong catalog_id/bucket_name in iceberg_tables and in the shard reservation (bucketName: catalog.id), silently misrouting a recovered table to an unrelated catalog.

    Verification: pre-existing. The mechanism is real and reachable: reconciler.ts:148 reads catalogName = namespaceResp.properties?.['bucket-name']; the upstream bucket-name property is commented out in tenant-catalog.ts:234-239 ("Underline catalog doesn't support this"), so catalogName is always undefined, findCatalogByName is never invoked, and control falls to findFirstCatalog (reconciler.ts:154).…

Signed-off-by: Ferhat Elmas <elmas.ferhat@gmail.com>
@ferhatelmas
ferhatelmas force-pushed the ferhat/free-by-catalog-id branch from 9579f7a to 5a8a41e Compare September 8, 2026 18:01
@ferhatelmas
ferhatelmas requested a balanced review from Copilot September 8, 2026 18:01

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

It introduces new production reclamation logic that can delete shard allocations and adds an admin-triggered worker/endpoint, so it merits careful human validation of operational safety despite strong tests.

Review details
  • Files reviewed: 17/17 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reviewed this PR's latest push (commit 5a8a41e, "add job to reclaim leaked shards") and found no new bugs. The concerns I raised in earlier rounds on this PR — legacy catalog-name-keyed reservations not being migrated when the reconciler restores an upstream-orphan table, the orphan-restore loop aborting entirely on one table's failure, the namespace advisory lock spanning external REST-catalog calls, and the admin endpoint lacking a way to resume a scan past a poison reservation — all now appear addressed in the current code (a migrateLegacyAllocation step, per-table try/catch, the listNamespaces probe moved outside the lock/transaction with bounded lock/statement timeouts, and an exposed afterReservationId body field). Given the size and sensitivity of this change (new background worker, admin endpoint, reworked reconciler, and shard-slot locking logic) and outstanding third-party review threads, a human look is still worthwhile.

Extended reasoning...

Overview

This run reviews the final state of a large, multi-push PR that adds an Iceberg shard-slot reclamation background worker and admin endpoint, and reworks the reconciler and locking logic that allocates/frees shard slots for Iceberg tables. The diff spans 17 files and ~2000 changed lines, including a substantial new commit (5a8a41e) pushed after all of my prior reviews on this PR.

Security risks

No new injection, auth-bypass, or data-exposure risk was found in this run. The admin route is behind API-key auth and gated on multitenant+queue-enabled mode; SQL queries use parameter binding throughout the reconciler and reclaimer.

Level of scrutiny

This PR touches shard allocation, locking, and reconciliation logic that is directly responsible for preventing data loss/corruption and resource leaks across tenants — high-sensitivity code that merits careful review, which is consistent with the many rounds of feedback already exchanged on it.

Other factors

I traced each of my four previously-raised concerns (reconciler.ts legacy-resource-id migration, per-table loop isolation, namespace-lock-across-network-calls, and the admin endpoint's missing resume parameter) against the current code and found each one substantively addressed by the latest commit. I did not re-verify the Copilot bot's more recent inline comments (reconciler.ts:285, reclaim-shard-slots.ts:152) against the current code, since their content isn't visible to me and the file has changed significantly since those were posted — that residual uncertainty, plus the overall scope of the change, is why I'm deferring to a human rather than approving outright.

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.

3 participants