Skip to content

Commit 5980899

Browse files
authored
Always upsert initial periodic job records (#998)
Tweak the behavior for periodic job records inserted through the pilot so that instead of being upserted the first time they're worked, upsert them immediately on program start up, even if `RunOnStart` isn't configured for them. This won't actually cause them to run, but will add the durable record a little earlier than it would be before.
1 parent f63be3d commit 5980899

3 files changed

Lines changed: 67 additions & 13 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3131
- Cleaner retention periods (`CancelledJobRetentionPeriod`, `CompletedJobRetentionPeriod`, `DiscardedJobRetentionPeriod`) can be configured to -1 to disable them so that the corresponding type of job is retained indefinitely. [PR #990](https://github.com/riverqueue/river/pull/990).
3232
- Jobs inserted from periodic jobs with IDs now have metadata `river:periodic_job_id` set so they can be traced back to the periodic job that inserted them. [PR #992](https://github.com/riverqueue/river/pull/992).
3333
- The unused function `WorkerDefaults.Hooks` has been removed. This is technically a breaking change, but this function was a vestigal refactoring artifact that was never used by anything, so in practice it shouldn't be breaking. [PR #997](https://github.com/riverqueue/river/pull/997).
34+
- Periodic job records are upserted immediately through a pilot when a client is started rather than the first time their associated job would run. This doesn't mean they're run immediately (they'll only run if `RunOnStart` is enabled), but rather just tracked immediately. [PR #998](https://github.com/riverqueue/river/pull/998).
3435

3536
### Fixed
3637

internal/maintenance/periodic_job_enqueuer.go

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,15 @@ type PeriodicJobEnqueuerTestSignals struct {
3232
EnteredLoop testsignal.TestSignal[struct{}] // notifies when the enqueuer finishes start up and enters its initial run loop
3333
InsertedJobs testsignal.TestSignal[struct{}] // notifies when a batch of jobs is inserted
3434
PeriodicJobKeepAliveAndReap testsignal.TestSignal[struct{}] // notifies when the background services that runs keep alive and reap on periodic jobs ticks
35+
PeriodicJobUpserted testsignal.TestSignal[struct{}] // notifies when a batch of periodic job records are upserted to pilot
3536
SkippedJob testsignal.TestSignal[struct{}] // notifies when a job is skipped because of nil JobInsertParams
3637
}
3738

3839
func (ts *PeriodicJobEnqueuerTestSignals) Init(tb testutil.TestingTB) {
3940
ts.EnteredLoop.Init(tb)
4041
ts.InsertedJobs.Init(tb)
4142
ts.PeriodicJobKeepAliveAndReap.Init(tb)
43+
ts.PeriodicJobUpserted.Init(tb)
4244
ts.SkippedJob.Init(tb)
4345
}
4446

@@ -332,20 +334,20 @@ func (s *PeriodicJobEnqueuer) Start(ctx context.Context) error {
332334
periodicJob.nextRunAt = periodicJob.ScheduleFunc(now)
333335
}
334336

337+
if periodicJob.ID != "" {
338+
periodicJobUpsertParams.Jobs = append(periodicJobUpsertParams.Jobs, &riverpilot.PeriodicJobUpsertParams{
339+
ID: periodicJob.ID,
340+
NextRunAt: periodicJob.nextRunAt,
341+
})
342+
}
343+
335344
if !periodicJob.RunOnStart {
336345
continue
337346
}
338347

339348
if insertParams, ok := s.insertParamsFromConstructor(ctx, periodicJob.ID, periodicJob.ConstructorFunc, now); ok {
340349
insertParamsMany = append(insertParamsMany, insertParams)
341350
}
342-
343-
if periodicJob.ID != "" {
344-
periodicJobUpsertParams.Jobs = append(periodicJobUpsertParams.Jobs, &riverpilot.PeriodicJobUpsertParams{
345-
ID: periodicJob.ID,
346-
NextRunAt: periodicJob.nextRunAt,
347-
})
348-
}
349351
}
350352

351353
s.insertBatch(ctx, insertParamsMany, periodicJobUpsertParams)
@@ -436,7 +438,7 @@ func (s *PeriodicJobEnqueuer) Start(ctx context.Context) error {
436438
}
437439

438440
func (s *PeriodicJobEnqueuer) insertBatch(ctx context.Context, insertParamsMany []*rivertype.JobInsertParams, periodicJobUpsertParams *riverpilot.PeriodicJobUpsertManyParams) {
439-
if len(insertParamsMany) < 1 {
441+
if len(insertParamsMany) < 1 && len(periodicJobUpsertParams.Jobs) < 1 {
440442
return
441443
}
442444

@@ -467,7 +469,12 @@ func (s *PeriodicJobEnqueuer) insertBatch(ctx context.Context, insertParamsMany
467469
return
468470
}
469471

470-
s.TestSignals.InsertedJobs.Signal(struct{}{})
472+
if len(insertParamsMany) > 0 {
473+
s.TestSignals.InsertedJobs.Signal(struct{}{})
474+
}
475+
if len(periodicJobUpsertParams.Jobs) > 0 {
476+
s.TestSignals.PeriodicJobUpserted.Signal(struct{}{})
477+
}
471478
}
472479

473480
func (s *PeriodicJobEnqueuer) insertParamsFromConstructor(ctx context.Context, periodicJobID string, constructorFunc func() (*rivertype.JobInsertParams, error), scheduledAt time.Time) (*rivertype.JobInsertParams, bool) {

internal/maintenance/periodic_job_enqueuer_test.go

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -759,10 +759,9 @@ func TestPeriodicJobEnqueuer(t *testing.T) {
759759
return nil, nil
760760
}
761761

762-
var periodicJobUpsertManyMockCalled bool
762+
var insertedPeriodicJobIDs [][]string
763763
bundle.pilotMock.PeriodicJobUpsertManyMock = func(ctx context.Context, exec riverdriver.Executor, params *riverpilot.PeriodicJobUpsertManyParams) ([]*riverpilot.PeriodicJob, error) {
764-
periodicJobUpsertManyMockCalled = true
765-
require.Equal(t, []string{"periodic_job_100ms"}, sliceutil.Map(params.Jobs, func(j *riverpilot.PeriodicJobUpsertParams) string { return j.ID }))
764+
insertedPeriodicJobIDs = append(insertedPeriodicJobIDs, sliceutil.Map(params.Jobs, func(j *riverpilot.PeriodicJobUpsertParams) string { return j.ID }))
766765
require.Equal(t, bundle.schema, params.Schema)
767766
return nil, nil
768767
}
@@ -777,13 +776,23 @@ func TestPeriodicJobEnqueuer(t *testing.T) {
777776
startService(t, svc)
778777

779778
svc.TestSignals.InsertedJobs.WaitOrTimeout()
779+
780+
// periodic_job_100ms runs immediately because it didn't have a
781+
// persisted record from PeriodicJobGetAllMock
780782
insertedPeriodicJobs := requireNJobs(t, bundle, "periodic_job_100ms", 1)
781783
requireNJobs(t, bundle, "periodic_job_500ms", 0)
782784
requireNJobs(t, bundle, "periodic_job_1500ms", 0)
783-
require.True(t, periodicJobUpsertManyMockCalled)
784785

785786
require.Equal(t, "periodic_job_100ms", gjson.GetBytes(insertedPeriodicJobs[0].Metadata, rivercommon.MetadataKeyPeriodicJobID).Str)
786787

788+
// During the first invocation periodic job records for all three jobs
789+
// are inserted (this happens on start up), then after one run we expect
790+
// only an insertion for the job that actually ran.
791+
require.Equal(t, [][]string{
792+
{"periodic_job_100ms", "periodic_job_500ms", "periodic_job_1500ms"},
793+
{"periodic_job_100ms"},
794+
}, insertedPeriodicJobIDs)
795+
787796
svc.TestSignals.PeriodicJobKeepAliveAndReap.WaitOrTimeout()
788797
require.True(t, periodicJobKeepAliveAndReapMockCalled)
789798

@@ -831,6 +840,43 @@ func TestPeriodicJobEnqueuer(t *testing.T) {
831840
require.False(t, periodicJobKeepAliveAndReapMockCalled)
832841
})
833842

843+
t.Run("PeriodicJobsWithIDAlwaysUpserted", func(t *testing.T) {
844+
t.Parallel()
845+
846+
svc, bundle := setup(t)
847+
848+
bundle.pilotMock.PeriodicJobGetAllMock = func(ctx context.Context, exec riverdriver.Executor, params *riverpilot.PeriodicJobGetAllParams) ([]*riverpilot.PeriodicJob, error) {
849+
return []*riverpilot.PeriodicJob{}, nil
850+
}
851+
852+
bundle.pilotMock.PeriodicJobKeepAliveAndReapMock = func(ctx context.Context, exec riverdriver.Executor, params *riverpilot.PeriodicJobKeepAliveAndReapParams) ([]*riverpilot.PeriodicJob, error) {
853+
return nil, nil
854+
}
855+
856+
var insertedPeriodicJobIDs [][]string
857+
bundle.pilotMock.PeriodicJobUpsertManyMock = func(ctx context.Context, exec riverdriver.Executor, params *riverpilot.PeriodicJobUpsertManyParams) ([]*riverpilot.PeriodicJob, error) {
858+
insertedPeriodicJobIDs = append(insertedPeriodicJobIDs, sliceutil.Map(params.Jobs, func(j *riverpilot.PeriodicJobUpsertParams) string { return j.ID }))
859+
return nil, nil
860+
}
861+
862+
_, err := svc.AddManySafely([]*PeriodicJob{
863+
{ID: "periodic_job_10m", ScheduleFunc: periodicIntervalSchedule(10 * time.Minute), ConstructorFunc: jobConstructorFunc("periodic_job_10m", false)},
864+
{ID: "periodic_job_20m", ScheduleFunc: periodicIntervalSchedule(20 * time.Minute), ConstructorFunc: jobConstructorFunc("periodic_job_20m", false)},
865+
866+
// this one doesn't have an ID and won't get an initial insert
867+
{ScheduleFunc: periodicIntervalSchedule(30 * time.Minute), ConstructorFunc: jobConstructorFunc("periodic_job_30m", false)},
868+
})
869+
require.NoError(t, err)
870+
871+
startService(t, svc)
872+
873+
svc.TestSignals.PeriodicJobUpserted.WaitOrTimeout()
874+
875+
require.Equal(t, [][]string{
876+
{"periodic_job_10m", "periodic_job_20m"},
877+
}, insertedPeriodicJobIDs)
878+
})
879+
834880
t.Run("DuplicateIDError", func(t *testing.T) {
835881
t.Parallel()
836882

0 commit comments

Comments
 (0)