Added database.change_batch_retention, deleting old change batches

Each save that records a batch also deletes, in the same
transaction, the batches older than database.change_batch_retention
(30 days by default; 0 or less keeps them for ever), with their
records of opportunities. A client still holding a deleted batch
gets 410 Gone from ?changed_after= and reloads the whole list, as
for any unknown batch.
This commit is contained in:
bwilliams committed 2026-10-16 16:10:32 +01:00
1 parent 1988d07a29
commit 65e5bc6b26
10 files changed
+153 -19

No files matched your search

+19 -6
View File
@@ -51,6 +51,9 @@ const DefaultListLimit = 50
// concurrent use.
type Store struct {
db *sqlx.DB
// changeBatchRetention is how long change batches are kept; 0 or less
// means for ever (see WithChangeBatchRetention).
changeBatchRetention time.Duration
}
// SaveMultipleResult reports what SaveOpportunities changed.
@@ -243,7 +246,8 @@ func Open(ctx context.Context, path string, opts ...Option) (*Store, error) {
}
return &Store{
db: sqlx.NewDb(sqldb, "sqlite3"),
db: sqlx.NewDb(sqldb, "sqlite3"),
changeBatchRetention: settings.changeBatchRetention,
}, nil
}
@@ -307,7 +311,7 @@ func (s *Store) SaveOpportunity(
return SaveResult{}, err
}
_, err = recordChangeBatch(
_, err = s.recordChangeBatch(
ctx,
tx,
changedUUIDs([]*types.Opportunity{opportunity}, []SaveResult{result}),
@@ -387,7 +391,7 @@ func (s *Store) SaveOpportunities(
total.AddedClassifications = added
total.RemovedClassificationIDs = removed
total.ChangeBatch, err = recordChangeBatch(
total.ChangeBatch, err = s.recordChangeBatch(
ctx, tx, changedUUIDs(opportunities, total.OpportunityResults),
)
if err != nil {
@@ -465,14 +469,15 @@ func changedUUIDs(
// recordChangeBatch records a change batch of the opportunities uuids inside
// tx and returns its hash, or "" without recording anything if uuids is
// empty.
// empty. Batches older than the store's retention are deleted (see
// WithChangeBatchRetention).
//
// The batch's created_at is the current time, or just after the newest
// batch's if that isn't later (the clock went back, or two batches came
// within its resolution), so batches are ordered by created_at. tx holds the
// write lock from its start (see dsn), so no other batch can be recorded in
// between.
func recordChangeBatch(
func (s *Store) recordChangeBatch(
ctx context.Context, tx *sqlx.Tx, uuids []string,
) (string, error) {
if len(uuids) == 0 {
@@ -506,6 +511,14 @@ func recordChangeBatch(
}
}
if s.changeBatchRetention > 0 {
cutoff := createdAt.Add(-s.changeBatchRetention)
_, err = tx.ExecContext(ctx, deleteChangeBatchesBefore, cutoff)
if err != nil {
return "", fmt.Errorf("deleting old change batches: %w", err)
}
}
return hash, nil
}
@@ -681,7 +694,7 @@ func (s *Store) SetMarks(
return err
}
_, err = recordChangeBatch(ctx, tx, []string{marks.UUID})
_, err = s.recordChangeBatch(ctx, tx, []string{marks.UUID})
if err != nil {
return err
}