Unable to enqueue changeset to be reconciled while it's being reconciled
Created by: mrnugget
Status quo
In order to enqueue a changeset
to be picked up by the reconciler, we set its reconciler_state
column to queued
.
The reconciler then dequeues the changeset by setting the reconciler_state
to processing
:
Once it's done, it sets the state
to completed
or errored
.
Problem
We want to move the closing of changesets into the background (https://github.com/sourcegraph/sourcegraph/issues/12644) by setting a "please close this" boolean on the changeset
and enqueuing the changeset so that the reconciler picks it up and closes it.
We also want to allow this while a changeset
is being processed by the reconciler, because the reconciler could — in the future — always be processing a changeset and that shouldn't stop the user from marking a changeset as "to be closed". See https://github.com/sourcegraph/sourcegraph/pull/12782#issue-463915652 for more context.
But here is the problem: if we enqueue a changeset while it's being processed by setting reconciler_state = 'queued'
then that value will simply be overwritten by the reconciler once it's done with the current processing.
In other words: we can't enqueue a changeset to be processed again while it's being processed.
That's due to the architecture of the underlying dbworker/store
, which assumes that every job is a one-off job that ends in a terminal state and is not re-enqueued again.
Possible Solution 1
We split the reconciler_state
up into two fields:
enqueued: bool
processing_state: string
In order to enqueue a changeset, we set enqueued = TRUE
.
We then need to change the dbworker/store
so that the selectCandidateQuery
allows us to set these fields, like this (see the selectCandidateQuery
above):
WITH candidate AS (
SELECT {id} FROM %s
WHERE
-- This is new
({state} = 'queued' OR (enqueued IS TRUE)) AND
({process_after} IS NULL OR {process_after} <= NOW())
%s
ORDER BY %s
FOR UPDATE SKIP LOCKED
LIMIT 1
)
UPDATE %s
SET
-- This is new
enqueued = FALSE,
{state} = 'processing',
{started_at} = NOW()
WHERE {id} IN (SELECT {id} FROM candidate)
RETURNING {id}
We could then compute the Changeset.ReconcilerState()
by looking at both fields.
And we could always set the enqueued
, even while a changeset is being processed, because the field acts like a dirty bit that's only cleared when dequeued. If we set it to true
again, while it's being processed, the reconciler will pick it up again after it's done processing.
Possible Solution 2
We could switch away from using the changesets
table as re-usable jobs that are re-executed again and again and instead create a new reconciler_job
whenever we want the reconciler to look at the changeset.
That would require another table, of course, and that we need to fetch the jobs whenever we want to check the status of a changeset. We could make that easier with a database view, though, in which we always join the changesets against the reconciler_jobs
.
But then we'd have the problem that we create a new job while the old one is being processed and both would try to modify the changeset, so we'd need to lock the changeset.
I'm leaning towards solution 1.