Skip to content

Fix the nightly: purge the Recycle Bin, correct sweeper delete order, stop lanes wiping .auth - #1

Open
darshangm92 wants to merge 2 commits into
mainfrom
feature-sweeper-recycle-bin-purge
Open

Fix the nightly: purge the Recycle Bin, correct sweeper delete order, stop lanes wiping .auth#1
darshangm92 wants to merge 2 commits into
mainfrom
feature-sweeper-recycle-bin-purge

Conversation

@darshangm92

Copy link
Copy Markdown
Owner

The nightly has been failing intermittently since 2026-08-21 (runs on 08-21, 08-22, 08-23, 08-27, 08-28 and 08-29). Three separate faults — one cause, two feeding it.

1. The org ran out of data storage — this is the actual cause

Measured 2026-08-29: 5 MB max, 0 MB remaining, against ~1,500 live records. Every write failed STORAGE_LIMIT_EXCEEDED (35 occurrences in the 08-29 run alone).

Nothing in the reports said "storage". Journey stage 6 died on locator.waitFor: Timeout 30000ms exceeded waiting for an Edit modal to close; contracted-pricing died on a 60s locator timeout. A modal that will not close is what a rejected save looks like — so the symptom pointed at the UI.

A REST DELETE is a soft delete. The record sits in the Recycle Bin for 15 days and keeps consuming storage the whole time, and nothing here ever emptied it. It isn't mainly the sweeper's own output either: per-test CpqDataFactory.cleanup() fills the bin on every green run, so the exhaust of every passing nightly was billed against a 5 MB org for a fortnight.

The sweeper now purges the bin after its deletes, via SOAP emptyRecycleBin (REST has no equivalent; Bulk API hard delete needs a profile permission this integration user lacks). Allowlist-only, same --retention-days cutoff, --confirm required, --no-purge to opt out.

Running it freed the org: 1629 records purged, and a create that had been rejected succeeded immediately after.

Two things worth knowing, both learned the hard way and both now in comments:

  • queryAll ... WHERE IsDeleted = true answers the 15-day delete-tracking window, not bin membership. It returned 4758; only 1629 actually held a bin entry. The first run therefore reported 2878 "failures" that were all no recycle bin entry found — records already costing nothing. Now classified as benign and counted separately, and the purge walks parents first so a cascade covers the children.
  • DataStorageMB in /limits is recalculated periodically, not live. It still read Remaining: 0 right after the purge that fixed the org. Confirm with a create instead.

2. Two delete-order faults — the sweep's 8 recurring failures every night

  • SBQQ__Subscription__c was deleted before the quote lines that reference it. An amendment/renewal quote line points back at the subscription it revises: could not be completed because it is associated with the following quote lines.: QL-0005393. Moved after SBQQ__Quote__c.
  • A contracted Order cannot be reverted to Draft while its Contract exists, so the single pre-delete unblock() could never clear it — Status -> Draft refused, then the delete refused with "Cannot delete an order with active products". Failed deletes now get a second pass that re-unblocks after the plan has drained, by which point DELETE_ORDER has already removed the Contracts. A retry rather than a re-ordering, because the dependency resolves as the plan drains.

3. Lanes were wiping each other's session

global-teardown.js wiped .auth/ unconditionally under CI — but every lane run-parallel.js spawns runs globalTeardown, and .auth/ is shared. The first lane to finish took the session from the lanes still running: Cannot find module '../../.auth/sf-session.json', ENOENT: .auth/session.json, in lanes with nothing wrong with them (7 occurrences on 08-29, across pricing-methods, solar and quote).

This is the same hazard Section 3.18 documents for the ledger merge, fixed the same proven way: the child defers on CPQ_REUSE_SESSION, the parent wipes once after the last lane exits. The flag was already in the child's environment and the ledger branch beside it already read it — the wipe simply predated lanes.

Verification (2026-08-29, Developer org)

  • Sweeper dry run and --confirm run both clean: 0 delete failures, 0 real purge failures.
  • Smoke suite 2 passed — the first successful org writes since the limit was hit.
  • npm run lint:tags green; lane coverage guard 17/17.
  • No spec, tag, fixture, flow or data value changed; no tier-3 measurement comment removed.

The nightly suite had been failing intermittently since 2026-08-21. Three
separate faults, one of which was the cause and two of which were feeding it.

STORAGE. The Developer org was at 5 MB of data storage with 0 remaining, so
every write failed STORAGE_LIMIT_EXCEEDED. That surfaces in the specs as a
Lightning modal that will not close or a poll that never satisfies, never as
anything naming storage, which is why the nightly read as unrelated flakiness
across the journeys, pricing-methods, smartwatch, solar and quote lanes.

Salesforce's REST DELETE is a SOFT delete: the record sits in the Recycle Bin
for 15 days and keeps consuming storage the whole time. Nothing in this
framework ever emptied that bin, and per-test CpqDataFactory.cleanup() fills
it on every green run, so the exhaust of every successful run was billed
against storage for a fortnight. The sweeper now purges the bin after its
deletes, via SOAP emptyRecycleBin (REST has no equivalent). Allowlist-only,
same --retention-days cutoff, --confirm required, --no-purge to opt out.

Running it freed the org: 1629 records purged, and a create that had been
rejected succeeded immediately after.

DELETE ORDER. Two ordering faults left residue behind every night, both
visible as the sweep's 8 recurring failures. SBQQ__Subscription__c was
deleted before the quote lines that reference it ("could not be completed
because it is associated with the following quote lines"), so it moves after
the quotes. And a contracted Order cannot be reverted to Draft while its
Contract exists, so the pre-delete unblock could never clear it; failed
deletes now get a second pass that re-unblocks after the plan has drained,
by which point the Contracts are gone.

.AUTH RACE. global-teardown.js wiped .auth/ unconditionally under CI, but
every lane run-parallel.js spawns runs it, so the first lane to finish took
the session away from the lanes still running -- "Cannot find module
'../../.auth/sf-session.json'" in lanes that had nothing wrong with them.
Children now defer, exactly as they already do for the ledger merge, and the
parent wipes once after the last lane exits.

Verified: sweeper dry run and confirmed run clean, smoke suite green against
the Developer org (2 passed), lint:tags and the lane coverage guard pass.
Follow-up to fd1b591, which was necessary and not sufficient. That commit
freed 1629 records and the very next full-suite run hit
STORAGE_LIMIT_EXCEEDED again, 26 times. Two things were wrong.

THE ACTUAL STORAGE HOG IS THE PACKAGE, NOT THE SUITE. Measured via
/limits/recordCount: SBQQ__RecordJob__c held 1585 rows dating back to
2025-01-14 -- roughly 3.1 MB of a 5 MB org, against 781 live records across
all ten allowlisted objects combined. It is CPQ's async-job receipt: one row
per queued calculation or contracting job, written by the package and never
removed. Nothing reads them back.

It gets its own pass rather than an eleventh entry in DELETE_ORDER. Every
allowlisted object is verified by marker or ancestry before deletion because
the suite created it; this one fails all of those tests -- the package creates
it, so there is no Description to stamp, no ledger row, and no lookup back to
a marked record. Folding it in would mean weakening verifyCandidates for one
object and quietly weakening it for the other ten. Separate list, separate age
gate, separate reporting, --no-housekeeping to skip.

THE PURGE NO LONGER INHERITS --retention-days. Retention protects LIVE records
from being reclaimed early; a record already in the bin has passed that gate,
so re-applying the same age test downstream of a decision already made freed
nothing and excluded precisely the recent bulk filling the bin. Purge age is
its own axis now, defaulting to 0, with --purge-retention-days=N for an org
that wants an undo window.

Result: 1528 records deleted and purged, 99 left inside retention, and
DataStorageMB went from Max 5 / Remaining 0 to Max 5 / Remaining 3.

Also here:
- removeMany() via sObject Collections, 200 per call. The housekeeping counts
  are in the thousands and one DELETE per record took minutes and spent a
  percentage point of the daily API allowance. 1528 deletes become 8 calls.
  The suite's own records still delete one at a time -- each needs its own
  outcome against a plan a human reads, and several need a per-record unblock.
- queryPaged(). The housekeeping scan first used queryAll(), which INCLUDES
  soft-deleted rows, so the dry run straight after the first sweep offered the
  same 1528 records it had just deleted. Caught before it could turn every
  nightly sweep into 1528 pointless deletes.
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