Appearance
Retention and Purging
How to control how long completed, failed, and cancelled runs stay in the database.
Why Terminal Rows Accumulate
By default, a terminal run stays in cyclone_runs with no time limit. It also stays in cyclone_steps. The terminal states are completed, failed, and cancelled. Over time, these rows accumulate and:
- Increase table bloat.
- Vacuum slows down.
- Slow down monitoring queries that scan
cyclone_v_runs. - Grow disk usage.
Configure retention to delete terminal rows after a defined period.
A paused run is not terminal. Retention does not delete it until it ends.
A terminal child run stays until its parent run completes or is deleted. A running, paused, or failed parent run can still need its result. A retry of the parent run reuses the child run. A cancelled parent run starts again when a retry of its own parent reopens it.
Default Retention Behavior
With no retention configuration, Cyclone keeps terminal runs forever. You must configure retention, or you must call PurgeTerminalRuns manually.
WithRetention(d): Per-Step Retention
Set a retention period on a step definition:
go
var QuickJob = cyclone.NewStep("quick_job", quickFn,
cyclone.WithRetention(24*time.Hour), // keep terminal run for 24 hours
)Or set it on one enqueue:
go
cyclone.Enqueue(ctx, client, quickJob, in,
cyclone.WithRetention(72*time.Hour),
)Cyclone stores the retention duration in cyclone_steps.retention. When a run becomes terminal, it is eligible for purge after finished_at + max(retention across steps).
WithRetentionSweep: Automatic Background Purging
Configure the client to start a background sweeper. The sweeper deletes eligible terminal runs on an interval:
go
client, _ := cyclone.NewClient(pool, mux,
cyclone.WithWorkers(8),
cyclone.WithRetentionSweep(cyclone.RetentionConfig{
DefaultAge: 30 * 24 * time.Hour,
Interval: 5 * time.Minute,
BatchSize: 1000,
}),
)client.Start includes the sweeper. On every interval the sweeper does its own bounded delete. The delete honors each step's WithRetention. A step with no retention uses DefaultAge (COALESCE(retention, DefaultAge)). The sweeper deletes a run only when every step has passed its retention period.
The sweeper does not call PurgeTerminalRuns. That function is a separate, flat age cutoff. See Manual PurgeTerminalRuns. PurgeTerminalRuns ignores per-step retention entirely.
Start with a conservative batch size (500–1000). A batch that is too large can cause a long transaction. That transaction can block vacuum.
Manual PurgeTerminalRuns
Call it directly for one-off purges or custom scheduling:
go
deleted, err := client.PurgeTerminalRuns(ctx, 7*24*time.Hour, 500)
if err != nil {
return fmt.Errorf("purge: %w", err)
}
fmt.Printf("deleted %d terminal runs\n", deleted)PurgeTerminalRuns applies one flat age cutoff. It repeats bounded deletes internally until no eligible rows remain. It then returns the total deleted count.
Batch Sizing
Each PurgeTerminalRuns call loops. Each pass issues one bounded DELETE per batch until a short batch drains the eligible set. Each batch is its own implicit transaction. The call as a whole is not atomic. Large batches hold each transaction open longer. That can:
- Delay autovacuum on the deleted pages.
- Cause lock contention on
cyclone_runsunder heavy insert load.
Recommended: 500–2000 rows per batch. Tune the size from your row size and your vacuum settings.
Impact on Outputs
Cyclone stores step outputs in cyclone_steps.output. A purge of a run deletes all of its step rows. That delete includes their outputs. After the purge, run.Result, run.Output, and cyclone.OutputNode return ErrRunNotFound.
If you need the outputs after retention, copy them to your application tables. Use ctx.CompletionTx(). Do this before the step completes, or before the retention window closes.
Signals and Schedule Fires
cyclone_signals and cyclone_schedule_fires have no retention of their own. Both live as long as their run. The ON DELETE CASCADE from cyclone_runs deletes them when retention purges the run. This includes consumed signal rows. Cyclone keeps those rows for audit until then.
Mailbox size does not grow without bound. Signal refuses an insert once the mailbox holds as many rows as the declared wait can use. The limit is 1 for WaitSignal and WaitAny. The limit is N for WaitN. A delivery that arrives after the wait has completed is not persisted. Cyclone bounds mailbox depth per run. It does not bound that depth per signal sender.
The cascade cost does grow when the child tables are large. Both tables therefore have a plain index on run_id. Without that index, retention scans those tables for each deleted run. Purge cost then tracks total mailbox volume and fire volume. It does not track batch size.
Relationship to Debugging and Auditing
Shorter retention means less history for:
- Post-mortem debugging of failed runs.
- An audit of which jobs executed, and of their outputs.
- Replays of failed workflows from old step inputs.
You can:
- Keep
failedandcancelledruns longer thancompletedruns. Those runs are more likely to need investigation. - Export job history to an analytics table before the purge. Do this when you need a permanent audit trail.
Operations Checklist
- Choose a retention period for your debugging needs and your compliance needs.
- Set
WithRetentionon steps. Or configure a default withWithRetentionSweep. - Test the purge in a staging environment before you enable it in production.
- Monitor table sizes in
pg_stat_user_tablesafter you enable the purge. - Make autovacuum reclaim deleted space often enough. See Database Maintenance.
Related
- Retention: policy design and compliance considerations
- Database Maintenance: autovacuum and bloat