Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Fixed

- Fixed `AddWorkerSafely` leaving a worker's primary kind and earlier aliases registered when a later kind alias conflicts with an existing registration. Failed registrations now leave the worker registry unchanged. [PR #1440](https://github.com/riverqueue/river/pull/1440).

## [0.49.0] - 2026-10-05

### Changed
Expand Down
32 changes: 13 additions & 19 deletions worker.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,32 +162,26 @@ func NewWorkers() *Workers {
}

func (w Workers) add(jobArgs JobArgs, workUnitFactory workunit.WorkUnitFactory) error {
checkRegistered := func(kind string) error {
if _, ok := w.workersMap[kind]; ok {
kinds := []string{jobArgs.Kind()}
if args, ok := jobArgs.(JobArgsWithKindAliases); ok {
kinds = append(kinds, args.KindAliases()...)
}

// Validate all kinds before changing the registry.
seen := make(map[string]bool, len(kinds))
for _, kind := range kinds {
if _, ok := w.workersMap[kind]; ok || seen[kind] {
return fmt.Errorf("worker for kind %q is already registered", kind)
}
return nil
seen[kind] = true
}

workerInfo := workerInfo{
info := workerInfo{
jobArgs: jobArgs,
workUnitFactory: workUnitFactory,
}

kind := jobArgs.Kind()
if err := checkRegistered(kind); err != nil {
return err
}
w.workersMap[kind] = workerInfo

// Jobs can register an alternate kind to make renaming easier.
if jobArgsWithKindAliases, ok := jobArgs.(JobArgsWithKindAliases); ok {
for _, kind := range jobArgsWithKindAliases.KindAliases() {
if err := checkRegistered(kind); err != nil {
return err
}
w.workersMap[kind] = workerInfo
}
for _, kind := range kinds {
w.workersMap[kind] = info
}

return nil
Expand Down
20 changes: 20 additions & 0 deletions worker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,19 @@ import (
"github.com/riverqueue/river/rivershared/util/testutil"
)

func TestAddWorkerSafelyAliasCollisionDoesNotRegisterKinds(t *testing.T) {
t.Parallel()

workers := NewWorkers()

require.NoError(t, AddWorkerSafely(workers, WorkFunc(func(context.Context, *Job[withKindAliasesArgs]) error { return nil })))
before := len(workers.workersMap)
require.Error(t, AddWorkerSafely(workers, WorkFunc(func(context.Context, *Job[lateAliasCollisionArgs]) error { return nil })))
require.Len(t, workers.workersMap, before)
require.NotContains(t, workers.workersMap, "candidate_primary")
require.NotContains(t, workers.workersMap, "candidate_early_alias")
}

func TestWork(t *testing.T) {
t.Parallel()

Expand Down Expand Up @@ -74,6 +87,13 @@ func (w *configurableWorker) Work(ctx context.Context, job *Job[configurableArgs
return nil
}

type lateAliasCollisionArgs struct{}

func (lateAliasCollisionArgs) Kind() string { return "candidate_primary" }
func (lateAliasCollisionArgs) KindAliases() []string {
return []string{"candidate_early_alias", "with_kind_alternate_alternate"}
}

type withKindAliasesArgs struct{}

func (a withKindAliasesArgs) Kind() string { return "with_kind_alternate" }
Expand Down
Loading