Skip to content
Open
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
5 changes: 0 additions & 5 deletions internal/rivercommon/river_common.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,6 @@ const (

// MetadataKeyRescueCount records how many times the job has been rescued.
MetadataKeyRescueCount = "river:rescue_count"

// MetadataKeyUniqueNonce is a special metadata key used by the SQLite driver to
// determine whether an upsert is was skipped or not because the `(xmax != 0)`
// trick we use in Postgres doesn't work in SQLite.
MetadataKeyUniqueNonce = "river:unique_nonce"
)

type ContextKeyClient struct{}
Expand Down
18 changes: 18 additions & 0 deletions riverdriver/riverdatabasesql/internal/dbsqlc/pg_misc.sql.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

67 changes: 63 additions & 4 deletions riverdriver/riverdatabasesql/river_database_sql_driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import (
"io/fs"
"math"
"strings"
"sync"
"sync/atomic"
"time"

"github.com/lib/pq"
Expand All @@ -26,6 +28,7 @@ import (
"github.com/riverqueue/river/rivershared/uniquestates"
"github.com/riverqueue/river/rivershared/util/dbutil"
"github.com/riverqueue/river/rivershared/util/ptrutil"
"github.com/riverqueue/river/rivershared/util/randutil"
"github.com/riverqueue/river/rivershared/util/savepointutil"
"github.com/riverqueue/river/rivershared/util/sliceutil"
"github.com/riverqueue/river/rivertype"
Expand All @@ -36,8 +39,10 @@ var migrationFS embed.FS

// Driver is an implementation of riverdriver.Driver for database/sql.
type Driver struct {
dbPool *sql.DB
replacer sqlctemplate.Replacer
dbPool *sql.DB
replacer sqlctemplate.Replacer
uniqueInsertMode atomic.Uint32
uniqueInsertModeInitMu sync.Mutex
}

// New returns a new database/sql River driver for use with River.
Expand Down Expand Up @@ -362,6 +367,16 @@ func (e *Executor) JobGetStuck(ctx context.Context, params *riverdriver.JobGetSt
}

func (e *Executor) JobInsertFastMany(ctx context.Context, params *riverdriver.JobInsertFastManyParams) ([]*riverdriver.JobInsertFastResult, error) {
uniqueInsertMode, err := e.uniqueInsertMode(ctx)
if err != nil {
return nil, err
}

var uniqueNonce string
if uniqueInsertMode == riverdriver.UniqueInsertModeMetadataNonce {
uniqueNonce = randutil.Hex(8)
}

insertJobsParams := &dbsqlc.JobInsertFastManyParams{
ID: make([]int64, len(params.Jobs)),
Args: make([]string, len(params.Jobs)),
Expand Down Expand Up @@ -402,7 +417,16 @@ func (e *Executor) JobInsertFastMany(ctx context.Context, params *riverdriver.Jo
insertJobsParams.CreatedAt[i] = createdAt
insertJobsParams.Kind[i] = params.Kind
insertJobsParams.MaxAttempts[i] = int16(min(params.MaxAttempts, math.MaxInt16)) //nolint:gosec
insertJobsParams.Metadata[i] = cmp.Or(string(params.Metadata), "{}")
metadata := []byte(cmp.Or(string(params.Metadata), "{}"))
if uniqueNonce != "" {
var err error
metadata, err = riverdriver.UniqueInsertMetadataWithNonce(metadata, uniqueNonce)
if err != nil {
return nil, err
}
}

insertJobsParams.Metadata[i] = string(metadata)
insertJobsParams.Priority[i] = int16(min(params.Priority, math.MaxInt16)) //nolint:gosec
insertJobsParams.Queue[i] = params.Queue
insertJobsParams.ScheduledAt[i] = scheduledAt
Expand All @@ -412,6 +436,9 @@ func (e *Executor) JobInsertFastMany(ctx context.Context, params *riverdriver.Jo
insertJobsParams.UniqueStates[i] = int32(params.UniqueStates)
}

ctx = sqlctemplate.WithReplacements(ctx, map[string]sqlctemplate.Replacement{
"unique_skipped_as_duplicate": {Value: uniqueInsertMode.SQL(), Stable: true},
}, nil)
items, err := dbsqlc.New().JobInsertFastMany(schemaTemplateParam(ctx, params.Schema), e.dbtx, insertJobsParams)
if err != nil {
return nil, interpretError(err)
Expand All @@ -422,7 +449,13 @@ func (e *Executor) JobInsertFastMany(ctx context.Context, params *riverdriver.Jo
if err != nil {
return nil, err
}
return &riverdriver.JobInsertFastResult{Job: job, UniqueSkippedAsDuplicate: row.UniqueSkippedAsDuplicate}, nil

uniqueSkippedAsDuplicate := row.UniqueSkippedAsDuplicate
if uniqueInsertMode == riverdriver.UniqueInsertModeMetadataNonce {
uniqueSkippedAsDuplicate = riverdriver.UniqueInsertMetadataIsDuplicate(job.Metadata, uniqueNonce)
}

return &riverdriver.JobInsertFastResult{Job: job, UniqueSkippedAsDuplicate: uniqueSkippedAsDuplicate}, nil
})
}

Expand Down Expand Up @@ -1050,6 +1083,32 @@ func (e *Executor) TableTruncate(ctx context.Context, params *riverdriver.TableT
return interpretError(err)
}

func (e *Executor) uniqueInsertMode(ctx context.Context) (riverdriver.UniqueInsertMode, error) {
if e.driver != nil {
if mode := riverdriver.UniqueInsertMode(e.driver.uniqueInsertMode.Load()); mode != riverdriver.UniqueInsertModeUnknown {
return mode, nil
}

e.driver.uniqueInsertModeInitMu.Lock()
defer e.driver.uniqueInsertModeInitMu.Unlock()

if mode := riverdriver.UniqueInsertMode(e.driver.uniqueInsertMode.Load()); mode != riverdriver.UniqueInsertModeUnknown {
return mode, nil
}
}

productAndVersion, err := dbsqlc.New().PGGetProductAndVersion(ctx, e.dbtx)
if err != nil {
return riverdriver.UniqueInsertModeUnknown, interpretError(err)
}

mode := riverdriver.UniqueInsertModeFromProductAndVersion(productAndVersion.Product, productAndVersion.VersionNum)
if e.driver != nil {
e.driver.uniqueInsertMode.Store(uint32(mode))
}
return mode, nil
}

type ExecutorTx struct {
Executor

Expand Down
2 changes: 1 addition & 1 deletion riverdriver/riverdrivertest/job_insert.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ func exerciseJobInsert[TTx any](ctx context.Context, t *testing.T,
// SQLite needs to set a special metadata key to be able to
// check for duplicates. Remove this for purposes of comparing
// inserted metadata.
job.Metadata, err = sjson.DeleteBytes(job.Metadata, rivercommon.MetadataKeyUniqueNonce)
job.Metadata, err = sjson.DeleteBytes(job.Metadata, riverdriver.UniqueInsertMetadataKey)
require.NoError(t, err)

require.Equal(t, idStart+int64(i), job.ID)
Expand Down
5 changes: 5 additions & 0 deletions riverdriver/riverpgxv5/internal/dbsqlc/pg_misc.sql
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
-- name: PGAdvisoryXactLock :exec
SELECT pg_advisory_xact_lock(@key);

-- name: PGGetProductAndVersion :one
SELECT
version()::text AS product,
current_setting('server_version_num')::int AS version_num;

-- name: PGNotifyMany :exec
WITH topic_to_notify AS (
SELECT
Expand Down
18 changes: 18 additions & 0 deletions riverdriver/riverpgxv5/internal/dbsqlc/pg_misc.sql.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion riverdriver/riverpgxv5/internal/dbsqlc/river_job.sql
Original file line number Diff line number Diff line change
Expand Up @@ -318,7 +318,9 @@ ON CONFLICT (unique_key)
AND /* TEMPLATE: schema */river_job_state_in_bitmask(unique_states, state)
-- Something needs to be updated for a row to be returned on a conflict.
DO UPDATE SET kind = EXCLUDED.kind
RETURNING sqlc.embed(river_job), (xmax != 0) AS unique_skipped_as_duplicate;
RETURNING
sqlc.embed(river_job),
/* TEMPLATE_BEGIN: unique_skipped_as_duplicate */ (xmax != 0) /* TEMPLATE_END */ AS unique_skipped_as_duplicate;

-- name: JobInsertFastManyNoReturning :execrows
INSERT INTO /* TEMPLATE: schema */river_job(
Expand Down
4 changes: 3 additions & 1 deletion riverdriver/riverpgxv5/internal/dbsqlc/river_job.sql.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

66 changes: 62 additions & 4 deletions riverdriver/riverpgxv5/river_pgx_v5_driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"math"
"strings"
"sync"
"sync/atomic"
"time"

"github.com/jackc/pgx/v5"
Expand All @@ -30,6 +31,7 @@ import (
"github.com/riverqueue/river/rivershared/uniquestates"
"github.com/riverqueue/river/rivershared/util/dbutil"
"github.com/riverqueue/river/rivershared/util/ptrutil"
"github.com/riverqueue/river/rivershared/util/randutil"
"github.com/riverqueue/river/rivershared/util/sliceutil"
"github.com/riverqueue/river/rivertype"
)
Expand All @@ -39,8 +41,10 @@ var migrationFS embed.FS

// Driver is an implementation of riverdriver.Driver for Pgx v5.
type Driver struct {
dbPool *pgxpool.Pool
replacer sqlctemplate.Replacer
dbPool *pgxpool.Pool
replacer sqlctemplate.Replacer
uniqueInsertMode atomic.Uint32
uniqueInsertModeInitMu sync.Mutex
}

// New returns a new Pgx v5 River driver for use with River.
Expand Down Expand Up @@ -366,6 +370,16 @@ func (e *Executor) JobGetStuck(ctx context.Context, params *riverdriver.JobGetSt
}

func (e *Executor) JobInsertFastMany(ctx context.Context, params *riverdriver.JobInsertFastManyParams) ([]*riverdriver.JobInsertFastResult, error) {
uniqueInsertMode, err := e.uniqueInsertMode(ctx)
if err != nil {
return nil, err
}

var uniqueNonce string
if uniqueInsertMode == riverdriver.UniqueInsertModeMetadataNonce {
uniqueNonce = randutil.Hex(8)
}

insertJobsParams := &dbsqlc.JobInsertFastManyParams{
ID: make([]int64, len(params.Jobs)),
Args: make([][]byte, len(params.Jobs)),
Expand Down Expand Up @@ -407,7 +421,16 @@ func (e *Executor) JobInsertFastMany(ctx context.Context, params *riverdriver.Jo
insertJobsParams.CreatedAt[i] = createdAt
insertJobsParams.Kind[i] = params.Kind
insertJobsParams.MaxAttempts[i] = int16(min(params.MaxAttempts, math.MaxInt16)) //nolint:gosec
insertJobsParams.Metadata[i] = sliceutil.FirstNonEmpty(params.Metadata, defaultObject)
metadata := sliceutil.FirstNonEmpty(params.Metadata, defaultObject)
if uniqueNonce != "" {
var err error
metadata, err = riverdriver.UniqueInsertMetadataWithNonce(metadata, uniqueNonce)
if err != nil {
return nil, err
}
}

insertJobsParams.Metadata[i] = metadata
insertJobsParams.Priority[i] = int16(min(params.Priority, math.MaxInt16)) //nolint:gosec
insertJobsParams.Queue[i] = params.Queue
insertJobsParams.ScheduledAt[i] = scheduledAt
Expand All @@ -417,6 +440,9 @@ func (e *Executor) JobInsertFastMany(ctx context.Context, params *riverdriver.Jo
insertJobsParams.UniqueStates[i] = int32(params.UniqueStates)
}

ctx = sqlctemplate.WithReplacements(ctx, map[string]sqlctemplate.Replacement{
"unique_skipped_as_duplicate": {Value: uniqueInsertMode.SQL(), Stable: true},
}, nil)
items, err := dbsqlc.New().JobInsertFastMany(schemaTemplateParam(ctx, params.Schema), e.dbtx, insertJobsParams)
if err != nil {
return nil, interpretError(err)
Expand All @@ -427,7 +453,13 @@ func (e *Executor) JobInsertFastMany(ctx context.Context, params *riverdriver.Jo
if err != nil {
return nil, err
}
return &riverdriver.JobInsertFastResult{Job: job, UniqueSkippedAsDuplicate: row.UniqueSkippedAsDuplicate}, nil

uniqueSkippedAsDuplicate := row.UniqueSkippedAsDuplicate
if uniqueInsertMode == riverdriver.UniqueInsertModeMetadataNonce {
uniqueSkippedAsDuplicate = riverdriver.UniqueInsertMetadataIsDuplicate(job.Metadata, uniqueNonce)
}

return &riverdriver.JobInsertFastResult{Job: job, UniqueSkippedAsDuplicate: uniqueSkippedAsDuplicate}, nil
})
}

Expand Down Expand Up @@ -1035,6 +1067,32 @@ func (e *Executor) TableTruncate(ctx context.Context, params *riverdriver.TableT
return interpretError(err)
}

func (e *Executor) uniqueInsertMode(ctx context.Context) (riverdriver.UniqueInsertMode, error) {
if e.driver != nil {
if mode := riverdriver.UniqueInsertMode(e.driver.uniqueInsertMode.Load()); mode != riverdriver.UniqueInsertModeUnknown {
return mode, nil
}

e.driver.uniqueInsertModeInitMu.Lock()
defer e.driver.uniqueInsertModeInitMu.Unlock()

if mode := riverdriver.UniqueInsertMode(e.driver.uniqueInsertMode.Load()); mode != riverdriver.UniqueInsertModeUnknown {
return mode, nil
}
}

productAndVersion, err := dbsqlc.New().PGGetProductAndVersion(ctx, e.dbtx)
if err != nil {
return riverdriver.UniqueInsertModeUnknown, interpretError(err)
}

mode := riverdriver.UniqueInsertModeFromProductAndVersion(productAndVersion.Product, productAndVersion.VersionNum)
if e.driver != nil {
e.driver.uniqueInsertMode.Store(uint32(mode))
}
return mode, nil
}

type ExecutorTx struct {
Executor

Expand Down
5 changes: 0 additions & 5 deletions riverdriver/riversqlite/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,14 @@ go 1.25.0
toolchain go1.25.7

require (
github.com/riverqueue/river v0.43.0
github.com/riverqueue/river/riverdriver v0.43.0
github.com/riverqueue/river/rivershared v0.43.0
github.com/riverqueue/river/rivertype v0.43.0
github.com/stretchr/testify v1.11.1
github.com/tidwall/gjson v1.19.0
github.com/tidwall/sjson v1.2.5
)

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/tidwall/match v1.2.0 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
11 changes: 0 additions & 11 deletions riverdriver/riversqlite/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -28,17 +28,6 @@ github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/gjson v1.19.0 h1:xwxm7n691Uf3u5OFjzngavjGTh55KX5q/9w9xHW88JU=
github.com/tidwall/gjson v1.19.0/go.mod h1:V37/opeE/JbLUOfH0QTXiNez2l0RUjYUhpT4szFQAfc=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/match v1.2.0 h1:0pt8FlkOwjN2fPt4bIl4BoNxb98gGHN2ObFEDkrfZnM=
github.com/tidwall/match v1.2.0/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
Expand Down
Loading
Loading