Skip to content

Commit 1dd8817

Browse files
committed
Inject worker metadata to pilot + testable job args
Here, add a mechanism that has the client inject worker metadata into pilots. This gives pilots information about what jobs/workers are registered, and helps in cases where say we want pilots to be able to examine things like `Hooks()` implementations on each worker. I also ended up adding a function for `river.AddWorkerArgs` that lets a concrete args object be injected into the registration process rather than one that's implicitly allocated. I'm a little partial on this one because it'd be better if it wasn't necessary, but it cleans up tests somewhat that are trying to test specific things on args like `Hooks()`, and make tests on features like that _possible_ when there's no workaround available like storing information that'll be persisted to a job's metadata (there's otherwise no good way to carry them out, with the only way I can think of a global variable)
1 parent d7265f4 commit 1dd8817

7 files changed

Lines changed: 115 additions & 101 deletions

File tree

client.go

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -776,10 +776,29 @@ func NewClient[TTx any](driver riverdriver.Driver[TTx], config *Config) (*Client
776776
pluginDriver.PluginInit(archetype)
777777
client.pilot = pluginDriver.PluginPilot()
778778
}
779+
780+
var workerMetadata []*rivertype.WorkerMetadata
781+
if config.Workers != nil {
782+
workerMetadata = make([]*rivertype.WorkerMetadata, 0, len(config.Workers.workersMap))
783+
for kind, workerInfo := range config.Workers.workersMap {
784+
var hooks []rivertype.Hook
785+
if jobArgsWithHooks, ok := workerInfo.jobArgs.(JobArgsWithHooks); ok {
786+
hooks = jobArgsWithHooks.Hooks()
787+
}
788+
789+
workerMetadata = append(workerMetadata, &rivertype.WorkerMetadata{
790+
JobArgHooks: hooks,
791+
Kind: kind,
792+
})
793+
}
794+
}
795+
779796
if client.pilot == nil {
780797
client.pilot = &riverpilot.StandardPilot{}
781798
}
782-
client.pilot.PilotInit(archetype)
799+
client.pilot.PilotInit(archetype, &riverpilot.PilotInitParams{
800+
WorkerMetadata: workerMetadata,
801+
})
783802
pluginPilot, _ := client.pilot.(pilotPlugin)
784803

785804
if withBaseService, ok := config.RetryPolicy.(baseservice.WithBaseService); ok {

client_test.go

Lines changed: 51 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ import (
2424
"github.com/tidwall/sjson"
2525

2626
"github.com/riverqueue/river/internal/dbunique"
27-
"github.com/riverqueue/river/internal/jobexecutor"
2827
"github.com/riverqueue/river/internal/maintenance"
2928
"github.com/riverqueue/river/internal/middlewarelookup"
3029
"github.com/riverqueue/river/internal/notifier"
@@ -840,38 +839,47 @@ func Test_Client_Common(t *testing.T) {
840839

841840
_, bundle := setup(t)
842841

843-
type JobArgs struct {
844-
testutil.JobArgsReflectKind[JobArgs]
845-
hookEmbed[metadataHookInsertBegin]
846-
}
842+
var hookInsertBeginCalled atomic.Bool
847843

848-
AddWorker(bundle.config.Workers, WorkFunc(func(ctx context.Context, job *Job[JobArgs]) error {
844+
jobArgs := JobArgsWithHooksFunc(func() []rivertype.Hook {
845+
return []rivertype.Hook{
846+
HookInsertBeginFunc(func(ctx context.Context, params *rivertype.JobInsertParams) error {
847+
hookInsertBeginCalled.Store(true)
848+
return nil
849+
}),
850+
}
851+
})
852+
853+
AddWorkerArgs(bundle.config.Workers, jobArgs, WorkFunc(func(ctx context.Context, job *Job[JobArgsWithHooksFunc]) error {
849854
return nil
850855
}))
851856

852857
client, err := NewClient(riverpgxv5.New(bundle.dbPool), bundle.config)
853858
require.NoError(t, err)
854859

855-
insertRes, err := client.Insert(ctx, JobArgs{}, nil)
860+
_, err = client.Insert(ctx, jobArgs, nil)
856861
require.NoError(t, err)
857862

858-
var metadataMap map[string]any
859-
err = json.Unmarshal(insertRes.Job.Metadata, &metadataMap)
860-
require.NoError(t, err)
861-
require.Equal(t, metadataHookCalled, metadataMap[metadataHookInsertBeginKey])
863+
require.True(t, hookInsertBeginCalled.Load())
862864
})
863865

864866
t.Run("WithWorkBeginHookOnJobArgs", func(t *testing.T) { //nolint:dupl
865867
t.Parallel()
866868

867869
_, bundle := setup(t)
868870

869-
type JobArgs struct {
870-
testutil.JobArgsReflectKind[JobArgs]
871-
hookEmbed[metadataHookWorkBegin]
872-
}
871+
var hookWorkBeginCalled atomic.Bool
873872

874-
AddWorker(bundle.config.Workers, WorkFunc(func(ctx context.Context, job *Job[JobArgs]) error {
873+
jobArgs := JobArgsWithHooksFunc(func() []rivertype.Hook {
874+
return []rivertype.Hook{
875+
HookWorkBeginFunc(func(ctx context.Context, job *rivertype.JobRow) error {
876+
hookWorkBeginCalled.Store(true)
877+
return nil
878+
}),
879+
}
880+
})
881+
882+
AddWorkerArgs(bundle.config.Workers, jobArgs, WorkFunc(func(ctx context.Context, job *Job[JobArgsWithHooksFunc]) error {
875883
return nil
876884
}))
877885

@@ -881,30 +889,33 @@ func Test_Client_Common(t *testing.T) {
881889
subscribeChan := subscribe(t, client)
882890
startClient(ctx, t, client)
883891

884-
insertRes, err := client.Insert(ctx, JobArgs{}, nil)
892+
insertRes, err := client.Insert(ctx, jobArgs, nil)
885893
require.NoError(t, err)
886894

887895
event := riversharedtest.WaitOrTimeout(t, subscribeChan)
888896
require.Equal(t, EventKindJobCompleted, event.Kind)
889897
require.Equal(t, insertRes.Job.ID, event.Job.ID)
890898

891-
var metadataMap map[string]any
892-
err = json.Unmarshal(event.Job.Metadata, &metadataMap)
893-
require.NoError(t, err)
894-
require.Equal(t, metadataHookCalled, metadataMap[metadataHookWorkBeginKey])
899+
require.True(t, hookWorkBeginCalled.Load())
895900
})
896901

897902
t.Run("WithWorkEndHookOnJobArgs", func(t *testing.T) { //nolint:dupl
898903
t.Parallel()
899904

900905
_, bundle := setup(t)
901906

902-
type JobArgs struct {
903-
testutil.JobArgsReflectKind[JobArgs]
904-
hookEmbed[metadataHookWorkEnd]
905-
}
907+
var hookWorkEndCalled atomic.Bool
906908

907-
AddWorker(bundle.config.Workers, WorkFunc(func(ctx context.Context, job *Job[JobArgs]) error {
909+
jobArgs := JobArgsWithHooksFunc(func() []rivertype.Hook {
910+
return []rivertype.Hook{
911+
HookWorkEndFunc(func(ctx context.Context, job *rivertype.JobRow, err error) error {
912+
hookWorkEndCalled.Store(true)
913+
return nil
914+
}),
915+
}
916+
})
917+
918+
AddWorkerArgs(bundle.config.Workers, jobArgs, WorkFunc(func(ctx context.Context, job *Job[JobArgsWithHooksFunc]) error {
908919
return nil
909920
}))
910921

@@ -914,17 +925,14 @@ func Test_Client_Common(t *testing.T) {
914925
subscribeChan := subscribe(t, client)
915926
startClient(ctx, t, client)
916927

917-
insertRes, err := client.Insert(ctx, JobArgs{}, nil)
928+
insertRes, err := client.Insert(ctx, jobArgs, nil)
918929
require.NoError(t, err)
919930

920931
event := riversharedtest.WaitOrTimeout(t, subscribeChan)
921932
require.Equal(t, EventKindJobCompleted, event.Kind)
922933
require.Equal(t, insertRes.Job.ID, event.Job.ID)
923934

924-
var metadataMap map[string]any
925-
err = json.Unmarshal(event.Job.Metadata, &metadataMap)
926-
require.NoError(t, err)
927-
require.Equal(t, metadataHookCalled, metadataMap[metadataHookWorkEndKey])
935+
require.True(t, hookWorkEndCalled.Load())
928936
})
929937

930938
t.Run("WithGlobalWorkerMiddleware", func(t *testing.T) {
@@ -1384,73 +1392,6 @@ func Test_Client_Common(t *testing.T) {
13841392
})
13851393
}
13861394

1387-
// hookEmbed can be embedded on a JobArgs to add a hook to it in such a way that
1388-
// it can be encapsulated within a test case.
1389-
type hookEmbed[T rivertype.Hook] struct{}
1390-
1391-
func (f hookEmbed[T]) Hooks() []rivertype.Hook {
1392-
var hook T
1393-
return []rivertype.Hook{hook}
1394-
}
1395-
1396-
const (
1397-
metadataHookCalled = "called"
1398-
metadataHookInsertBeginKey = "insert_begin"
1399-
metadataHookWorkBeginKey = "work_begin"
1400-
metadataHookWorkEndKey = "work_end"
1401-
)
1402-
1403-
type metadataHookInsertBegin struct{ rivertype.Hook }
1404-
1405-
func (metadataHookInsertBegin) InsertBegin(ctx context.Context, params *rivertype.JobInsertParams) error {
1406-
var metadataMap map[string]any
1407-
if err := json.Unmarshal(params.Metadata, &metadataMap); err != nil {
1408-
return err
1409-
}
1410-
1411-
metadataMap[metadataHookInsertBeginKey] = metadataHookCalled
1412-
1413-
var err error
1414-
params.Metadata, err = json.Marshal(metadataMap)
1415-
if err != nil {
1416-
return err
1417-
}
1418-
1419-
return nil
1420-
}
1421-
1422-
type metadataHookWorkBegin struct{ rivertype.Hook }
1423-
1424-
func (metadataHookWorkBegin) WorkBegin(ctx context.Context, job *rivertype.JobRow) error {
1425-
metadataUpdates, hasMetadataUpdates := jobexecutor.MetadataUpdatesFromWorkContext(ctx)
1426-
if !hasMetadataUpdates {
1427-
panic("expected to be called from within job executor")
1428-
}
1429-
1430-
metadataUpdates[metadataHookWorkBeginKey] = metadataHookCalled
1431-
1432-
return nil
1433-
}
1434-
1435-
type metadataHookWorkEnd struct{ rivertype.Hook }
1436-
1437-
func (metadataHookWorkEnd) WorkEnd(ctx context.Context, job *rivertype.JobRow, err error) error {
1438-
metadataUpdates, hasMetadataUpdates := jobexecutor.MetadataUpdatesFromWorkContext(ctx)
1439-
if !hasMetadataUpdates {
1440-
panic("expected to be called from within job executor")
1441-
}
1442-
1443-
metadataUpdates[metadataHookWorkEndKey] = metadataHookCalled
1444-
1445-
return err
1446-
}
1447-
1448-
var (
1449-
_ rivertype.HookInsertBegin = metadataHookInsertBegin{}
1450-
_ rivertype.HookWorkBegin = metadataHookWorkBegin{}
1451-
_ rivertype.HookWorkEnd = metadataHookWorkEnd{}
1452-
)
1453-
14541395
type workerWithMiddleware[T JobArgs] struct {
14551396
WorkerDefaults[T]
14561397

@@ -8036,3 +7977,15 @@ func NewDriverWithoutListenNotify(dbPool *pgxpool.Pool) *DriverWithoutListenNoti
80367977

80377978
func (d *DriverWithoutListenNotify) SupportsListener() bool { return false }
80387979
func (d *DriverWithoutListenNotify) SupportsListenNotify() bool { return false }
7980+
7981+
type JobArgsWithHooksFunc func() []rivertype.Hook
7982+
7983+
func (JobArgsWithHooksFunc) Kind() string { return "job_args_with_hooks" }
7984+
7985+
func (f JobArgsWithHooksFunc) Hooks() []rivertype.Hook {
7986+
return f()
7987+
}
7988+
7989+
func (JobArgsWithHooksFunc) MarshalJSON() ([]byte, error) { return []byte("{}"), nil }
7990+
7991+
func (JobArgsWithHooksFunc) UnmarshalJSON([]byte) error { return nil }

rivershared/riverpilot/pilot.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ type Pilot interface {
3636

3737
JobSetStateIfRunningMany(ctx context.Context, exec riverdriver.Executor, params *riverdriver.JobSetStateIfRunningManyParams) ([]*rivertype.JobRow, error)
3838

39-
PilotInit(archetype *baseservice.Archetype)
39+
PilotInit(archetype *baseservice.Archetype, params *PilotInitParams)
4040

4141
// ProducerInit is called when a producer is started. It should return the ID
4242
// of the new producer, a new state object that will be used to track the
@@ -50,6 +50,17 @@ type Pilot interface {
5050
QueueMetadataChanged(ctx context.Context, exec riverdriver.Executor, params *QueueMetadataChangedParams) error
5151
}
5252

53+
// PilotInitParams are parameters for initializing a pilot.
54+
//
55+
// API is not stable. DO NOT USE.
56+
type PilotInitParams struct {
57+
// WorkerMetadata is metadata about registered workers as received from the
58+
// client's worker bundle. Only available when a client will work jobs (i.e.
59+
// has Workers configured), so while it's safe to assume the presence of
60+
// this value in places like maintenance services, it's not in all contexts.
61+
WorkerMetadata []*rivertype.WorkerMetadata
62+
}
63+
5364
// PilotPeriodicJob contains pilot functions related to periodic jobs. This is
5465
// extracted as its own interface so there's less surface area to mock in places
5566
// like the periodic job enqueuer where that's needed.

rivershared/riverpilot/standard.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ func (p *StandardPilot) PeriodicJobUpsertMany(ctx context.Context, exec riverdri
4848
return nil, nil
4949
}
5050

51-
func (p *StandardPilot) PilotInit(archetype *baseservice.Archetype) {
51+
func (p *StandardPilot) PilotInit(archetype *baseservice.Archetype, params *PilotInitParams) {
5252
// No-op
5353
}
5454

rivertype/river_type.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -494,3 +494,13 @@ func UniqueOptsByStateDefault() []JobState {
494494
JobStateScheduled,
495495
}
496496
}
497+
498+
// WorkerMetadata is metadata about workers registerd with a client.
499+
type WorkerMetadata struct {
500+
// JobArgHooks are job args specific hooks returned from a JobArgsWithHooks
501+
// implementation.
502+
JobArgHooks []Hook
503+
504+
// Kind is the kind returned from job args and recognized by worker to work.
505+
Kind string
506+
}

worker.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,17 @@ func AddWorker[T JobArgs](workers *Workers, worker Worker[T]) {
105105
}
106106
}
107107

108+
// AddWorkerArgs is the same as AddWorker except that it lets args be passed
109+
// explicitly rather than being instantiated implicitly. We don't know of any
110+
// use for this function beyond exercising some args-related edge cases in tests
111+
// are are difficult/impossible to exercise otherwise, and its use should be
112+
// considered internal only.
113+
func AddWorkerArgs[T JobArgs](workers *Workers, jobArgs T, worker Worker[T]) {
114+
if err := workers.add(jobArgs, &workUnitFactoryWrapper[T]{worker: worker}); err != nil {
115+
panic(err)
116+
}
117+
}
118+
108119
// AddWorkerSafely registers a worker on the provided Workers bundle. Unlike AddWorker,
109120
// AddWorkerSafely does not panic and instead returns an error if the worker
110121
// is already registered or if its configuration is invalid.

worker_test.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,16 @@ func TestWork(t *testing.T) {
4545
return nil
4646
}))
4747
})
48+
49+
type OtherJobArgs struct {
50+
testutil.JobArgsReflectKind[OtherJobArgs]
51+
}
52+
53+
var jobArgs OtherJobArgs
54+
AddWorkerArgs(workers, jobArgs, WorkFunc(func(ctx context.Context, job *Job[OtherJobArgs]) error {
55+
return nil
56+
}))
57+
require.Contains(t, workers.workersMap, (OtherJobArgs{}).Kind())
4858
}
4959

5060
type configurableArgs struct {

0 commit comments

Comments
 (0)