Skip to content

feat: add automated SP dataset cleanup jobs - #691

Merged
silent-cipher merged 16 commits into
mainfrom
feat/automated-sp-dataset-cleanup
Sep 21, 2026
Merged

silent-cipher merged 16 commits into
mainfrom
feat/automated-sp-dataset-cleanup

Conversation

@beck-8

@beck-8 beck-8 commented Sep 1, 2026

Copy link
Copy Markdown
Member

https://telemetry.betterstack.com/team/t468215/dashboards/618457/charts/18254157065?rf=now-7d&rt=now
Situation where the testnet dataset is exceeded
image

Summary

  • New global job sp_dataset_pruning: prunes blocked SPs to 0 active
    data sets, and prunes any other SP (trickle-tier or full-rate)
    whose active data-set count exceeds its tier's target by more than
    <NET>_EXCESS_DATASET_BUFFER (default 5) — a safety net against
    unbounded accumulation regardless of cause.
  • New global job abandoned_dataset_sweep: cleans up data sets
    belonging to permanently unreachable SPs via PDPVerifier's
    permissionless deleteDataSet path (no signature needed, reclaims
    the cleanup deposit via cleanupPieces), and auto-resolves stuck
    rail settlements via the permissionless settleRail where
    possible — only escalating genuinely stuck cases (validator issues)
    to a human, surfaced via a structured log with a ready-to-use Safe
    Transaction Builder batch.
  • New metrics: sp_termination_attempts_total,
    sp_termination_stuck_gauge.
  • Runbook: docs/runbooks/wallet-and-session-keys.md (Automated SP
    Cleanup section).

Follow-ups (not in this PR)

  • infra: add a BetterStack/Grafana alert rule on
    sp_termination_stuck_gauge.
  • Separate investigation: why some SPs (e.g. calibnet 2/4/9, 23)
    over-accumulate active data sets. Confirmed at least one distinct
    mechanism for 23: data_set_lifecycle_check throwaway sets whose
    immediate termination keeps failing while proving stays healthy —
    so abandoned_dataset_sweep's inactivity-based cleanup never
    triggers for them (root cause likely on that SP's own RPC/relay
    path, not a dealbot bug). 2/4/9 haven't been traced yet — could
    be the same termination-failure pattern, a data-set-reuse issue in
    provisionNextMissingDataSet, or something else. This PR's
    excessDatasetBuffer pruning retries provider-relay termination
    daily regardless of cause, which helps if failures are intermittent
    but can't resolve a persistently broken termination path.

Test plan

  • pnpm --filter dealbot-backend test — 708 passing
  • pnpm --filter dealbot-backend typecheck
  • biome check
  • Deploy to calibnet first; watch the first abandoned_dataset_sweep
    run (large one-time historical backlog, will take a while and use
    real testnet FIL for gas) and the session key's FIL balance
    (wallet_balance{currency="FIL"})

related #605 #681 #689

Adds two global pg-boss jobs that stop dealbot's wallet from
accumulating leftover data sets that cost money without providing
value:

- sp_dataset_pruning: terminates active data sets for blocked SPs
  down to 0, and prunes any SP (trickle or full-rate) whose active
  count exceeds its tier's target by more than a configurable buffer
  — a safety net against unbounded accumulation independent of root
  cause.
- abandoned_dataset_sweep: for data sets abandoned past PDPVerifier's
  ~30-day inactivity window, deletes them directly and
  permissionlessly (no signature required) and reclaims the cleanup
  deposit; for terminated-but-unsettled rails past their lockup,
  first attempts the permissionless settleRail automatically, only
  surfacing genuinely stuck cases to a human via a structured log
  with a ready-to-use Safe Transaction Builder batch.

Closes #681, #689
Addresses #605
@FilOzzy FilOzzy added this to FOC Sep 1, 2026
@github-project-automation github-project-automation Bot moved this to 📌 Triage in FOC Sep 1, 2026
@beck-8
beck-8 requested a balanced review from Copilot September 1, 2026 13:11
@beck-8 beck-8 self-assigned this Sep 1, 2026
@FilOzzy FilOzzy moved this from 📌 Triage to ⌨️ In Progress in FOC Sep 1, 2026

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.

Pull request overview

Adds automated, network-aware cleanup of excess and abandoned SP datasets, including rail settlement recovery and operational monitoring.

Changes:

  • Adds scheduled dataset pruning and abandonment sweep jobs.
  • Adds cleanup metrics, configuration, and queue integration.
  • Documents configuration and operator recovery procedures.

Reviewed changes

Copilot reviewed 20 out of 20 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
docs/runbooks/wallet-and-session-keys.md Adds cleanup runbook.
docs/environment-variables.md Documents cleanup settings.
apps/backend/.env.example Adds example variables.
apps/backend/src/config/constants.ts Defines defaults.
apps/backend/src/config/env.schema.ts Validates variables.
apps/backend/src/config/loader.ts Loads cleanup configuration.
apps/backend/src/config/network-fields.ts Registers network variables.
apps/backend/src/config/types.ts Adds configuration types.
apps/backend/src/data-set-lifecycle/data-set-lifecycle.service.ts Exports relay termination helper.
apps/backend/src/database/entities/job-schedule-state.entity.ts Adds job types.
apps/backend/src/jobs/job-queues.ts Defines cleanup queues.
apps/backend/src/jobs/jobs.module.ts Imports cleanup module.
apps/backend/src/jobs/jobs.service.ts Schedules and runs cleanup jobs.
apps/backend/src/jobs/jobs.service.spec.ts Updates job-service fixtures.
apps/backend/src/jobs/repositories/job-schedule.repository.ts Adds queue metric mappings.
apps/backend/src/metrics-prometheus/metrics-prometheus.module.ts Registers cleanup metrics.
apps/backend/src/sp-cleanup/sp-cleanup.module.ts Defines cleanup module.
apps/backend/src/sp-cleanup/sp-cleanup.service.ts Implements pruning and sweeping.
apps/backend/src/sp-cleanup/sp-cleanup.service.spec.ts Tests cleanup behavior.
apps/backend/src/wallet-sdk/wallet-sdk.service.spec.ts Updates network fixture.
Suppressed comments (2)

apps/backend/src/sp-cleanup/sp-cleanup.service.ts:343

  • Important: verify that the mined deletion transaction succeeded before recording a successful attempt. Viem's waitForTransactionReceipt resolves for receipts whose status is reverted; this path would increment the success metric, log the data set as deleted, and enter cleanup even though no state changed.
      const hash = await writeContract(writeClient, request);
      await waitForTransactionReceipt(writeClient, { hash });
      this.recordAttempt(network, "abandonment", "success");

apps/backend/src/sp-cleanup/sp-cleanup.service.ts:393

  • Blocker: make piece-cleanup failures verifiable and retriable. A mined reverted receipt is currently treated as success here, while the catch below converts every RPC, submission, or receipt failure into “cleanup not needed” and returns. After deleteDataSet has entered cleanup mode, later sweeps have no path that resumes cleanupPieces, so remaining pieces and the FIL deposit can be stranded permanently. Check receipt.status, suppress only the specifically decoded already-finalized/not-in-cleanup condition, and ensure cleanup-mode data sets are rediscovered and resumed after transient failures.
        const hash = await writeContract(writeClient, request);
        await waitForTransactionReceipt(writeClient, { hash });

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

Comment thread apps/backend/src/jobs/jobs.service.ts Outdated
Comment thread apps/backend/src/sp-cleanup/sp-cleanup.service.ts Outdated
Comment thread docs/environment-variables.md Outdated
- handleAbandonmentCandidate: check receipt.status after deleteDataSet
  so a reverted-but-mined tx is treated as a failure, not a success
- runDatasetPruning: scope the non-blocked prune loop to
  findAllByNetwork instead of findActiveAddresses, so registry-inactive
  providers aren't silently excluded from the excess-dataset safety net
- runDatasetPruning / runAbandonedDatasetSweep: accept an AbortSignal,
  bounded by a new SP_CLEANUP_JOB_TIMEOUT_SECONDS (default 20m), wired
  into onApplicationShutdown's drain timeout like every other job

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

Cleanup recovery, rail finalization, provider coverage, and timeout enforcement contain correctness gaps that could leave resources permanently stranded.

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

Review details

Suppressed comments (2)

Previously missed (1) — in code that hasn't changed since the last review.

apps/backend/src/sp-cleanup/sp-cleanup.service.ts:552

  • Important: this is not the Safe Transaction Builder checksum algorithm, so every generated “ready-to-use” batch is flagged as modified on import. Safe canonicalizes recursively with sorted keys, removes the checksum, and sets meta.name to null before hashing; hashing plain JSON.stringify(batch) produces a different value. Use Safe’s canonical checksum implementation (or intentionally omit the optional checksum) and add a compatibility test against a known batch.

docs/environment-variables.md:709

  • Important: this description contradicts the implementation and runbook by saying stuck rails can only be surfaced to a human; the job first resolves them automatically with permissionless settleRail and escalates only reverts. Update this operational documentation so responders know which cases require the Safe batch.
**Role**: How often the `abandoned_dataset_sweep` job runs for this network. This global job scans dealbot's
entire wallet (not scoped to the blocklist) for data sets outside the PDPVerifier activity window and deletes
them directly and permissionlessly (no signature needed — see the runbook), and for terminated data sets whose
rail settlement is stuck past `endEpoch`, which it can only ever surface to a human operator via a structured
log (`stuck_terminations_detected`).
  • Files reviewed: 20/20 changed files
  • Comments generated: 5
  • Review effort level: Balanced

Comment thread apps/backend/src/sp-cleanup/sp-cleanup.service.ts
Comment thread apps/backend/src/sp-cleanup/sp-cleanup.service.ts Outdated
Comment thread apps/backend/src/sp-cleanup/sp-cleanup.service.ts Outdated
Comment thread apps/backend/src/sp-cleanup/sp-cleanup.service.ts Outdated
Comment thread apps/backend/src/sp-cleanup/sp-cleanup.service.ts Outdated
- settleOrFlagStuck: always attempt settleRail once getRail succeeds,
  instead of skipping when settledUpTo >= endEpoch. A terminated rail
  in that state is fully settled but not yet finalized (lockup still
  held) — settleRail's finalize-only path was being silently skipped,
  leaking the lockup forever. Switched from settleRailSync (which
  requires a RailSettled event that this path never emits) to a plain
  settleRail + receipt.status check.
- finishCleanupPieces: retry transient failures (RPC/receipt issues)
  up to 5 times in a row instead of giving up on the first error;
  only treat the specific DataSetNotInCleanupMode revert as "done".
  deleteDataSet already removes the data set from FWSS's
  clientDataSets, so a future sweep can never rediscover it to retry —
  this call is the only chance.
- Propagate the AbortSignal into terminateExcessDataSets and the
  abandonment/cleanup helpers, checked once per data set/iteration
  rather than only once per provider. Moved the finishCleanupPieces
  call outside deleteDataSet's try/catch so an abort raised inside it
  doesn't get relabeled as a deleteDataSet failure.
- docs: correct the abandoned_dataset_sweep description — stuck rails
  are resolved automatically via permissionless settleRail first, and
  only escalated to a human when settlement genuinely reverts.

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

Dataset selection, post-delete recovery, timeout enforcement, and reverted-settlement classification have unresolved correctness risks.

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

Review details

Suppressed comments (1)

apps/backend/src/sp-cleanup/sp-cleanup.service.ts:408

  • Blocker: Make post-delete piece cleanup recoverable. These lines confirm that a successful deleteDataSet removes the ID from the only discovery path, yet the method can subsequently exit on the 20-minute abort, a process restart, five transient RPC failures, or the iteration cap. Any such exit permanently strands the remaining pieces and cleanup deposit, which is especially likely during the documented large historical sweep. Persist or derive cleanup-mode data-set IDs independently of listDataSets and retry them across runs before considering deletion complete.
    // `deleteDataSet` already removed this data set from FWSS's `clientDataSets`, so a future
    // sweep's `listDataSets` can never rediscover it to retry a failed batch — every reasonable
    // effort has to happen right here, in this one call.
    const MAX_CONSECUTIVE_FAILURES = 5;
  • Files reviewed: 20/20 changed files
  • Comments generated: 3
  • Review effort level: Balanced

Comment thread apps/backend/src/sp-cleanup/sp-cleanup.service.ts Outdated
Comment thread apps/backend/src/sp-cleanup/sp-cleanup.service.ts Outdated
Comment thread apps/backend/src/sp-cleanup/sp-cleanup.service.ts
- terminateExcessDataSets: sort leaked data_set_lifecycle_check sets
  first regardless of age, before falling back to oldest-first. Pure
  count-based oldest-first pruning could otherwise terminate a real
  dealbotDS-tagged slot while a newer leaked throwaway set (which
  survives "keep the newest N") crowds it out.
- terminateExcessDataSets: wrap the provider-relay wait in
  awaitWithAbort, matching the existing pattern in
  data-set-lifecycle.service.ts, so an unresponsive SP status endpoint
  can't hold the sweep past its configured timeout.
- settleOrFlagStuck: a mined-but-reverted settleRail receipt is now
  returned as a StuckRailItem directly instead of being thrown as a
  plain Error — the latter failed isContractRevert's check and
  misclassified a genuine on-chain revert as a transient RPC failure.
@beck-8
beck-8 marked this pull request as ready for review September 2, 2026 09:43
@beck-8
beck-8 requested a review from SgtPooki September 2, 2026 09:44
@BigLep
BigLep requested review from BigLep and Kubuxu and removed request for SgtPooki September 3, 2026 05:39
@BigLep

BigLep commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

@beck-8 : thanks for posting.

I would like to take a look (I've assigned myself as a reviewer) but don't block on me.
I'm also thinking to put @Kubuxu rather than @SgtPooki given Kuba is on the Storage Service workstream and Russell is not (but others feel free to step in here if you disagree.)
That said, Kuba is likely backlogged on reviews with helping looking at builtin-actors for nv29 this week.

@Kubuxu

Kubuxu commented Sep 3, 2026

Copy link
Copy Markdown

I can't meaningfully review this PR this week. Even after that, I have almost no knowledge of the dealbot codebase. If I review this PR, it is going to be strongly LLM-guided.

@silent-cipher silent-cipher left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Also another nit: can we use dataSet instead of dataset?

Comment thread apps/backend/src/jobs/jobs.service.ts Outdated
Comment thread apps/backend/src/jobs/jobs.service.ts Outdated
Comment thread apps/backend/src/jobs/jobs.service.ts Outdated
Comment thread apps/backend/src/jobs/jobs.service.ts Outdated
Comment thread apps/backend/src/sp-cleanup/sp-cleanup.service.ts
Comment thread apps/backend/src/sp-cleanup/sp-cleanup.service.ts Outdated
Comment thread apps/backend/src/sp-cleanup/sp-cleanup.service.ts Outdated
Comment thread apps/backend/src/sp-cleanup/sp-cleanup.service.ts Outdated
Comment thread apps/backend/src/sp-cleanup/sp-cleanup.service.ts Outdated
Pick survivors by provisioning slot instead of by age (#691 review).

`terminateExcessDataSets` kept the newest `target` data sets, which is
unrelated to keeping one set per required slot: the newest N can all
belong to a single slot, leaving the only copies of the baseline and the
other `dealbotDS` slots to be terminated and immediately re-provisioned,
looping forever. Worse, `StorageContext.resolveByProviderId` in the SDK
resolves a slot to its *lowest* data-set id (preferring one that holds
pieces), so keeping the newest inverted the SDK's own choice and
terminated exactly the set deal jobs were writing to.

Pruning now reconstructs the required slots, keeps the set the SDK would
resolve each one to, and treats the rest as surplus. `excessDatasetBuffer`
now gates the surplus count rather than the total.

Pruning runs outside `SP_WORK_QUEUE`, so it holds no per-provider lock.
Rather than route the work through that queue — the plan is computed from
a wallet-wide snapshot taken outside the lock, and re-validating under it
needs a per-provider listing the SDK does not offer — the remaining races
are closed directly: the slot rule never targets a set a deal job could
resolve to, a lifecycle-check set is spared while younger than its job's
timeout (its `dealbotLifecycleCheck` tag is the creating job's
`Date.now()`), and every set is re-read on-chain immediately before its
terminate call.

Also:

- Queue both cleanup jobs with an explicit `expireInSeconds` past their
  own timeout. pg-boss expires and fails a job after 15 minutes by
  default, short of the 1200s cleanup timeout.
- Honour pg-boss's per-job `AbortSignal`, so an expiration or a graceful
  shutdown actually stops cleanup instead of leaving it running behind a
  job the queue has already failed.
- Rename the leftover `SpBlocklistCleanup*` identifiers and log text to
  match `sp_dataset_pruning`.
- Log `synapse_init_failed` on init failure, as deal and piece-cleanup do.
- Add `tryGetSynapseClient`: `getSynapseClient` throws when a network has
  no wallet-sdk state, which made the on-demand Synapse fallback in
  `SpCleanupService` unreachable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
beck-8 added a commit that referenced this pull request Sep 8, 2026
Pick survivors by provisioning slot instead of by age (#691 review).

`terminateExcessDataSets` kept the newest `target` data sets, which is
unrelated to keeping one set per required slot: the newest N can all
belong to a single slot, leaving the only copies of the baseline and the
other `dealbotDS` slots to be terminated and immediately re-provisioned,
looping forever. Worse, `StorageContext.resolveByProviderId` in the SDK
resolves a slot to its *lowest* data-set id (preferring one that holds
pieces), so keeping the newest inverted the SDK's own choice and
terminated exactly the set deal jobs were writing to.

Pruning now reconstructs the required slots, keeps the set the SDK would
resolve each one to, and treats the rest as surplus. `excessDatasetBuffer`
now gates the surplus count rather than the total.

Pruning runs outside `SP_WORK_QUEUE`, so it holds no per-provider lock.
Rather than route the work through that queue — the plan is computed from
a wallet-wide snapshot taken outside the lock, and re-validating under it
needs a per-provider listing the SDK does not offer — the remaining races
are closed directly: the slot rule never targets a set a deal job could
resolve to, a lifecycle-check set is spared while younger than its job's
timeout (its `dealbotLifecycleCheck` tag is the creating job's
`Date.now()`), and every set is re-read on-chain immediately before its
terminate call.

Also:

- Queue both cleanup jobs with an explicit `expireInSeconds` past their
  own timeout. pg-boss expires and fails a job after 15 minutes by
  default, short of the 1200s cleanup timeout.
- Honour pg-boss's per-job `AbortSignal`, so an expiration or a graceful
  shutdown actually stops cleanup instead of leaving it running behind a
  job the queue has already failed.
- Rename the leftover `SpBlocklistCleanup*` identifiers and log text to
  match `sp_dataset_pruning`.
- Log `synapse_init_failed` on init failure, as deal and piece-cleanup do.
- Add `tryGetSynapseClient`: `getSynapseClient` throws when a network has
  no wallet-sdk state, which made the on-demand Synapse fallback in
  `SpCleanupService` unreachable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018eiTbbcRdxL5PgLtDzUTwK
beck-8 and others added 2 commits September 8, 2026 16:05
`keccak256(JSON.stringify(batch))` is not Safe's algorithm. Safe canonicalizes
recursively with sorted keys and nulls `meta.name` before hashing, so the
checksum this produced never matched and every generated batch imported into
the Transaction Builder with a "changed properties" warning.

Extract Safe's actual algorithm into a shared helper, pinned by a vector
derived from Safe's own implementation, so the SP cleanup jobs can reuse it
rather than copying the wrong one a third time.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Both jobs walk every data set the wallet holds. Measured against dealbot's
multisig on calibration (9,666 undeleted data sets), neither could finish.

`listDataSets` enriches every data set in one unbounded `Promise.all`, which
does not run slowly but fails outright — "fetch failed" at roughly the 7,582nd
concurrent request. Same bug upstream (filecoin-pin#362, PR #708, still open).
Switch to synapse-core's `getPdpDataSets`, which pages the listing and enriches
through a bounded queue: 536s on calibration, 17s on mainnet.

The sweep then issued one sequential eth_call per data set — at 0.353 s/call,
~36 minutes for calibration's 6,147 candidates against a 20-minute timeout, so
the tail was never reached. Terminated data sets stay in `clientDataSets` until
deleted, so each run re-read the same prefix forever. Batch both read phases
through Multicall3; the read floor drops from 45 to 11 minutes.

Exceeding the node's eth_call gas limit does not throw: with `allowFailure`
every item returns `status: "failure"`, indistinguishable from every call
legitimately reverting, which would make the sweep silently skip everything on
every run. `readInBatches` stays far below the measured ceiling and splits and
retries any batch whose failures carry the `SysErrOutOfGas` signature.

`getPdpDataSets` resolves each provider from the SP registry, so pruning now
takes the provider — relay service URL included — off the data set. The
storage_provider table is consulted only for `isApproved`, which has no
on-chain equivalent, and a provider missing from the registry is pruned rather
than skipped.

Both jobs move onto one `sp.cleanup` queue with pg-boss's singleton policy and
a shared per-network key. They are seeded with the same `next_run_at`, so they
previously ran concurrently, each walking the whole wallet; serialising also
means the sweep observes pruning's terminations.

Also from this review round:

- Rename the `Dataset` identifiers this PR introduced to `DataSet`. Environment
  variables keep `DATASET_`, the existing majority spelling.
- Adopt the shared Safe checksum helper for the stuck-rail batch.
- Re-throw on abort in four catch blocks that swallowed it as a per-item
  failure, which recorded a timed-out run as successful.
- Trim duplicated explanation from the previous round's comments.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
beck-8 and others added 2 commits September 8, 2026 16:25
Pruning matches a data set to a provisioning slot by exact metadata equality,
and reconstructed that metadata from `getBaseDataSetMetadata`. That is what our
callers pass *in*; the SDK adds `source` from the Synapse instance before both
storing and matching (`combineMetadata`, applied in `StorageManager.createContext`).

Read back from mainnet, all 236 of dealbot's data sets carry
`{source, withIPFSIndexing}` or those plus `dealbotDS` — so the reconstruction
matched none of them. Since an unmatched data set counts as surplus, pruning
would have terminated every data set the wallet holds.

The unit tests missed it because the fixture invented its own metadata shape
rather than the one on chain, so they validated the code against that invention.
Pin the real key set instead, and stop pruning a provider whose active sets
match no slot at all: that is not a state normal operation reaches, so it now
logs and skips rather than treating the whole holding as surplus.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…enting it

The `source` bug was not an isolated slip: pruning had hand-written copies of
three pieces of SDK logic, and any drift in any of them terminates data sets the
deal jobs are using. Two of the three did not need to exist.

`findMatchingDataSets` is exported from @filoz/synapse-core and already applies
exact metadata equality and the same ordering `resolveByProviderId` selects with
— piece-bearing sets first, then lowest id — so its first entry *is* the set
`createContext` resolves a slot to. Use it, and drop the local
`metadataMatchesExactly` and `pickSlotSurvivor`.

It also filters on `live` and `managed`, which the local copy did not, so a set
the SDK would never resolve to is no longer treated as a slot's survivor.

The unit tests now import the real matcher rather than mocking it, so a change
in what the SDK considers a match fails the suite instead of passing against our
own copy of the rule.

What remains derived by hand is the slot metadata itself, because the `source`
the SDK stamps on comes from a private field. That one is pinned against real
mainnet metadata and guarded at runtime: a provider whose sets match no slot at
all is skipped, not emptied.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@beck-8
beck-8 force-pushed the feat/automated-sp-dataset-cleanup branch from 8baf251 to 1ff4005 Compare September 8, 2026 08:36
@beck-8

beck-8 commented Sep 8, 2026

Copy link
Copy Markdown
Member Author

At this point, I’m feeling a bit overwhelmed—the number of changes has far exceeded my expectations, and I can’t guarantee that the AI will execute exactly as I envision. From what I’ve seen so far, there are still many omissions. When we’re cleaning up, would it be better to add a persistent data table to track these states? That way, we could reduce RPC calls, continue tracking even if a task fails, and handle edge cases during cleanup. I’d like to ask someone else to take over this task or replace it outright.

@silent-cipher silent-cipher left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I asked the agent to prepare a Q&A from the review discussion, and I'm pasting it here to make reasoning easier for everyone:

SP cleanup jobs: consolidated rationale

The conclusions below were checked against the deployed contract behavior and the live calibration wallet—not inferred only from this repository.

Summary

  • Keep sp_data_set_pruning and abandoned_data_set_sweep as separate global jobs.
  • Pruning handles cooperative SPs through the provider relay.
  • The sweep handles abandoned datasets and delayed rail settlement through permissionless on-chain calls.
  • Neither job belongs on SP_WORK_QUEUE.
  • The sweep addresses an existing calibration backlog, not a theoretical problem.
  • Incomplete cleanupPieces work is acceptable because stopping proofs and releasing dealbot’s lockup happen earlier.

Why two separate global jobs?

Job Purpose Authorization Why global
sp_data_set_pruning Remove blocked or excess datasets Requires a reachable, cooperative SP through the relay Dataset listing is wallet-wide. Running per SP would repeatedly fetch the same wallet data.
abandoned_data_set_sweep Delete abandoned datasets and settle terminated rails Uses permissionless on-chain paths It must include datasets whose SP has disappeared from the registry, and it never contacts the SP server.

The sweep is not merely a fallback for pruning. Most terminated datasets come from the existing lifecycle-check job, while abandoned datasets can belong to SPs that pruning would never target.

Why pruning does not use SP_WORK_QUEUE

The existing per-SP queue is already close to full worst-case occupancy: approximately 3,492.5 seconds per hour for a full-rate SP.

Adding another consumer would most affect busy or degraded SPs—the same ones most likely to need pruning. Blocked SPs already have no collision risk because they are excluded from normal SP scheduling. For non-blocked SPs, pruning follows the existing maintenance-window deferral and performs only brief relay requests.

Contract behavior and ownership of funds

The deleteDataSet path does not have the session-key problem seen in #546:

  • FWSS.terminateService requires the caller to be the payer or SP, so dealbot’s session key cannot call it directly.
  • PDPVerifier.deleteDataSet requires the SP only during the approximately 30-day activity window. After that, it is permissionless.
  • FWSS independently checks the same inactivity condition when processing dataSetDeleted.

One correction from the earlier discussion: the PDP cleanup deposit is funded by the SP, not dealbot. Whoever completes cleanup receives that deposit. It is therefore a keeper bounty, not dealbot recovering its own funds.

The payment-rail lockup is different: that is dealbot’s money. Settling the rail releases funds belonging to dealbot.

Why Curio cleanup does not make the sweep redundant

Curio cleanup helps, but dealbot should not depend on it exclusively:

  1. A dead SP cannot submit cleanup transactions.
  2. SP cleanup recovers the SP-funded deposit, not dealbot’s funds.
  3. Curio cleanup is rate-limited, so rail settlement may be delayed indefinitely from dealbot’s perspective.

A cooperative SP may eventually settle the rail itself. The reason for dealbot to do it is control over when its own lockup is released—not because settlement is otherwise impossible.

Current calibration evidence

At approximately block 4,077,068 on 2026-09-17:

  • 22 datasets were active but never terminated.
  • 15 of those were already outside the inactivity window, some stale for 101–233 days.
  • 49 terminated datasets were past their payment end epoch.
  • 35 had been cleaned up.
  • 14 remained stuck, including 10 for roughly 228 days.

That is a 29% stuck rate among eligible terminated datasets. The sweep is addressing a real backlog. This check has not yet been repeated against mainnet.

Accepted limitation

If the job times out after deleteDataSet succeeds but before cleanupPieces finishes:

  • proof obligations have already stopped;
  • dataSetDeleted has already abandoned the rails and released dealbot’s lockup;
  • only the SP-funded cleanup bounty remains unclaimed.

That outcome satisfies dealbot’s goals, so no additional recovery mechanism is planned.

@silent-cipher
silent-cipher self-requested a review September 17, 2026 16:56
@silent-cipher

Copy link
Copy Markdown
Collaborator

Found a throughput problem with both cleanup jobs: they process data sets strictly in sequence, and each termination/deletion needs an on-chain confirmation (~30s block time). With a 20-minute job window, that caps us at roughly 40 data sets cleaned per run in ideal case. On calibration, some SPs have ~500 data sets to terminate, at this rate that's weeks to clear a single SP's backlog, and other SPs would sit starved behind it in the meantime.

Holding off on merging until this is resolved.

@github-project-automation github-project-automation Bot moved this from ⌨️ In Progress to ✔️ Approved by reviewer in FOC Sep 19, 2026
@silent-cipher
silent-cipher merged commit ec55345 into main Sep 21, 2026
13 of 14 checks passed
@silent-cipher
silent-cipher deleted the feat/automated-sp-dataset-cleanup branch September 21, 2026 04:30
@github-project-automation github-project-automation Bot moved this from ✔️ Approved by reviewer to 🎉 Done in FOC Sep 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: 🎉 Done

Development

Successfully merging this pull request may close these issues.

6 participants