You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Support UniqueOpts without JobStateRunning in ByState #1183
This is a followup to this issue and my comment here. I want to make a stand-alone feature request. So copying my comment here:
I am a newcomer to River, but explanation why JobStateRunning is required is surprising to me based on the rest of the documentation. UniqueOpts is part of InsertOpts and that to me means that it only controls when the job can be inserted, not how it can transition later on. For my use case (I described in #1178) I care that (transitionally) I can skip adding additional job if one is already pending, but not running. Jobs are designed so that they could run 100 of them in parallel, they are also idempotent, it is just that it is unnecessary to have more then one at any given time being scheduled to run.
About how many jobs are running in parallel, my understanding is that this is what concurrency limits are for, not insert options.
To me this is also surprising because when as a user I insert a job, it is never in any other state than an initial state. I cannot insert a job in the "running" state. That makes no sense. Only River can move job to a running state. So why would "InsertOps" control how can River move jobs between states? To me "InsertOps" is only about the user - can I as a user insert a job if any job is in a pending state but not yet running?
In fact, to me this is a feature: River should be able to transition any available job to running state. But if River does that, then my insertion (where "running" state is not among UniqueOpts) should transactionally succeed. And this is exactly the behavior I would need. If at any given moment job is not yet running or even not being started to be run, skip adding the job. It is unnecessary. The job which will shortly run will do the job. But if the job is already running, then schedule it. Because it might be that the job running will miss a bit of work.
We've been looking at this as a design question rather than a bug, and ran an executable arbitration prototype locally to compare possible contracts, in case it's useful for deciding a direction here. Disclosure: this comment was drafted and posted with AI assistance; everything below is limited to what we actually ran, and it's hypothetical research — nothing is submitted as an implementation.
Starting point. We treat the current JobStateRunning requirement in UniqueOpts.ByState as intentional design (consistent with the doc correction merged in #1179), and did not find a production bug in current behavior.
Two liveness gaps we reproduced in a hypothetical "strict placeholder" adapter (experimental SQL protocol on a local checkout, not River code). In the scenario this issue describes, a running job reentering waiting while another job holds the waiting reservation means the old obligation gets superseded into a successor. Then:
If that successor is cancelled or exhausts its retries, no executable bearer of the original obligation remains.
An immutable CONFLICT receipt keeps replaying CONFLICT after the incumbent completes, and nothing durable owns scheduling a new operation at that point.
Three contract options we compared:
A. Strict placeholder + named coverage/deferred-obligation adapter. Keep one indexed waiting row per key; allow supersession only under an explicit obligation-set contract (we only defined it for exact set-inclusion evidence, not opaque idempotent args). Closing the two gaps above additionally requires an inherited-cancellation policy and a durable replay actor.
B. Insertion-admission-only. Release uniqueness membership permanently on first fetch and let old rows retry outside the insertion reservation, allowing multiple waiting rows per key (one indexed). Simplest contract, but it gives up strict one-waiting-job occupancy and needs a persisted mode/version and a coordinated producer/worker rollout.
C. Keep the current constraints, or specify an explicit deferred-state protocol (blocked state + reservation + replay actor) without A's coverage promise.
What we validated in the A direction (Postgres 17.6 and SQLite 3.54.1, real concurrent transactions): under these named experimental semantics, cancelling the successor preserved the inherited obligation and it could still be scheduled and executed; a persistent actor rebuilt in a new process after a simulated crash still made progress; and a caller rollback left no partially-committed intent (each command is savepoint-isolated inside the caller's transaction). Real costs remain: extra durable protocol tables, exclusive cooperating writers, and no GC/capacity/fairness guarantees. We have not attempted the cross-language parity or migration work a real change here would require.
Curious which of these directions (if any) feels worth pursuing to you — in particular whether B's multiple waiting rows are acceptable, or whether an inherited-obligation/replay-ownership contract like A/C is something River wants to define at all.
Activity
PoisonGodv commentedon Nov 4, 2026
We've been looking at this as a design question rather than a bug, and ran an executable arbitration prototype locally to compare possible contracts, in case it's useful for deciding a direction here. Disclosure: this comment was drafted and posted with AI assistance; everything below is limited to what we actually ran, and it's hypothetical research — nothing is submitted as an implementation.
Starting point. We treat the current
JobStateRunningrequirement inUniqueOpts.ByStateas intentional design (consistent with the doc correction merged in #1179), and did not find a production bug in current behavior.Two liveness gaps we reproduced in a hypothetical "strict placeholder" adapter (experimental SQL protocol on a local checkout, not River code). In the scenario this issue describes, a running job reentering waiting while another job holds the waiting reservation means the old obligation gets superseded into a successor. Then:
Three contract options we compared:
What we validated in the A direction (Postgres 17.6 and SQLite 3.54.1, real concurrent transactions): under these named experimental semantics, cancelling the successor preserved the inherited obligation and it could still be scheduled and executed; a persistent actor rebuilt in a new process after a simulated crash still made progress; and a caller rollback left no partially-committed intent (each command is savepoint-isolated inside the caller's transaction). Real costs remain: extra durable protocol tables, exclusive cooperating writers, and no GC/capacity/fairness guarantees. We have not attempted the cross-language parity or migration work a real change here would require.
Curious which of these directions (if any) feels worth pursuing to you — in particular whether B's multiple waiting rows are acceptable, or whether an inherited-obligation/replay-ownership contract like A/C is something River wants to define at all.