Skip to content

feat: prune repeated stream values on a schedule - #1424

Merged
MicBun merged 1 commit into
feat/duplicate-prune-schedulerfrom
feat/duplicate-prune-schedule
Sep 3, 2026
Merged

feat: prune repeated stream values on a schedule#1424
MicBun merged 1 commit into
feat/duplicate-prune-schedulerfrom
feat/duplicate-prune-schedule

Conversation

@MicBun

@MicBun MicBun commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Second of two, and stacked on the first, so this diff is the scheduler alone. A node
operator can now let a network drop repeat records on its own, instead of running the
prune action by hand.

What this adds

A second cron in tn_digest that drains auto_prune_duplicates the way the digest job
drains auto_digest. It shares the extension's leader gating and its config-reload
worker, and nothing else: duplicate_prune_config rather than digest_config, its own
enabled flag, its own schedule, its own cron and its own context.

That separation is the point, and two tests pin it in both directions: a digest schedule
change and a digest disable each have to leave the sweep running. On a shared cron either
one would silently cancel a prune drain partway through, with nothing to say it had
happened.

It ships off, and the gate is the table, not a build flag.
duplicate_prune_config.enabled already ships false from migration 056, and it is what
turns pruning on. TrimTxEventsEnabled is a Go constant because tx-event trimming has no
config table; this has one, so a second gate would mean an operator sets the column
through a signed exec-sql and watches nothing happen, and turning pruning off again would
need a release.

Two things worth reviewing closely

The stop ordering. gocron.Scheduler.Stop() does not signal, it joins:
executor.stop() ends in wg.Wait(). A prune drain can sit for minutes waiting on the
drain slot and only stops waiting when its context is cancelled, so stopping the cron
before cancelling deadlocks, and holding the scheduler mutex across it blocks the job that
needs that mutex on entry. leaderwatch's lose-leadership callback would block with it.
Both stop paths now cancel first and hold no lock. The same reordering was applied to the
existing Stop, which had the same shape and the same latent bug, narrowly before this
change and widely after it.

Worth knowing how that was caught. The first regression test passed against the broken
code
: it blocked a plain goroutine on the slot, and gocron only joins jobs it owns, so
there was nothing for Stop to wait for. TestStopPrune_ReleasesASweepWaitingForTheSlot
drives a real cron job, and against the wrong order it hangs to a 90-second timeout.

The drain slot. Digest and prune both broadcast from the node's signer account and
both default to 0 */6 * * *, so on most firings they start at the same instant and would
fetch the same nonce. One token serialises them. It waits rather than skips: with
identical schedules, a firing that skipped on contention would skip every time.

Sizing

In scheduler/constants.go, with the reasoning beside the numbers: 100 streams a run, 100
runs a firing, so 10,000 streams a firing and about 19 firings to cover mainnet's ~182,000
primitive streams. The delete cap is digest's proven 100,000.

The sweep is cyclic, so has_more_to_delete means "the cursor has not finished a pass"
rather than "there is more to delete", and a firing runs its whole loop. A run that
deleted nothing therefore waits 5 s instead of 60 s: once the backlog is gone every run is
one of those, and a flat 60 s would spend 100 minutes of wall clock a firing moving a
cursor. The full delay stays for the runs that actually delete, which is what it is for.

Tests

Thirteen unit tests.

  • Lifecycle (8): ships disabled; starts with digest off; stops on lost leadership;
    enable and disable through a config reload; the two separation cases above; and a node
    whose binary is ahead of its migrations, which has to leave the sweep off rather than
    fail every reload.
  • Drain slot (5): mutual exclusion, giving up on a cancelled context, a spurious
    release, a hand-built scheduler with a nil slot, and the stop-ordering regression.
go test ./extensions/tn_digest/...              ok
golangci-lint run ./extensions/tn_digest/...    0 issues

Not fixed here

go test -race ./extensions/tn_digest/ fails, and fails on main too: Extension's
config snapshot is written by the background retry worker and read by the consensus
goroutine with nothing between them. CI does not run -race on this package, so it has
gone unseen. The two fields added here follow the same discipline as the existing two
rather than diverging from them; the fix is a lock around the whole snapshot and belongs
in its own change.

Rollout

Nothing prunes on merge. Turning it on is a signed kwil-cli exec-sql against
duplicate_prune_config, never psql, and the operator notes are in the extension README.

@MicBun MicBun self-assigned this Sep 3, 2026
@holdex

holdex Bot commented Sep 3, 2026

Copy link
Copy Markdown

Time Submission Status

Member # Time Running Total Status Last Update
MicBun 4h ✅ Submitted Sep 3, 2026, 1:16 PM

Submit or update total time with:

@holdex pr submit-time 2h

Add time on top of previous submission with:

@holdex pr add-time 1h30m

See available commands to help comply with our Guidelines.

@coderabbitai

coderabbitai Bot commented Sep 3, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Team

Run ID: a95b7d22-4eee-4372-800f-2c31d155a789

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@MicBun

MicBun commented Sep 3, 2026

Copy link
Copy Markdown
Contributor Author

@holdex pr submit-time 4h

@MicBun
MicBun merged commit f23d42d into feat/duplicate-prune-scheduler Sep 3, 2026
7 checks passed
@MicBun
MicBun deleted the feat/duplicate-prune-schedule branch September 3, 2026 13:16
MicBun added a commit that referenced this pull request Sep 3, 2026
* feat: run a duplicate prune sweep from the node

* feat: prune repeated stream values on a schedule (#1424)

* feat: refuse a one-off prune while a drain is running
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