Skip to content

Commit 000c152

Browse files
committed
Make Plugin a marker interface
Make hooks and middleware opt into `Plugin` through their default embedders and function helpers. Keep `Config.Plugins` as the unified registration path.
1 parent 6d60ff7 commit 000c152

12 files changed

Lines changed: 234 additions & 87 deletions

client.go

Lines changed: 93 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ import (
2525
"github.com/riverqueue/river/internal/middlewarelookup"
2626
"github.com/riverqueue/river/internal/notifier"
2727
"github.com/riverqueue/river/internal/notifylimiter"
28-
"github.com/riverqueue/river/internal/pluginconfig"
2928
"github.com/riverqueue/river/internal/rivercommon"
3029
"github.com/riverqueue/river/internal/rivermiddleware"
3130
"github.com/riverqueue/river/internal/workunit"
@@ -245,6 +244,8 @@ type Config struct {
245244
//
246245
// If a type in Hooks also implements rivertype.Middleware, it will be
247246
// installed as middleware too.
247+
//
248+
// Deprecated: Use Plugins instead.
248249
Hooks []rivertype.Hook
249250

250251
// Logger is the structured logger to use for logging purposes. If none is
@@ -281,20 +282,22 @@ type Config struct {
281282
//
282283
// If a type in Middleware also implements rivertype.Hook, it will be
283284
// installed as a hook too.
285+
//
286+
// Deprecated: Use Plugins instead.
284287
Middleware []rivertype.Middleware
285288

286-
// Plugins contains extensions installed globally as both hooks and
287-
// middleware.
289+
// Plugins contains extensions installed globally as hooks, middleware, or
290+
// both.
288291
//
289-
// A plugin must implement both rivertype.Hook and rivertype.Middleware. It
290-
// may embed PluginDefaults, or embed both HookDefaults and
291-
// MiddlewareDefaults directly, then implement any operation-specific hook or
292-
// middleware interfaces it needs.
292+
// A type qualifies as a plugin by implementing [rivertype.Plugin]. Most
293+
// existing hook and middleware implementations already do this by embedding
294+
// HookDefaults or MiddlewareDefaults. If a type participates on both sides,
295+
// it may embed PluginDefaults, or embed both HookDefaults and
296+
// MiddlewareDefaults directly and define its own IsPlugin method, then
297+
// implement any operation-specific hook or middleware interfaces it needs.
293298
//
294-
// Hooks and Middleware are still supported. If a type in Hooks also
295-
// implements middleware, or a type in Middleware also implements hooks, River
296-
// will install it on both sides automatically. The Plugins list exists as a
297-
// generic place for new extensions to be registered.
299+
// Hooks and Middleware are still supported for backward compatibility, but
300+
// Plugins is the preferred place to register new extensions.
298301
Plugins []rivertype.Plugin
299302

300303
// PeriodicJobs are a set of periodic jobs to run at the specified intervals
@@ -542,6 +545,83 @@ func (c *Config) WithDefaults() *Config {
542545
}
543546
}
544547

548+
func effectiveHooksForConfig(config *Config) []rivertype.Hook {
549+
configuredMiddleware := middlewareFromConfig(config)
550+
hooks := make([]rivertype.Hook, 0, len(config.Hooks)+len(configuredMiddleware)+len(config.Plugins))
551+
hooks = append(hooks, config.Hooks...)
552+
553+
for _, middleware := range configuredMiddleware {
554+
if hook, ok := middleware.(rivertype.Hook); ok {
555+
hooks = append(hooks, hook)
556+
}
557+
}
558+
559+
for _, plugin := range config.Plugins {
560+
if plugin == nil {
561+
continue
562+
}
563+
564+
if hook, ok := plugin.(rivertype.Hook); ok {
565+
hooks = append(hooks, hook)
566+
}
567+
}
568+
569+
return hooks
570+
}
571+
572+
func effectiveMiddlewareForConfig(config *Config) []rivertype.Middleware {
573+
configuredMiddleware := middlewareFromConfig(config)
574+
middleware := make([]rivertype.Middleware, 0, len(config.Hooks)+len(configuredMiddleware)+len(config.Plugins))
575+
middleware = append(middleware, configuredMiddleware...)
576+
577+
for _, hook := range config.Hooks {
578+
if middlewareItem, ok := hook.(rivertype.Middleware); ok {
579+
middleware = append(middleware, middlewareItem)
580+
}
581+
}
582+
583+
for _, plugin := range config.Plugins {
584+
if plugin == nil {
585+
continue
586+
}
587+
588+
if middlewareItem, ok := plugin.(rivertype.Middleware); ok {
589+
middleware = append(middleware, middlewareItem)
590+
}
591+
}
592+
593+
return middleware
594+
}
595+
596+
func middlewareFromConfig(config *Config) []rivertype.Middleware {
597+
middleware := make([]rivertype.Middleware, 0,
598+
len(config.Middleware)+len(config.JobInsertMiddleware)+len(config.WorkerMiddleware))
599+
middleware = append(middleware, config.Middleware...)
600+
601+
for _, jobInsertMiddleware := range config.JobInsertMiddleware {
602+
middleware = append(middleware, jobInsertMiddleware)
603+
}
604+
605+
outerLoop:
606+
for _, workerMiddleware := range config.WorkerMiddleware {
607+
// Don't add the middleware if it also implements JobInsertMiddleware
608+
// and the instance has been added to config.JobInsertMiddleware. This
609+
// is a hedge to make sure we don't accidentally double add middleware
610+
// as we've converted over to the unified config.Middleware setting.
611+
if workerMiddlewareAsJobInsertMiddleware, ok := workerMiddleware.(rivertype.JobInsertMiddleware); ok {
612+
for _, jobInsertMiddleware := range config.JobInsertMiddleware {
613+
if workerMiddlewareAsJobInsertMiddleware == jobInsertMiddleware {
614+
continue outerLoop
615+
}
616+
}
617+
}
618+
619+
middleware = append(middleware, workerMiddleware)
620+
}
621+
622+
return middleware
623+
}
624+
545625
func (c *Config) validate() error {
546626
if c.CancelledJobRetentionPeriod < -1 {
547627
return errors.New("CancelledJobRetentionPeriod time cannot be less than zero, except for -1 (infinite)")
@@ -824,9 +904,8 @@ func NewClient[TTx any](driver riverdriver.Driver[TTx], config *Config) (*Client
824904
}
825905
}
826906

827-
configuredMiddleware := middlewareFromConfig(config)
828-
effectiveHooks := pluginconfig.Hooks(config.Hooks, configuredMiddleware, config.Plugins)
829-
effectiveMiddleware := pluginconfig.Middleware(config.Hooks, configuredMiddleware, config.Plugins)
907+
effectiveHooks := effectiveHooksForConfig(config)
908+
effectiveMiddleware := effectiveMiddlewareForConfig(config)
830909

831910
for _, hook := range effectiveHooks {
832911
if withBaseService, ok := hook.(baseservice.WithBaseService); ok {
@@ -1075,35 +1154,6 @@ func NewClient[TTx any](driver riverdriver.Driver[TTx], config *Config) (*Client
10751154
return client, nil
10761155
}
10771156

1078-
func middlewareFromConfig(config *Config) []rivertype.Middleware {
1079-
middleware := make([]rivertype.Middleware, 0,
1080-
len(config.Middleware)+len(config.JobInsertMiddleware)+len(config.WorkerMiddleware))
1081-
middleware = append(middleware, config.Middleware...)
1082-
1083-
for _, jobInsertMiddleware := range config.JobInsertMiddleware {
1084-
middleware = append(middleware, jobInsertMiddleware)
1085-
}
1086-
1087-
outerLoop:
1088-
for _, workerMiddleware := range config.WorkerMiddleware {
1089-
// Don't add the middleware if it also implements JobInsertMiddleware
1090-
// and the instance has been added to config.JobInsertMiddleware. This
1091-
// is a hedge to make sure we don't accidentally double add middleware
1092-
// as we've converted over to the unified config.Middleware setting.
1093-
if workerMiddlewareAsJobInsertMiddleware, ok := workerMiddleware.(rivertype.JobInsertMiddleware); ok {
1094-
for _, jobInsertMiddleware := range config.JobInsertMiddleware {
1095-
if workerMiddlewareAsJobInsertMiddleware == jobInsertMiddleware {
1096-
continue outerLoop
1097-
}
1098-
}
1099-
}
1100-
1101-
middleware = append(middleware, workerMiddleware)
1102-
}
1103-
1104-
return middleware
1105-
}
1106-
11071157
// Start starts the client's job fetching and working loops. Once this is called,
11081158
// the client will run in a background goroutine until stopped. All jobs are
11091159
// run with a context inheriting from the provided context, but with a timeout

client_test.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ var (
8181

8282
_ rivertype.HookInsertBegin = hookMiddlewareValuePlugin{}
8383
_ rivertype.JobInsertMiddleware = hookMiddlewareValuePlugin{}
84+
_ rivertype.Plugin = hookMiddlewareValuePlugin{}
8485
)
8586

8687
type hookMiddlewareEmbeddedDefaultsPlugin struct {
@@ -91,6 +92,8 @@ type hookMiddlewareEmbeddedDefaultsPlugin struct {
9192
insertManyCount int
9293
}
9394

95+
func (p *hookMiddlewareEmbeddedDefaultsPlugin) IsPlugin() bool { return true }
96+
9497
func (p *hookMiddlewareEmbeddedDefaultsPlugin) InsertBegin(ctx context.Context, params *rivertype.JobInsertParams) error {
9598
p.insertBeginCount++
9699
return nil
@@ -134,6 +137,8 @@ func (p hookMiddlewareValuePlugin) InsertMany(ctx context.Context, manyParams []
134137

135138
func (p hookMiddlewareValuePlugin) IsHook() bool { return true }
136139

140+
func (p hookMiddlewareValuePlugin) IsPlugin() bool { return true }
141+
137142
func (p hookMiddlewareValuePlugin) IsMiddleware() bool { return true }
138143

139144
type hookMiddlewareValuePluginCounts struct {

example_global_hooks_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ func Example_globalHooks() { //nolint:dupl
6565

6666
riverClient, err := river.NewClient(riverpgxv5.New(dbPool), initTestConfig(ctx, dbPool, &river.Config{
6767
// Order is significant. See output below.
68-
Hooks: []rivertype.Hook{
68+
Plugins: []rivertype.Plugin{
6969
&BothInsertAndWorkBeginHook{},
7070
&InsertBeginHook{},
7171
&WorkBeginHook{},

example_global_middleware_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ func Example_globalMiddleware() { //nolint:dupl
6565

6666
riverClient, err := river.NewClient(riverpgxv5.New(dbPool), initTestConfig(ctx, dbPool, &river.Config{
6767
// Order is significant. See output below.
68-
Middleware: []rivertype.Middleware{
68+
Plugins: []rivertype.Plugin{
6969
&JobBothInsertAndWorkMiddleware{},
7070
&JobInsertMiddleware{},
7171
&WorkerMiddleware{},

hook_defaults_funcs.go

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,14 @@ import (
77
)
88

99
// HookDefaults should be embedded on any hooks implementation. It helps
10-
// identify a struct as hooks, and guarantee forward compatibility in case
11-
// additions are necessary to the rivertype.Hook interface.
10+
// identify a struct as hooks and a plugin, and guarantee forward compatibility
11+
// in case additions are necessary to the rivertype.Hook interface.
1212
type HookDefaults struct{}
1313

1414
func (d *HookDefaults) IsHook() bool { return true }
1515

16+
func (d *HookDefaults) IsPlugin() bool { return true }
17+
1618
// HookInsertBeginFunc is a convenience helper for implementing
1719
// rivertype.HookInsertBegin using a simple function instead of a struct.
1820
type HookInsertBeginFunc func(ctx context.Context, params *rivertype.JobInsertParams) error
@@ -23,12 +25,16 @@ func (f HookInsertBeginFunc) InsertBegin(ctx context.Context, params *rivertype.
2325

2426
func (f HookInsertBeginFunc) IsHook() bool { return true }
2527

28+
func (f HookInsertBeginFunc) IsPlugin() bool { return true }
29+
2630
// HookPeriodicJobsStartFunc is a convenience helper for implementing
2731
// rivertype.HookPeriodicJobsStart using a simple function instead of a struct.
2832
type HookPeriodicJobsStartFunc func(ctx context.Context, params *rivertype.HookPeriodicJobsStartParams) error
2933

3034
func (f HookPeriodicJobsStartFunc) IsHook() bool { return true }
3135

36+
func (f HookPeriodicJobsStartFunc) IsPlugin() bool { return true }
37+
3238
func (f HookPeriodicJobsStartFunc) Start(ctx context.Context, params *rivertype.HookPeriodicJobsStartParams) error {
3339
return f(ctx, params)
3440
}
@@ -37,18 +43,22 @@ func (f HookPeriodicJobsStartFunc) Start(ctx context.Context, params *rivertype.
3743
// rivertype.HookWorkBegin using a simple function instead of a struct.
3844
type HookWorkBeginFunc func(ctx context.Context, job *rivertype.JobRow) error
3945

46+
func (f HookWorkBeginFunc) IsHook() bool { return true }
47+
48+
func (f HookWorkBeginFunc) IsPlugin() bool { return true }
49+
4050
func (f HookWorkBeginFunc) WorkBegin(ctx context.Context, job *rivertype.JobRow) error {
4151
return f(ctx, job)
4252
}
4353

44-
func (f HookWorkBeginFunc) IsHook() bool { return true }
45-
4654
// HookWorkEndFunc is a convenience helper for implementing
4755
// rivertype.HookWorkEnd using a simple function instead of a struct.
4856
type HookWorkEndFunc func(ctx context.Context, job *rivertype.JobRow, err error) error
4957

58+
func (f HookWorkEndFunc) IsHook() bool { return true }
59+
60+
func (f HookWorkEndFunc) IsPlugin() bool { return true }
61+
5062
func (f HookWorkEndFunc) WorkEnd(ctx context.Context, job *rivertype.JobRow, err error) error {
5163
return f(ctx, job, err)
5264
}
53-
54-
func (f HookWorkEndFunc) IsHook() bool { return true }

internal/pluginconfig/plugin_config.go

Lines changed: 10 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@ package pluginconfig
22

33
import "github.com/riverqueue/river/rivertype"
44

5-
// Hooks returns the effective hook list from configured hooks, middleware, and
6-
// plugins. Explicit hooks are preserved first, followed by middleware that also
7-
// implement hooks, then plugins.
8-
func Hooks(hooks []rivertype.Hook, middleware []rivertype.Middleware, plugins []rivertype.Plugin) []rivertype.Hook {
9-
effectiveHooks := make([]rivertype.Hook, 0, len(hooks)+len(middleware)+len(plugins))
5+
// Hooks returns the effective hook list from configured hooks and middleware.
6+
// Explicit hooks are preserved first, followed by middleware that also
7+
// implement hooks.
8+
func Hooks(hooks []rivertype.Hook, middleware []rivertype.Middleware) []rivertype.Hook {
9+
effectiveHooks := make([]rivertype.Hook, 0, len(hooks)+len(middleware))
1010

1111
effectiveHooks = append(effectiveHooks, hooks...)
1212

@@ -19,18 +19,14 @@ func Hooks(hooks []rivertype.Hook, middleware []rivertype.Middleware, plugins []
1919
effectiveHooks = append(effectiveHooks, hook)
2020
}
2121

22-
for _, plugin := range plugins {
23-
effectiveHooks = append(effectiveHooks, plugin)
24-
}
25-
2622
return effectiveHooks
2723
}
2824

29-
// Middleware returns the effective middleware list from configured hooks,
30-
// middleware, and plugins. Explicit middleware are preserved first, followed by
31-
// hooks that also implement middleware, then plugins.
32-
func Middleware(hooks []rivertype.Hook, middleware []rivertype.Middleware, plugins []rivertype.Plugin) []rivertype.Middleware {
33-
effectiveMiddleware := make([]rivertype.Middleware, 0, len(hooks)+len(middleware)+len(plugins))
25+
// Middleware returns the effective middleware list from configured hooks and
26+
// middleware. Explicit middleware are preserved first, followed by hooks that
27+
// also implement middleware.
28+
func Middleware(hooks []rivertype.Hook, middleware []rivertype.Middleware) []rivertype.Middleware {
29+
effectiveMiddleware := make([]rivertype.Middleware, 0, len(hooks)+len(middleware))
3430

3531
effectiveMiddleware = append(effectiveMiddleware, middleware...)
3632

@@ -43,9 +39,5 @@ func Middleware(hooks []rivertype.Hook, middleware []rivertype.Middleware, plugi
4339
effectiveMiddleware = append(effectiveMiddleware, middlewareItem)
4440
}
4541

46-
for _, plugin := range plugins {
47-
effectiveMiddleware = append(effectiveMiddleware, plugin)
48-
}
49-
5042
return effectiveMiddleware
5143
}

middleware_defaults.go

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,15 @@ import (
77
)
88

99
// MiddlewareDefaults should be embedded on any middleware implementation. It
10-
// helps identify a struct as middleware, and guarantees forward compatibility in
11-
// case additions are necessary to the rivertype.Middleware interface.
10+
// helps identify a struct as middleware and a plugin, and guarantees forward
11+
// compatibility in case additions are necessary to the rivertype.Middleware
12+
// interface.
1213
type MiddlewareDefaults struct{}
1314

1415
func (d *MiddlewareDefaults) IsMiddleware() bool { return true }
1516

17+
func (d *MiddlewareDefaults) IsPlugin() bool { return true }
18+
1619
// JobInsertMiddlewareDefaults is an embeddable struct that provides default
1720
// implementations for the rivertype.JobInsertMiddleware. Use of this struct is
1821
// recommended in case rivertype.JobInsertMiddleware is expanded in the future
@@ -35,6 +38,8 @@ func (f JobInsertMiddlewareFunc) InsertMany(ctx context.Context, manyParams []*r
3538

3639
func (f JobInsertMiddlewareFunc) IsMiddleware() bool { return true }
3740

41+
func (f JobInsertMiddlewareFunc) IsPlugin() bool { return true }
42+
3843
// WorkerInsertMiddlewareDefaults is an embeddable struct that provides default
3944
// implementations for the rivertype.WorkerMiddleware. Use of this struct is
4045
// recommended in case rivertype.WorkerMiddleware is expanded in the future so
@@ -51,8 +56,10 @@ func (d *WorkerMiddlewareDefaults) Work(ctx context.Context, job *rivertype.JobR
5156
// rivertype.WorkerMiddleware using a simple function instead of a struct.
5257
type WorkerMiddlewareFunc func(ctx context.Context, job *rivertype.JobRow, doInner func(ctx context.Context) error) error
5358

59+
func (f WorkerMiddlewareFunc) IsMiddleware() bool { return true }
60+
61+
func (f WorkerMiddlewareFunc) IsPlugin() bool { return true }
62+
5463
func (f WorkerMiddlewareFunc) Work(ctx context.Context, job *rivertype.JobRow, doInner func(ctx context.Context) error) error {
5564
return f(ctx, job, doInner)
5665
}
57-
58-
func (f WorkerMiddlewareFunc) IsMiddleware() bool { return true }

plugin_defaults.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
package river
22

33
// PluginDefaults should be embedded on plugin implementations. It helps
4-
// identify a struct as both hooks and middleware, and guarantees forward
4+
// identify a struct as both hook and middleware, and guarantees forward
55
// compatibility in case additions are necessary to the rivertype.Hook or
66
// rivertype.Middleware interfaces.
77
type PluginDefaults struct {
88
HookDefaults
99
MiddlewareDefaults
1010
}
11+
12+
func (d *PluginDefaults) IsPlugin() bool { return true }

plugin_defaults_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,6 @@ var (
66
_ rivertype.Hook = &PluginDefaults{}
77
_ rivertype.Middleware = &PluginDefaults{}
88
_ rivertype.Plugin = &PluginDefaults{}
9+
_ rivertype.Plugin = &HookDefaults{}
10+
_ rivertype.Plugin = &MiddlewareDefaults{}
911
)

0 commit comments

Comments
 (0)