Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
801a0bf
test: verify RemoveWorkflowInstances cleans up history and attributes
nodeselector Feb 11, 2026
5328881
sqlite: fix RemoveWorkflowInstances column names, add LIMIT
nodeselector Feb 11, 2026
16a164c
sqlite: return errors from Scan instead of silently ignoring
nodeselector Feb 11, 2026
07fe600
sqlite: add defer rows.Close() in GetStats
nodeselector Feb 11, 2026
7b55fcb
sqlite: fix error handling in events.go
nodeselector Feb 11, 2026
0d11a4e
sqlite: fix RemoveWorkflowInstances column names, add LIMIT
nodeselector Feb 11, 2026
311adc7
fix: loop expiration activity to drain backlog incrementally
nodeselector Feb 12, 2026
51166ad
sqlite: enable auto_vacuum=full to reclaim disk space
nodeselector Feb 23, 2026
ff97056
fix: throttle expiration batches to reduce lock contention
nodeselector Feb 24, 2026
e894670
remove startup VACUUM — defer to separate rollout step
nodeselector Feb 24, 2026
a6b31ff
Handle done while in the CancelPending state.
EricHorton Mar 11, 2026
71ad0a1
Merge pull request #1 from nodeselector/ns/sqlite-removal-test-and-fix
EricHorton Mar 23, 2026
a93d090
Merge pull request #9 from nodeselector/ns/sqlite-bug-fixes-scan-errors
EricHorton Mar 23, 2026
4ae7187
Merge pull request #4 from nodeselector/ns/sqlite-bug-fixes-stats-rows
EricHorton Mar 23, 2026
a96d689
Merge pull request #5 from nodeselector/ns/sqlite-bug-fixes-events
EricHorton Mar 23, 2026
9dc5f27
Merge pull request #6 from nodeselector/ns/expiration-loop
EricHorton Mar 23, 2026
910adb7
Format.
EricHorton Mar 23, 2026
91f8df5
Merge pull request #10 from EricHorton/cancel-pending
EricHorton Mar 23, 2026
0f625c0
Add an option to enable auto-vacuum.
EricHorton Mar 13, 2026
c099e23
Bump the default batch size.
EricHorton Mar 26, 2026
7893443
Merge branch 'main' into sqlite-vacuum
cschleiden May 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions backend/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ type Backend interface {
// RemoveWorkflowInstance removes a workflow instance
RemoveWorkflowInstance(ctx context.Context, instance *workflow.Instance) error

// RemoveWorkflowInstances removes multiple workflow instances
RemoveWorkflowInstances(ctx context.Context, options ...RemovalOption) error
// RemoveWorkflowInstances removes multiple workflow instances and returns the number removed
RemoveWorkflowInstances(ctx context.Context, options ...RemovalOption) (int, error)

// GetWorkflowInstanceState returns the state of the given workflow instance
GetWorkflowInstanceState(ctx context.Context, instance *workflow.Instance) (core.WorkflowInstanceState, error)
Expand Down
17 changes: 12 additions & 5 deletions backend/mock_Backend.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

23 changes: 13 additions & 10 deletions backend/mysql/mysql.go
Original file line number Diff line number Diff line change
Expand Up @@ -246,41 +246,42 @@ func (mb *mysqlBackend) removeWorkflowInstance(ctx context.Context, instance *co
return nil
}

func (mb *mysqlBackend) RemoveWorkflowInstances(ctx context.Context, options ...backend.RemovalOption) error {
func (mb *mysqlBackend) RemoveWorkflowInstances(ctx context.Context, options ...backend.RemovalOption) (int, error) {
ro := backend.DefaultRemovalOptions
for _, opt := range options {
opt(&ro)
}

rows, err := mb.db.QueryContext(ctx, `SELECT instance_id, execution_id FROM instances WHERE completed_at < ?`, ro.FinishedBefore)
if err != nil {
return err
return 0, err
}

instanceIDs := []string{}
executionIDs := []string{}
for rows.Next() {
var id, executionID string
if err := rows.Scan(&id, &executionID); err != nil {
return err
return 0, err
}

instanceIDs = append(instanceIDs, id)
executionIDs = append(executionIDs, executionID)
}

if rows.Err() != nil {
return rows.Err()
return 0, rows.Err()
}

removed := 0
batchSize := ro.BatchSize
for i := 0; i < len(instanceIDs); i += batchSize {
instanceIDs := instanceIDs[i:min(i+batchSize, len(instanceIDs))]
executionIDs := executionIDs[i:min(i+batchSize, len(executionIDs))]

tx, err := mb.db.BeginTx(ctx, nil)
if err != nil {
return err
return removed, err
}

defer tx.Rollback()
Expand All @@ -297,23 +298,25 @@ func (mb *mysqlBackend) RemoveWorkflowInstances(ctx context.Context, options ...

// Delete from instances, history and attributes tables
if _, err := tx.ExecContext(ctx, fmt.Sprintf("DELETE FROM `instances` WHERE %v", whereCondition), args...); err != nil {
return err
return removed, err
}

if _, err := tx.ExecContext(ctx, fmt.Sprintf("DELETE FROM `history` WHERE %v", whereCondition), args...); err != nil {
return err
return removed, err
}

if _, err := tx.ExecContext(ctx, fmt.Sprintf("DELETE FROM `attributes` WHERE %v", whereCondition), args...); err != nil {
return err
return removed, err
}

if err := tx.Commit(); err != nil {
return err
return removed, err
}

removed += len(instanceIDs)
}

return nil
return removed, nil
}

func (mb *mysqlBackend) CancelWorkflowInstance(ctx context.Context, instance *workflow.Instance, event *history.Event) error {
Expand Down
23 changes: 13 additions & 10 deletions backend/postgres/postgres.go
Original file line number Diff line number Diff line change
Expand Up @@ -248,15 +248,15 @@ func (pb *postgresBackend) removeWorkflowInstance(ctx context.Context, instance
return nil
}

func (pb *postgresBackend) RemoveWorkflowInstances(ctx context.Context, options ...backend.RemovalOption) error {
func (pb *postgresBackend) RemoveWorkflowInstances(ctx context.Context, options ...backend.RemovalOption) (int, error) {
ro := backend.DefaultRemovalOptions
for _, opt := range options {
opt(&ro)
}

rows, err := pb.db.QueryContext(ctx, "SELECT instance_id, execution_id FROM instances WHERE completed_at < $1", ro.FinishedBefore)
if err != nil {
return err
return 0, err
}
defer rows.Close()

Expand All @@ -267,7 +267,7 @@ func (pb *postgresBackend) RemoveWorkflowInstances(ctx context.Context, options
for rows.Next() {
var id, executionID string
if err := rows.Scan(&id, &executionID); err != nil {
return err
return 0, err
}
pairs = append(pairs, struct {
instanceID string
Expand All @@ -279,7 +279,7 @@ func (pb *postgresBackend) RemoveWorkflowInstances(ctx context.Context, options
}

if rows.Err() != nil {
return rows.Err()
return 0, rows.Err()
}

pgPairedPlaceholders := func(startIdx, pairCount int) string {
Expand All @@ -290,11 +290,12 @@ func (pb *postgresBackend) RemoveWorkflowInstances(ctx context.Context, options
return strings.Join(placeholders, ", ")
}

removed := 0
batchSize := ro.BatchSize
for i := 0; i < len(pairs); i += batchSize {
tx, err := pb.db.BeginTx(ctx, nil)
if err != nil {
return err
return removed, err
}

batch := pairs[i:min(i+batchSize, len(pairs))]
Expand All @@ -311,25 +312,27 @@ func (pb *postgresBackend) RemoveWorkflowInstances(ctx context.Context, options
// Delete from instances, history and attributes tables
if _, err := tx.ExecContext(ctx, fmt.Sprintf("DELETE FROM instances WHERE %v", whereCondition), args...); err != nil {
_ = tx.Rollback()
return err
return removed, err
}

if _, err := tx.ExecContext(ctx, fmt.Sprintf("DELETE FROM history WHERE %v", whereCondition), args...); err != nil {
_ = tx.Rollback()
return err
return removed, err
}

if _, err := tx.ExecContext(ctx, fmt.Sprintf("DELETE FROM attributes WHERE %v", whereCondition), args...); err != nil {
_ = tx.Rollback()
return err
return removed, err
}

if err := tx.Commit(); err != nil {
return err
return removed, err
}

removed += len(batch)
}

return nil
return removed, nil
}

func (pb *postgresBackend) CancelWorkflowInstance(ctx context.Context, instance *workflow.Instance, event *history.Event) error {
Expand Down
4 changes: 2 additions & 2 deletions backend/redis/instance.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,8 +167,8 @@ func (rb *redisBackend) RemoveWorkflowInstance(ctx context.Context, instance *co
return rb.deleteInstance(ctx, instance)
}

func (rb *redisBackend) RemoveWorkflowInstances(ctx context.Context, options ...backend.RemovalOption) error {
return backend.ErrNotSupported{
func (rb *redisBackend) RemoveWorkflowInstances(ctx context.Context, options ...backend.RemovalOption) (int, error) {
return 0, backend.ErrNotSupported{
Message: "not supported, use auto-expiration",
}
}
Expand Down
2 changes: 1 addition & 1 deletion backend/removal.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ type RemovalOptions struct {
}

var DefaultRemovalOptions = RemovalOptions{
BatchSize: 100,
BatchSize: 500,
}

type RemovalOption func(o *RemovalOptions)
Expand Down
10 changes: 5 additions & 5 deletions backend/sqlite/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,6 @@ func (sb *sqliteBackend) GetFutureEvents(ctx context.Context) ([]*history.Event,
if err != nil {
return nil, fmt.Errorf("getting history: %w", err)
}
if futureEvents.Err() != nil {
return nil, futureEvents.Err()
}

defer futureEvents.Close()

f := make([]*history.Event, 0)
Expand Down Expand Up @@ -69,6 +65,10 @@ func (sb *sqliteBackend) GetFutureEvents(ctx context.Context) ([]*history.Event,
f = append(f, fe)
}

if futureEvents.Err() != nil {
return nil, futureEvents.Err()
}

return f, nil
}

Expand Down Expand Up @@ -269,5 +269,5 @@ func removeFutureEvent(ctx context.Context, tx *sql.Tx, instance *core.WorkflowI
}
}

return err
return nil
}
17 changes: 17 additions & 0 deletions backend/sqlite/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,16 @@ type options struct {

// ApplyMigrations automatically applies database migrations on startup.
ApplyMigrations bool

// AutoVacuum runs the `PRAGMA auto_vacuum=full` when creating the connection to enable the sqlite auto-vacuum feature.
//
// The `VACUUM` statement is always run after enabling auto-vacuum to ensure auto-vacuum is correctly enabled and to
// reorganize the database file and reclaim disk space.
//
// See
// - https://sqlite.org/pragma.html#pragma_auto_vacuum
// - https://sqlite.org/lang_vacuum.html.
AutoVacuum bool
}

type option func(*options)
Expand All @@ -28,3 +38,10 @@ func WithBackendOptions(opts ...backend.BackendOption) option {
}
}
}

// WithAutoVacuum sets sqlite auto-vacuum to full. See options.AutoVacuum for details.
func WithAutoVacuum() option {
return func(o *options) {
o.AutoVacuum = true
}
}
Loading
Loading