diff --git a/cmd/river/go.mod b/cmd/river/go.mod index 81f7e82ae..4ef087da7 100644 --- a/cmd/river/go.mod +++ b/cmd/river/go.mod @@ -29,7 +29,7 @@ require ( github.com/ncruces/go-strftime v1.0.0 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/spf13/pflag v1.0.9 // indirect - github.com/tidwall/gjson v1.19.0 // indirect + github.com/tidwall/gjson v1.20.0 // indirect github.com/tidwall/match v1.2.0 // indirect github.com/tidwall/pretty v1.2.1 // indirect github.com/tidwall/sjson v1.2.5 // indirect diff --git a/cmd/river/go.sum b/cmd/river/go.sum index cd9858617..5152f0599 100644 --- a/cmd/river/go.sum +++ b/cmd/river/go.sum @@ -29,18 +29,18 @@ github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJm github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= -github.com/riverqueue/river v0.48.0 h1:SJwDBsNY/GuSdcW4d7xf2BeRrC1XThqd2Sns+i8ufQU= -github.com/riverqueue/river v0.48.0/go.mod h1:p0oR3A4EPF9IkyKhFiyPPRWgHRP4lF2zqtcPWs39Kp0= -github.com/riverqueue/river/riverdriver v0.48.0 h1:7oKPZb3tNjvNyQsi+JapWWREpLnOdUamb9YTnLGY3Lg= -github.com/riverqueue/river/riverdriver v0.48.0/go.mod h1:1MpM6Mf/VqlJp/iF4pzTcPrF/cMsLZ96ARw1tjfmewI= -github.com/riverqueue/river/riverdriver/riverpgxv5 v0.48.0 h1:IIS1ZfusT5zdTC5oUF/MnIthYKjUEyUGBZB3QmuqyOU= -github.com/riverqueue/river/riverdriver/riverpgxv5 v0.48.0/go.mod h1:CZe68VANQs/2fjvw96AJC0XXyptJW7KiGHSFT5XSUvo= -github.com/riverqueue/river/riverdriver/riversqlite v0.48.0 h1:6qdp9pVIlw7aPHYo3W96Vfk750j1CIKhzeUCLP0/U/U= -github.com/riverqueue/river/riverdriver/riversqlite v0.48.0/go.mod h1:g9uElXcqkIwfq7/ebVz3bL2vD7i9mLSlTltqK5sXD2I= -github.com/riverqueue/river/rivershared v0.48.0 h1:PGKa+ke7nqgBqchaSOXtQJ6Ghok5wSWJg3Su5m+m0PU= -github.com/riverqueue/river/rivershared v0.48.0/go.mod h1:FmqY+WQVCot+obBqsTWoPcIOHjYXdacHRMqZNw/oXRk= -github.com/riverqueue/river/rivertype v0.48.0 h1:t9giVes2Y2w9pGaeuRyUyGC7/QoJgTdr2vhY5W1PazU= -github.com/riverqueue/river/rivertype v0.48.0/go.mod h1:XKkcRQR6zm8RR/JQa1Q2ywpj8uXQu21quPa4Lpw1Xhw= +github.com/riverqueue/river v0.49.0 h1:JUCLFgregbX1Wu+bSTHCjF/WHGdrwGbtz/ddInHfeb0= +github.com/riverqueue/river v0.49.0/go.mod h1:USYb57gpMBXLQm2l7poskAalOPaf5uHjSZgp2AOoYDw= +github.com/riverqueue/river/riverdriver v0.49.0 h1:kSykNQJNB7AeG6kudjB0mThV29PvykoOBIFT8ipCHEc= +github.com/riverqueue/river/riverdriver v0.49.0/go.mod h1:rVUuX/fTF2kAiJjpPT+10Lft8n90kvLqjm6b+rFXaVE= +github.com/riverqueue/river/riverdriver/riverpgxv5 v0.49.0 h1:c7YA1plP/nrNS3SEHuKizf4wHx9OQ87Yxuhj3V0eQ50= +github.com/riverqueue/river/riverdriver/riverpgxv5 v0.49.0/go.mod h1:o7zkFstM+Fk+mnlOW2sx92mQW4JswSc6zvN191l8Isg= +github.com/riverqueue/river/riverdriver/riversqlite v0.49.0 h1:DfATYWIuQ4bKjhhqj/XYn3ZGpluXqG0VAZfk+voL558= +github.com/riverqueue/river/riverdriver/riversqlite v0.49.0/go.mod h1:sOSH7dNsGt2UWLFUk7Cy9qNRNkRTswFrTwYf+TJkpeg= +github.com/riverqueue/river/rivershared v0.49.0 h1:wnCYVwftMiu85kT1JrUPKEgujKkBIWoRtSNZJTUtotY= +github.com/riverqueue/river/rivershared v0.49.0/go.mod h1:E8UzQAdDutFT8rVL1wZeNbuphmIS81TgngU7/1F4U5E= +github.com/riverqueue/river/rivertype v0.49.0 h1:3up3P2DtOqnM2yEYSC1xeK9AjrljOtIfl10yZlaF4aM= +github.com/riverqueue/river/rivertype v0.49.0/go.mod h1:XKkcRQR6zm8RR/JQa1Q2ywpj8uXQu21quPa4Lpw1Xhw= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= @@ -54,8 +54,8 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE= github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg= 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/gjson v1.20.0 h1:+agJ3rEzKcCXKDKo0ml26UROcpcAlnZyjq+TjbY5Fto= +github.com/tidwall/gjson v1.20.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= diff --git a/conformance/cmd/generatefixtures/snooze_counters.go b/conformance/cmd/generatefixtures/snooze_counters.go index 2fd8a9ec2..743966840 100644 --- a/conformance/cmd/generatefixtures/snooze_counters.go +++ b/conformance/cmd/generatefixtures/snooze_counters.go @@ -17,9 +17,9 @@ type snoozeCounterCase struct { Name string `json:"name"` } -// makeSnoozeCounters records the `snoozes` count the job executor writes when -// a job with the given metadata snoozes, including how it coerces values that -// aren't integers. +// makeSnoozeCounters records increments of canonical non-negative integer +// counters and initialization when the counter is absent. Recovery from invalid +// counter values is implementation-specific, not part of the shared protocol. func makeSnoozeCounters() snoozeCounters { fixture := snoozeCounters{ Comment: generatedComment("jobexecutor.NextSnoozeCount"), @@ -30,21 +30,10 @@ func makeSnoozeCounters() snoozeCounters { name string }{ {metadata: `{}`, name: "absent"}, - {metadata: `{"snoozes":2}`, name: "integer"}, - {metadata: `{"snoozes":2.9}`, name: "fraction_truncates"}, - {metadata: `{"snoozes":-2.5}`, name: "negative_fraction_truncates_toward_zero"}, - {metadata: `{"snoozes":1e3}`, name: "exponent"}, {metadata: `{"snoozes":9007199254740993}`, name: "beyond_float_precision"}, - {metadata: `{"snoozes":"4"}`, name: "numeric_string"}, - {metadata: `{"snoozes":"-7"}`, name: "negative_numeric_string"}, - {metadata: `{"snoozes":"4.5"}`, name: "fractional_string_is_zero"}, - {metadata: `{"snoozes":" 5"}`, name: "padded_string_is_zero"}, - {metadata: `{"snoozes":"abc"}`, name: "non_numeric_string_is_zero"}, - {metadata: `{"snoozes":true}`, name: "true_is_one"}, - {metadata: `{"snoozes":false}`, name: "false_is_zero"}, - {metadata: `{"snoozes":null}`, name: "null_is_zero"}, - {metadata: `{"snoozes":[3]}`, name: "array_is_zero"}, - {metadata: `{"snoozes":{"count":3}}`, name: "object_is_zero"}, + {metadata: `{"snoozes":2}`, name: "integer"}, + {metadata: `{"snoozes":9223372036854775806}`, name: "largest_increment"}, + {metadata: `{"snoozes":0}`, name: "zero"}, } { fixture.SnoozeCounters = append(fixture.SnoozeCounters, snoozeCounterCase{ ExpectedSnoozes: jobexecutor.NextSnoozeCount([]byte(testCase.metadata)), diff --git a/conformance/go.mod b/conformance/go.mod index 96f3084ea..618066bef 100644 --- a/conformance/go.mod +++ b/conformance/go.mod @@ -17,9 +17,10 @@ require ( ) require ( - github.com/tidwall/gjson v1.19.0 // indirect + github.com/tidwall/gjson v1.20.0 // indirect github.com/tidwall/match v1.2.0 // indirect github.com/tidwall/pretty v1.2.1 // indirect github.com/tidwall/sjson v1.2.5 // indirect golang.org/x/sync v0.23.0 // indirect + golang.org/x/tools v0.50.0 // indirect ) diff --git a/conformance/go.sum b/conformance/go.sum index 6059fab7d..c5709ce4b 100644 --- a/conformance/go.sum +++ b/conformance/go.sum @@ -21,8 +21,8 @@ github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzG github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE= github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg= 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/gjson v1.20.0 h1:+agJ3rEzKcCXKDKo0ml26UROcpcAlnZyjq+TjbY5Fto= +github.com/tidwall/gjson v1.20.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= @@ -41,5 +41,5 @@ golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk= golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0= golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI= golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E= -golang.org/x/tools v0.49.0 h1:3NI7VXzL9+1WZD52Dx2ttoPwD5DWrFGpl9mFZDlmisI= -golang.org/x/tools v0.49.0/go.mod h1:SJNXV9DBKT0UbdttsQjbfJlAE/q+y36++zo3uL3N0Oo= +golang.org/x/tools v0.50.0 h1:c2ifzfcuY7L90lZ2aKd8S4K2NpASF08SZx9ZuJkHmSU= +golang.org/x/tools v0.50.0/go.mod h1:7ulVMw3831Mwi5EZD6RomGyffr4VFjuNYXf2BbCEAV0= diff --git a/go.mod b/go.mod index a5f54031d..01b8e6d55 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,7 @@ require ( github.com/riverqueue/river/rivertype v0.49.0 github.com/robfig/cron/v3 v3.0.1 github.com/stretchr/testify v1.12.1 - github.com/tidwall/gjson v1.19.0 + github.com/tidwall/gjson v1.20.0 github.com/tidwall/sjson v1.2.5 golang.org/x/sync v0.23.0 ) diff --git a/go.sum b/go.sum index a4dbe884c..bd5f48364 100644 --- a/go.sum +++ b/go.sum @@ -10,14 +10,14 @@ github.com/jackc/pgx/v5 v5.11.0/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QII github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/riverqueue/river/riverdriver v0.48.0 h1:7oKPZb3tNjvNyQsi+JapWWREpLnOdUamb9YTnLGY3Lg= -github.com/riverqueue/river/riverdriver v0.48.0/go.mod h1:1MpM6Mf/VqlJp/iF4pzTcPrF/cMsLZ96ARw1tjfmewI= -github.com/riverqueue/river/riverdriver/riverpgxv5 v0.48.0 h1:IIS1ZfusT5zdTC5oUF/MnIthYKjUEyUGBZB3QmuqyOU= -github.com/riverqueue/river/riverdriver/riverpgxv5 v0.48.0/go.mod h1:CZe68VANQs/2fjvw96AJC0XXyptJW7KiGHSFT5XSUvo= -github.com/riverqueue/river/rivershared v0.48.0 h1:PGKa+ke7nqgBqchaSOXtQJ6Ghok5wSWJg3Su5m+m0PU= -github.com/riverqueue/river/rivershared v0.48.0/go.mod h1:FmqY+WQVCot+obBqsTWoPcIOHjYXdacHRMqZNw/oXRk= -github.com/riverqueue/river/rivertype v0.48.0 h1:t9giVes2Y2w9pGaeuRyUyGC7/QoJgTdr2vhY5W1PazU= -github.com/riverqueue/river/rivertype v0.48.0/go.mod h1:XKkcRQR6zm8RR/JQa1Q2ywpj8uXQu21quPa4Lpw1Xhw= +github.com/riverqueue/river/riverdriver v0.49.0 h1:kSykNQJNB7AeG6kudjB0mThV29PvykoOBIFT8ipCHEc= +github.com/riverqueue/river/riverdriver v0.49.0/go.mod h1:rVUuX/fTF2kAiJjpPT+10Lft8n90kvLqjm6b+rFXaVE= +github.com/riverqueue/river/riverdriver/riverpgxv5 v0.49.0 h1:c7YA1plP/nrNS3SEHuKizf4wHx9OQ87Yxuhj3V0eQ50= +github.com/riverqueue/river/riverdriver/riverpgxv5 v0.49.0/go.mod h1:o7zkFstM+Fk+mnlOW2sx92mQW4JswSc6zvN191l8Isg= +github.com/riverqueue/river/rivershared v0.49.0 h1:wnCYVwftMiu85kT1JrUPKEgujKkBIWoRtSNZJTUtotY= +github.com/riverqueue/river/rivershared v0.49.0/go.mod h1:E8UzQAdDutFT8rVL1wZeNbuphmIS81TgngU7/1F4U5E= +github.com/riverqueue/river/rivertype v0.49.0 h1:3up3P2DtOqnM2yEYSC1xeK9AjrljOtIfl10yZlaF4aM= +github.com/riverqueue/river/rivertype v0.49.0/go.mod h1:XKkcRQR6zm8RR/JQa1Q2ywpj8uXQu21quPa4Lpw1Xhw= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -26,8 +26,8 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE= github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg= 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/gjson v1.20.0 h1:+agJ3rEzKcCXKDKo0ml26UROcpcAlnZyjq+TjbY5Fto= +github.com/tidwall/gjson v1.20.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= diff --git a/internal/jobexecutor/job_executor.go b/internal/jobexecutor/job_executor.go index 07a2a0a94..b7b963287 100644 --- a/internal/jobexecutor/job_executor.go +++ b/internal/jobexecutor/job_executor.go @@ -81,9 +81,9 @@ func MetadataUpdatesFromWorkContext(ctx context.Context) (map[string]any, bool) } // NextSnoozeCount returns the snooze count recorded on a job that's snoozed -// again: its metadata's current `snoozes` value plus one. The current value is -// read leniently, so a missing, non-numeric, or malformed count reads as zero -// and a fractional one truncates toward zero. +// again: its metadata's current integer `snoozes` value plus one, starting at +// one when absent. Invalid values are read leniently; their exact coercion is +// not part of the cross-language protocol. func NextSnoozeCount(metadata []byte) int64 { return gjson.GetBytes(metadata, "snoozes").Int() + 1 } diff --git a/internal/jobexecutor/job_executor_test.go b/internal/jobexecutor/job_executor_test.go index a83b19a68..a5dd83dd4 100644 --- a/internal/jobexecutor/job_executor_test.go +++ b/internal/jobexecutor/job_executor_test.go @@ -1480,6 +1480,40 @@ func TestJobExecutor_Execute(t *testing.T) { }) } +func TestNextSnoozeCount_InvalidCounters(t *testing.T) { + t.Parallel() + + for _, testCase := range []struct { + name string + value string + }{ + {name: "Array", value: `[3]`}, + {name: "Boolean", value: `true`}, + {name: "ExcessiveInteger", value: `9223372036854775808`}, + {name: "ExcessiveNumber", value: `1e400`}, + {name: "ExponentString", value: `"1e3"`}, + {name: "FractionalNumber", value: `2.9`}, + {name: "FractionalString", value: `"4.5"`}, + {name: "MaxInteger", value: `9223372036854775807`}, + {name: "NegativeInteger", value: `-2`}, + {name: "Null", value: `null`}, + {name: "NumericString", value: `"4"`}, + {name: "Object", value: `{"count":3}`}, + {name: "String", value: `"abc"`}, + } { + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + metadata := []byte(`{"snoozes":` + testCase.value + `}`) + require.NotPanics(t, func() { + // The return type guarantees an integer. Invalid-value coercion + // is intentionally not pinned to a particular result. + _ = NextSnoozeCount(metadata) + }) + }) + } +} + // // *Func types are copied from the top level River package because they can't be // accessed from here. diff --git a/js/src/runtime.test.ts b/js/src/runtime.test.ts index 24340a2d4..c44c05346 100644 --- a/js/src/runtime.test.ts +++ b/js/src/runtime.test.ts @@ -3425,7 +3425,7 @@ describe("Client runtime", () => { driver.claim = [ { ...fakeJob(), - metadata: { snoozes: "2", user: true }, + metadata: { snoozes: 2, user: true }, }, ]; const definition = defineJob({ kind: "test" }); diff --git a/js/src/runtime/completion-command.test.ts b/js/src/runtime/completion-command.test.ts index cc6861c9c..8166b4526 100644 --- a/js/src/runtime/completion-command.test.ts +++ b/js/src/runtime/completion-command.test.ts @@ -50,20 +50,10 @@ describe("completionCommand", () => { readonly snooze_counters: readonly SnoozeCounterCase[]; }; expect(golden.snooze_counters.length).toBeGreaterThan(0); - const now = Temporal.Instant.from("2026-09-01T00:00:00Z"); - - const results = golden.snooze_counters.map(({ metadata, name }) => { - const command = completionCommand( - job(metadata), - "client", - { outcome: snooze({ seconds: 30 }), status: "succeeded" }, - now, - now, - now, - 5_000 - ); - return [name, numberText(command.metadata?.snoozes)]; - }); + const results = golden.snooze_counters.map(({ metadata, name }) => [ + name, + numberText(snoozeCommand(metadata).metadata?.snoozes), + ]); expect(results).toEqual( golden.snooze_counters.map(({ expected_snoozes, name }) => [ @@ -72,6 +62,31 @@ describe("completionCommand", () => { ]) ); }); + + it.each([ + "2.9", + "-2", + "1e400", + "9223372036854775807", + "9223372036854775808", + '"4"', + '"4.5"', + '"1e3"', + '" 5"', + '"abc"', + "true", + "false", + "null", + "[3]", + '{"count":3}', + ])("recovers safely from invalid snooze counter %s", (raw) => { + const metadata = parseJson(`{"snoozes":${raw}}`) as JsonObject; + const command = snoozeCommand(metadata); + + expect(command.kind).toBe("snooze"); + expect(command.scheduledAt).not.toBeNull(); + expect(BigInt(numberText(command.metadata?.snoozes))).toBeGreaterThan(0n); + }); }); function job(metadata: JsonObject): JobRow { @@ -104,3 +119,16 @@ function numberText(value: JsonValue | undefined): string { if (isExactJsonNumber(value)) return value.rawJSON; throw new Error(`not a JSON number: ${JSON.stringify(value)}`); } + +function snoozeCommand(metadata: JsonObject) { + const now = Temporal.Instant.from("2026-09-01T00:00:00Z"); + return completionCommand( + job(metadata), + "client", + { outcome: snooze({ seconds: 30 }), status: "succeeded" }, + now, + now, + now, + 5_000 + ); +} diff --git a/js/src/runtime/completion-command.ts b/js/src/runtime/completion-command.ts index b9addbb6c..9ea458897 100644 --- a/js/src/runtime/completion-command.ts +++ b/js/src/runtime/completion-command.ts @@ -10,7 +10,8 @@ import { toMilliseconds } from "../internal/duration.js"; import type { JobRow } from "../job.js"; import { exactJsonNumber, - isExactJsonNumber, + isJsonNumber, + jsonNumberToBigInt, type ExactJsonNumber, type JsonValue, } from "../json.js"; @@ -165,56 +166,25 @@ export function completionCommand( } /** - * The snooze count after one more snooze, like River for Go's executor, - * which writes `int(gjson.GetBytes(metadata, "snoozes").Int()) + 1`: `true` - * counts as 1, a decimal integer string as its value, a number truncated - * toward zero, and anything else as 0, with int64 wraparound. + * Increment an integer counter, restarting missing, invalid, or overflowing + * counters at one. Invalid-value recovery is not a cross-language contract. */ function nextSnoozeCount( value: JsonValue | undefined ): ExactJsonNumber | number { - const next = BigInt.asIntN(64, gjsonInt(value) + 1n); - return next >= BigInt(Number.MIN_SAFE_INTEGER) && - next <= BigInt(Number.MAX_SAFE_INTEGER) + let count: bigint; + try { + count = isJsonNumber(value) ? jsonNumberToBigInt(value) : 0n; + } catch { + count = 0n; + } + const next = + count >= 0n && count < 9_223_372_036_854_775_807n ? count + 1n : 1n; + return next <= BigInt(Number.MAX_SAFE_INTEGER) ? Number(next) : exactJsonNumber(next.toString(10)); } -/** gjson's `Result.Int()` for a JSON value. */ -function gjsonInt(value: JsonValue | undefined): bigint { - if (value === true) return 1n; - if (typeof value === "string") return gjsonParseInt(value) ?? 0n; - if (typeof value !== "number" && !isExactJsonNumber(value)) return 0n; - const raw = typeof value === "number" ? String(value) : value.rawJSON; - const float = typeof value === "number" ? value : Number(value.rawJSON); - // gjson's safeInt, then its parse of the raw integer text, then Go's - // float conversion, which saturates out of range on arm64. - if (Math.abs(float) <= Number.MAX_SAFE_INTEGER) { - return BigInt(Math.trunc(float)); - } - const parsed = gjsonParseInt(raw); - if (parsed !== undefined) return parsed; - if (Number.isNaN(float)) return 0n; - if (float >= 2 ** 63) return BigInt.asIntN(64, (1n << 63n) - 1n); - if (float <= -(2 ** 63)) return -(1n << 63n); - return BigInt(Math.trunc(float)); -} - -/** - * gjson's `parseInt`: an optional `-` then decimal digits only, wrapping - * like int64 arithmetic; undefined for anything else. - */ -function gjsonParseInt(text: string): bigint | undefined { - const negative = text.startsWith("-"); - const digits = negative ? text.slice(1) : text; - if (!/^[0-9]+$/.test(digits)) return undefined; - let result = 0n; - for (const digit of digits) { - result = BigInt.asIntN(64, result * 10n + BigInt(digit)); - } - return negative ? BigInt.asIntN(64, -result) : result; -} - /** The event announcing a committed completion, by the job's new state. */ export function completionEventKind( requested: JobCompletionCommand["kind"], diff --git a/riverdriver/go.sum b/riverdriver/go.sum index 9572c51b4..f3e1d783f 100644 --- a/riverdriver/go.sum +++ b/riverdriver/go.sum @@ -1,5 +1,5 @@ -github.com/riverqueue/river/rivertype v0.48.0 h1:t9giVes2Y2w9pGaeuRyUyGC7/QoJgTdr2vhY5W1PazU= -github.com/riverqueue/river/rivertype v0.48.0/go.mod h1:XKkcRQR6zm8RR/JQa1Q2ywpj8uXQu21quPa4Lpw1Xhw= +github.com/riverqueue/river/rivertype v0.49.0 h1:3up3P2DtOqnM2yEYSC1xeK9AjrljOtIfl10yZlaF4aM= +github.com/riverqueue/river/rivertype v0.49.0/go.mod h1:XKkcRQR6zm8RR/JQa1Q2ywpj8uXQu21quPa4Lpw1Xhw= github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE= github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg= go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= diff --git a/riverdriver/riverdatabasesql/go.mod b/riverdriver/riverdatabasesql/go.mod index 9e5ea6d87..72680110f 100644 --- a/riverdriver/riverdatabasesql/go.mod +++ b/riverdriver/riverdatabasesql/go.mod @@ -19,7 +19,7 @@ require ( github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect - github.com/tidwall/gjson v1.19.0 // indirect + github.com/tidwall/gjson v1.20.0 // indirect github.com/tidwall/match v1.2.0 // indirect github.com/tidwall/pretty v1.2.1 // indirect github.com/tidwall/sjson v1.2.5 // indirect diff --git a/riverdriver/riverdatabasesql/go.sum b/riverdriver/riverdatabasesql/go.sum index d92bd559c..2bab3585d 100644 --- a/riverdriver/riverdatabasesql/go.sum +++ b/riverdriver/riverdatabasesql/go.sum @@ -12,16 +12,16 @@ github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFr github.com/lib/pq v1.12.3 h1:tTWxr2YLKwIvK90ZXEw8GP7UFHtcbTtty8zsI+YjrfQ= github.com/lib/pq v1.12.3/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/riverqueue/river v0.48.0 h1:SJwDBsNY/GuSdcW4d7xf2BeRrC1XThqd2Sns+i8ufQU= -github.com/riverqueue/river v0.48.0/go.mod h1:p0oR3A4EPF9IkyKhFiyPPRWgHRP4lF2zqtcPWs39Kp0= -github.com/riverqueue/river/riverdriver v0.48.0 h1:7oKPZb3tNjvNyQsi+JapWWREpLnOdUamb9YTnLGY3Lg= -github.com/riverqueue/river/riverdriver v0.48.0/go.mod h1:1MpM6Mf/VqlJp/iF4pzTcPrF/cMsLZ96ARw1tjfmewI= -github.com/riverqueue/river/riverdriver/riverpgxv5 v0.48.0 h1:IIS1ZfusT5zdTC5oUF/MnIthYKjUEyUGBZB3QmuqyOU= -github.com/riverqueue/river/riverdriver/riverpgxv5 v0.48.0/go.mod h1:CZe68VANQs/2fjvw96AJC0XXyptJW7KiGHSFT5XSUvo= -github.com/riverqueue/river/rivershared v0.48.0 h1:PGKa+ke7nqgBqchaSOXtQJ6Ghok5wSWJg3Su5m+m0PU= -github.com/riverqueue/river/rivershared v0.48.0/go.mod h1:FmqY+WQVCot+obBqsTWoPcIOHjYXdacHRMqZNw/oXRk= -github.com/riverqueue/river/rivertype v0.48.0 h1:t9giVes2Y2w9pGaeuRyUyGC7/QoJgTdr2vhY5W1PazU= -github.com/riverqueue/river/rivertype v0.48.0/go.mod h1:XKkcRQR6zm8RR/JQa1Q2ywpj8uXQu21quPa4Lpw1Xhw= +github.com/riverqueue/river v0.49.0 h1:JUCLFgregbX1Wu+bSTHCjF/WHGdrwGbtz/ddInHfeb0= +github.com/riverqueue/river v0.49.0/go.mod h1:USYb57gpMBXLQm2l7poskAalOPaf5uHjSZgp2AOoYDw= +github.com/riverqueue/river/riverdriver v0.49.0 h1:kSykNQJNB7AeG6kudjB0mThV29PvykoOBIFT8ipCHEc= +github.com/riverqueue/river/riverdriver v0.49.0/go.mod h1:rVUuX/fTF2kAiJjpPT+10Lft8n90kvLqjm6b+rFXaVE= +github.com/riverqueue/river/riverdriver/riverpgxv5 v0.49.0 h1:c7YA1plP/nrNS3SEHuKizf4wHx9OQ87Yxuhj3V0eQ50= +github.com/riverqueue/river/riverdriver/riverpgxv5 v0.49.0/go.mod h1:o7zkFstM+Fk+mnlOW2sx92mQW4JswSc6zvN191l8Isg= +github.com/riverqueue/river/rivershared v0.49.0 h1:wnCYVwftMiu85kT1JrUPKEgujKkBIWoRtSNZJTUtotY= +github.com/riverqueue/river/rivershared v0.49.0/go.mod h1:E8UzQAdDutFT8rVL1wZeNbuphmIS81TgngU7/1F4U5E= +github.com/riverqueue/river/rivertype v0.49.0 h1:3up3P2DtOqnM2yEYSC1xeK9AjrljOtIfl10yZlaF4aM= +github.com/riverqueue/river/rivertype v0.49.0/go.mod h1:XKkcRQR6zm8RR/JQa1Q2ywpj8uXQu21quPa4Lpw1Xhw= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -30,8 +30,8 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE= github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg= 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/gjson v1.20.0 h1:+agJ3rEzKcCXKDKo0ml26UROcpcAlnZyjq+TjbY5Fto= +github.com/tidwall/gjson v1.20.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= diff --git a/riverdriver/riverdrivertest/go.mod b/riverdriver/riverdrivertest/go.mod index 15b4130f0..76f570f98 100644 --- a/riverdriver/riverdrivertest/go.mod +++ b/riverdriver/riverdrivertest/go.mod @@ -17,7 +17,7 @@ require ( github.com/riverqueue/river/rivershared v0.49.0 github.com/riverqueue/river/rivertype v0.49.0 github.com/stretchr/testify v1.12.1 - github.com/tidwall/gjson v1.19.0 + github.com/tidwall/gjson v1.20.0 github.com/tidwall/sjson v1.2.5 github.com/tursodatabase/libsql-client-go v0.0.0-20260528064733-9d5d30a29a60 golang.org/x/text v0.42.0 diff --git a/riverdriver/riverdrivertest/go.sum b/riverdriver/riverdrivertest/go.sum index 36c944e8f..7aaea5cf6 100644 --- a/riverdriver/riverdrivertest/go.sum +++ b/riverdriver/riverdrivertest/go.sum @@ -36,20 +36,20 @@ github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJm github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= -github.com/riverqueue/river v0.48.0 h1:SJwDBsNY/GuSdcW4d7xf2BeRrC1XThqd2Sns+i8ufQU= -github.com/riverqueue/river v0.48.0/go.mod h1:p0oR3A4EPF9IkyKhFiyPPRWgHRP4lF2zqtcPWs39Kp0= -github.com/riverqueue/river/riverdriver v0.48.0 h1:7oKPZb3tNjvNyQsi+JapWWREpLnOdUamb9YTnLGY3Lg= -github.com/riverqueue/river/riverdriver v0.48.0/go.mod h1:1MpM6Mf/VqlJp/iF4pzTcPrF/cMsLZ96ARw1tjfmewI= -github.com/riverqueue/river/riverdriver/riverdatabasesql v0.48.0 h1:nodpSDGX4Atw72f07UvgkakeYHLAk/tS1XsW9n40Pco= -github.com/riverqueue/river/riverdriver/riverdatabasesql v0.48.0/go.mod h1:0aA1LCyqnZ8hKMBqrwGVTkO5Ey/BwT1fDsFrm+PdbYs= -github.com/riverqueue/river/riverdriver/riverpgxv5 v0.48.0 h1:IIS1ZfusT5zdTC5oUF/MnIthYKjUEyUGBZB3QmuqyOU= -github.com/riverqueue/river/riverdriver/riverpgxv5 v0.48.0/go.mod h1:CZe68VANQs/2fjvw96AJC0XXyptJW7KiGHSFT5XSUvo= -github.com/riverqueue/river/riverdriver/riversqlite v0.48.0 h1:6qdp9pVIlw7aPHYo3W96Vfk750j1CIKhzeUCLP0/U/U= -github.com/riverqueue/river/riverdriver/riversqlite v0.48.0/go.mod h1:g9uElXcqkIwfq7/ebVz3bL2vD7i9mLSlTltqK5sXD2I= -github.com/riverqueue/river/rivershared v0.48.0 h1:PGKa+ke7nqgBqchaSOXtQJ6Ghok5wSWJg3Su5m+m0PU= -github.com/riverqueue/river/rivershared v0.48.0/go.mod h1:FmqY+WQVCot+obBqsTWoPcIOHjYXdacHRMqZNw/oXRk= -github.com/riverqueue/river/rivertype v0.48.0 h1:t9giVes2Y2w9pGaeuRyUyGC7/QoJgTdr2vhY5W1PazU= -github.com/riverqueue/river/rivertype v0.48.0/go.mod h1:XKkcRQR6zm8RR/JQa1Q2ywpj8uXQu21quPa4Lpw1Xhw= +github.com/riverqueue/river v0.49.0 h1:JUCLFgregbX1Wu+bSTHCjF/WHGdrwGbtz/ddInHfeb0= +github.com/riverqueue/river v0.49.0/go.mod h1:USYb57gpMBXLQm2l7poskAalOPaf5uHjSZgp2AOoYDw= +github.com/riverqueue/river/riverdriver v0.49.0 h1:kSykNQJNB7AeG6kudjB0mThV29PvykoOBIFT8ipCHEc= +github.com/riverqueue/river/riverdriver v0.49.0/go.mod h1:rVUuX/fTF2kAiJjpPT+10Lft8n90kvLqjm6b+rFXaVE= +github.com/riverqueue/river/riverdriver/riverdatabasesql v0.49.0 h1:sQESwukrQUqNMzIt7vOwfkJ/1C+F8A5DhYyF/ahBskI= +github.com/riverqueue/river/riverdriver/riverdatabasesql v0.49.0/go.mod h1:aIXWjvA3q5zawSMML/dIJgI1bK2da+rbpifY/dUgPWQ= +github.com/riverqueue/river/riverdriver/riverpgxv5 v0.49.0 h1:c7YA1plP/nrNS3SEHuKizf4wHx9OQ87Yxuhj3V0eQ50= +github.com/riverqueue/river/riverdriver/riverpgxv5 v0.49.0/go.mod h1:o7zkFstM+Fk+mnlOW2sx92mQW4JswSc6zvN191l8Isg= +github.com/riverqueue/river/riverdriver/riversqlite v0.49.0 h1:DfATYWIuQ4bKjhhqj/XYn3ZGpluXqG0VAZfk+voL558= +github.com/riverqueue/river/riverdriver/riversqlite v0.49.0/go.mod h1:sOSH7dNsGt2UWLFUk7Cy9qNRNkRTswFrTwYf+TJkpeg= +github.com/riverqueue/river/rivershared v0.49.0 h1:wnCYVwftMiu85kT1JrUPKEgujKkBIWoRtSNZJTUtotY= +github.com/riverqueue/river/rivershared v0.49.0/go.mod h1:E8UzQAdDutFT8rVL1wZeNbuphmIS81TgngU7/1F4U5E= +github.com/riverqueue/river/rivertype v0.49.0 h1:3up3P2DtOqnM2yEYSC1xeK9AjrljOtIfl10yZlaF4aM= +github.com/riverqueue/river/rivertype v0.49.0/go.mod h1:XKkcRQR6zm8RR/JQa1Q2ywpj8uXQu21quPa4Lpw1Xhw= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -58,8 +58,8 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE= github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg= 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/gjson v1.20.0 h1:+agJ3rEzKcCXKDKo0ml26UROcpcAlnZyjq+TjbY5Fto= +github.com/tidwall/gjson v1.20.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= diff --git a/riverdriver/riverpgxv5/go.sum b/riverdriver/riverpgxv5/go.sum index cab8ffa85..309925a9c 100644 --- a/riverdriver/riverpgxv5/go.sum +++ b/riverdriver/riverpgxv5/go.sum @@ -8,14 +8,14 @@ github.com/jackc/pgx/v5 v5.11.0/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QII github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/riverqueue/river v0.48.0 h1:SJwDBsNY/GuSdcW4d7xf2BeRrC1XThqd2Sns+i8ufQU= -github.com/riverqueue/river v0.48.0/go.mod h1:p0oR3A4EPF9IkyKhFiyPPRWgHRP4lF2zqtcPWs39Kp0= -github.com/riverqueue/river/riverdriver v0.48.0 h1:7oKPZb3tNjvNyQsi+JapWWREpLnOdUamb9YTnLGY3Lg= -github.com/riverqueue/river/riverdriver v0.48.0/go.mod h1:1MpM6Mf/VqlJp/iF4pzTcPrF/cMsLZ96ARw1tjfmewI= -github.com/riverqueue/river/rivershared v0.48.0 h1:PGKa+ke7nqgBqchaSOXtQJ6Ghok5wSWJg3Su5m+m0PU= -github.com/riverqueue/river/rivershared v0.48.0/go.mod h1:FmqY+WQVCot+obBqsTWoPcIOHjYXdacHRMqZNw/oXRk= -github.com/riverqueue/river/rivertype v0.48.0 h1:t9giVes2Y2w9pGaeuRyUyGC7/QoJgTdr2vhY5W1PazU= -github.com/riverqueue/river/rivertype v0.48.0/go.mod h1:XKkcRQR6zm8RR/JQa1Q2ywpj8uXQu21quPa4Lpw1Xhw= +github.com/riverqueue/river v0.49.0 h1:JUCLFgregbX1Wu+bSTHCjF/WHGdrwGbtz/ddInHfeb0= +github.com/riverqueue/river v0.49.0/go.mod h1:USYb57gpMBXLQm2l7poskAalOPaf5uHjSZgp2AOoYDw= +github.com/riverqueue/river/riverdriver v0.49.0 h1:kSykNQJNB7AeG6kudjB0mThV29PvykoOBIFT8ipCHEc= +github.com/riverqueue/river/riverdriver v0.49.0/go.mod h1:rVUuX/fTF2kAiJjpPT+10Lft8n90kvLqjm6b+rFXaVE= +github.com/riverqueue/river/rivershared v0.49.0 h1:wnCYVwftMiu85kT1JrUPKEgujKkBIWoRtSNZJTUtotY= +github.com/riverqueue/river/rivershared v0.49.0/go.mod h1:E8UzQAdDutFT8rVL1wZeNbuphmIS81TgngU7/1F4U5E= +github.com/riverqueue/river/rivertype v0.49.0 h1:3up3P2DtOqnM2yEYSC1xeK9AjrljOtIfl10yZlaF4aM= +github.com/riverqueue/river/rivertype v0.49.0/go.mod h1:XKkcRQR6zm8RR/JQa1Q2ywpj8uXQu21quPa4Lpw1Xhw= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= diff --git a/riverdriver/riversqlite/go.sum b/riverdriver/riversqlite/go.sum index 9fe23a878..82fe5fcf5 100644 --- a/riverdriver/riversqlite/go.sum +++ b/riverdriver/riversqlite/go.sum @@ -6,16 +6,16 @@ github.com/jackc/pgx/v5 v5.11.0 h1:IzBBtyK9AHqf98cctWFifYSci2hgQR/cd56wB4p+ogg= github.com/jackc/pgx/v5 v5.11.0/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= -github.com/riverqueue/river v0.48.0 h1:SJwDBsNY/GuSdcW4d7xf2BeRrC1XThqd2Sns+i8ufQU= -github.com/riverqueue/river v0.48.0/go.mod h1:p0oR3A4EPF9IkyKhFiyPPRWgHRP4lF2zqtcPWs39Kp0= -github.com/riverqueue/river/riverdriver v0.48.0 h1:7oKPZb3tNjvNyQsi+JapWWREpLnOdUamb9YTnLGY3Lg= -github.com/riverqueue/river/riverdriver v0.48.0/go.mod h1:1MpM6Mf/VqlJp/iF4pzTcPrF/cMsLZ96ARw1tjfmewI= -github.com/riverqueue/river/riverdriver/riverpgxv5 v0.48.0 h1:IIS1ZfusT5zdTC5oUF/MnIthYKjUEyUGBZB3QmuqyOU= -github.com/riverqueue/river/riverdriver/riverpgxv5 v0.48.0/go.mod h1:CZe68VANQs/2fjvw96AJC0XXyptJW7KiGHSFT5XSUvo= -github.com/riverqueue/river/rivershared v0.48.0 h1:PGKa+ke7nqgBqchaSOXtQJ6Ghok5wSWJg3Su5m+m0PU= -github.com/riverqueue/river/rivershared v0.48.0/go.mod h1:FmqY+WQVCot+obBqsTWoPcIOHjYXdacHRMqZNw/oXRk= -github.com/riverqueue/river/rivertype v0.48.0 h1:t9giVes2Y2w9pGaeuRyUyGC7/QoJgTdr2vhY5W1PazU= -github.com/riverqueue/river/rivertype v0.48.0/go.mod h1:XKkcRQR6zm8RR/JQa1Q2ywpj8uXQu21quPa4Lpw1Xhw= +github.com/riverqueue/river v0.49.0 h1:JUCLFgregbX1Wu+bSTHCjF/WHGdrwGbtz/ddInHfeb0= +github.com/riverqueue/river v0.49.0/go.mod h1:USYb57gpMBXLQm2l7poskAalOPaf5uHjSZgp2AOoYDw= +github.com/riverqueue/river/riverdriver v0.49.0 h1:kSykNQJNB7AeG6kudjB0mThV29PvykoOBIFT8ipCHEc= +github.com/riverqueue/river/riverdriver v0.49.0/go.mod h1:rVUuX/fTF2kAiJjpPT+10Lft8n90kvLqjm6b+rFXaVE= +github.com/riverqueue/river/riverdriver/riverpgxv5 v0.49.0 h1:c7YA1plP/nrNS3SEHuKizf4wHx9OQ87Yxuhj3V0eQ50= +github.com/riverqueue/river/riverdriver/riverpgxv5 v0.49.0/go.mod h1:o7zkFstM+Fk+mnlOW2sx92mQW4JswSc6zvN191l8Isg= +github.com/riverqueue/river/rivershared v0.49.0 h1:wnCYVwftMiu85kT1JrUPKEgujKkBIWoRtSNZJTUtotY= +github.com/riverqueue/river/rivershared v0.49.0/go.mod h1:E8UzQAdDutFT8rVL1wZeNbuphmIS81TgngU7/1F4U5E= +github.com/riverqueue/river/rivertype v0.49.0 h1:3up3P2DtOqnM2yEYSC1xeK9AjrljOtIfl10yZlaF4aM= +github.com/riverqueue/river/rivertype v0.49.0/go.mod h1:XKkcRQR6zm8RR/JQa1Q2ywpj8uXQu21quPa4Lpw1Xhw= github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE= github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= diff --git a/rivershared/go.mod b/rivershared/go.mod index 74919d7d0..47deb576a 100644 --- a/rivershared/go.mod +++ b/rivershared/go.mod @@ -11,7 +11,7 @@ require ( github.com/riverqueue/river/riverdriver/riverpgxv5 v0.49.0 github.com/riverqueue/river/rivertype v0.49.0 github.com/stretchr/testify v1.12.1 - github.com/tidwall/gjson v1.19.0 + github.com/tidwall/gjson v1.20.0 go.uber.org/goleak v1.3.0 golang.org/x/mod v0.41.0 ) diff --git a/rivershared/go.sum b/rivershared/go.sum index ed094b957..455c40d5b 100644 --- a/rivershared/go.sum +++ b/rivershared/go.sum @@ -10,21 +10,21 @@ github.com/jackc/pgx/v5 v5.11.0/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QII github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/riverqueue/river v0.48.0 h1:SJwDBsNY/GuSdcW4d7xf2BeRrC1XThqd2Sns+i8ufQU= -github.com/riverqueue/river v0.48.0/go.mod h1:p0oR3A4EPF9IkyKhFiyPPRWgHRP4lF2zqtcPWs39Kp0= -github.com/riverqueue/river/riverdriver v0.48.0 h1:7oKPZb3tNjvNyQsi+JapWWREpLnOdUamb9YTnLGY3Lg= -github.com/riverqueue/river/riverdriver v0.48.0/go.mod h1:1MpM6Mf/VqlJp/iF4pzTcPrF/cMsLZ96ARw1tjfmewI= -github.com/riverqueue/river/riverdriver/riverpgxv5 v0.48.0 h1:IIS1ZfusT5zdTC5oUF/MnIthYKjUEyUGBZB3QmuqyOU= -github.com/riverqueue/river/riverdriver/riverpgxv5 v0.48.0/go.mod h1:CZe68VANQs/2fjvw96AJC0XXyptJW7KiGHSFT5XSUvo= -github.com/riverqueue/river/rivertype v0.48.0 h1:t9giVes2Y2w9pGaeuRyUyGC7/QoJgTdr2vhY5W1PazU= -github.com/riverqueue/river/rivertype v0.48.0/go.mod h1:XKkcRQR6zm8RR/JQa1Q2ywpj8uXQu21quPa4Lpw1Xhw= +github.com/riverqueue/river v0.49.0 h1:JUCLFgregbX1Wu+bSTHCjF/WHGdrwGbtz/ddInHfeb0= +github.com/riverqueue/river v0.49.0/go.mod h1:USYb57gpMBXLQm2l7poskAalOPaf5uHjSZgp2AOoYDw= +github.com/riverqueue/river/riverdriver v0.49.0 h1:kSykNQJNB7AeG6kudjB0mThV29PvykoOBIFT8ipCHEc= +github.com/riverqueue/river/riverdriver v0.49.0/go.mod h1:rVUuX/fTF2kAiJjpPT+10Lft8n90kvLqjm6b+rFXaVE= +github.com/riverqueue/river/riverdriver/riverpgxv5 v0.49.0 h1:c7YA1plP/nrNS3SEHuKizf4wHx9OQ87Yxuhj3V0eQ50= +github.com/riverqueue/river/riverdriver/riverpgxv5 v0.49.0/go.mod h1:o7zkFstM+Fk+mnlOW2sx92mQW4JswSc6zvN191l8Isg= +github.com/riverqueue/river/rivertype v0.49.0 h1:3up3P2DtOqnM2yEYSC1xeK9AjrljOtIfl10yZlaF4aM= +github.com/riverqueue/river/rivertype v0.49.0/go.mod h1:XKkcRQR6zm8RR/JQa1Q2ywpj8uXQu21quPa4Lpw1Xhw= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE= github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg= -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/gjson v1.20.0 h1:+agJ3rEzKcCXKDKo0ml26UROcpcAlnZyjq+TjbY5Fto= +github.com/tidwall/gjson v1.20.0/go.mod h1:V37/opeE/JbLUOfH0QTXiNez2l0RUjYUhpT4szFQAfc= 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.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= diff --git a/rust/riverqueue/src/client/executor.rs b/rust/riverqueue/src/client/executor.rs index 4df75d75f..07520ee97 100644 --- a/rust/riverqueue/src/client/executor.rs +++ b/rust/riverqueue/src/client/executor.rs @@ -16,8 +16,9 @@ use crate::client::{ClientInner, PeerLedger, peers}; use crate::error::{Chain, panic_message}; use crate::extension::{WorkEndpoint, WorkNext}; use crate::{ - AttemptError, BoxError, Client, Error, ErrorHandlerDecision, JobEventKind, JobRow, JobState, - PanicError, WorkCancelled, WorkContext, WorkError, WorkOutcome, WorkResult, WorkerTimeout, + AttemptError, BoxError, Client, Error, ErrorHandlerDecision, JobEventKind, JobMetadata, JobRow, + JobState, PanicError, WorkCancelled, WorkContext, WorkError, WorkOutcome, WorkResult, + WorkerTimeout, }; /// Runs one claimed job's attempt and persists its result. @@ -547,7 +548,7 @@ pub(super) async fn persist_result( JobState::Scheduled }; let mut metadata = metadata_updates; - let snoozes = go_json_int(row.metadata.get_raw("snoozes")).wrapping_add(1); + let snoozes = next_snooze_count(&row.metadata); metadata.insert("snoozes".to_owned(), Value::from(snoozes)); ( state, @@ -694,64 +695,24 @@ pub(crate) fn default_retry_delay(row: &JobRow, now: DateTime, seed: u64) - .min(Duration::from_nanos(MAX_RETRY_NANOS)) } -/// Coerces a metadata value to an integer exactly like Go's `gjson.Int`, which -/// the Go executor uses to read the `snoozes` counter. Numbers truncate toward -/// zero, numeric strings of optional sign and digits parse, `true` is one, and -/// everything else is zero. -fn go_json_int(value: Option<&serde_json::value::RawValue>) -> i64 { - fn parse_digits(text: &str) -> Option { - let (negative, digits) = text - .strip_prefix('-') - .map_or((false, text), |digits| (true, digits)); - if digits.is_empty() { - return None; - } - let mut number = 0_i64; - for byte in digits.bytes() { - if !byte.is_ascii_digit() { - return None; - } - number = number.wrapping_mul(10).wrapping_add(i64::from(byte - b'0')); - } - Some(if negative { - number.wrapping_neg() - } else { - number - }) - } - - const MAX_SAFE_INTEGER: f64 = 9_007_199_254_740_991.0; - match value.map(serde_json::value::RawValue::get) { - Some("true") => 1, - Some(raw) if raw.starts_with('"') => serde_json::from_str::(raw) - .ok() - .and_then(|text| parse_digits(&text)) - .unwrap_or(0), - Some(raw) if raw.starts_with(['-', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9']) => { - let float = raw.parse::().unwrap_or(0.0); - if (-MAX_SAFE_INTEGER..=MAX_SAFE_INTEGER).contains(&float) { - #[expect( - clippy::cast_possible_truncation, - reason = "Go truncates safe floats toward zero" - )] - return float as i64; - } - #[expect( - clippy::cast_possible_truncation, - reason = "Go falls back to a float conversion for huge numbers" - )] - parse_digits(raw).unwrap_or(float as i64) - } - _ => 0, - } +/// Increments an integer counter, restarting missing, invalid, or overflowing +/// counters at one. Invalid-value recovery is not a cross-language contract. +fn next_snooze_count(metadata: &JobMetadata) -> i64 { + metadata + .get::("snoozes") + .ok() + .flatten() + .filter(|count| *count >= 0) + .and_then(|count| count.checked_add(1)) + .unwrap_or(1) } #[cfg(test)] -mod go_json_int_tests { +mod snooze_counter_tests { use crate::{JobMetadata, conformance::read_fixture}; use serde::Deserialize; - use super::go_json_int; + use super::next_snooze_count; #[derive(Deserialize)] struct Fixture { @@ -765,13 +726,38 @@ mod go_json_int_tests { name: String, } + #[test] + fn invalid_counters_recover_without_panicking() { + for raw in [ + "2.9", + "-2", + "1e3", + "1e400", + "9223372036854775807", + "9223372036854775808", + r#""4""#, + r#""4.5""#, + r#""1e3""#, + r#"" 5""#, + r#""abc""#, + "true", + "false", + "null", + "[3]", + r#"{"count":3}"#, + ] { + let metadata: JobMetadata = format!(r#"{{"snoozes":{raw}}}"#).parse().unwrap(); + assert!(next_snooze_count(&metadata) > 0, "{raw}"); + } + } + #[test] fn snooze_counter_matches_go_fixture() { let fixture: Fixture = serde_json::from_str(&read_fixture("snooze_counters.json")).unwrap(); assert!(!fixture.snooze_counters.is_empty()); for case in fixture.snooze_counters { assert_eq!( - go_json_int(case.metadata.get_raw("snoozes")).wrapping_add(1), + next_snooze_count(&case.metadata), case.expected_snoozes, "{}", case.name