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
In order to simplify the instance lifecycle state machine and ensure
that instance state updates are processed reliably, we intend to perform
instance state updates in a saga (which will be added in PR #5749). This
saga will require a notion of mutual exclusion between update sagas for
the same instance, in order to avoid race conditions like the following:
1. Sagas `S1` and `S2` start at the same time and observe the same
instance/VMM states, which indicate that the instance’s active VMM
has shut down
2. `S1` clears all the resources/provisioning counters and marks the
instance as `Stopped``
3. User restarts the instance
4. `S2` clears the same instance provisioning counters again
Presently, these races are avoided by the fact that instance state
updates are performed partially in `sled-agent`, which serves as an
"actor" with exclusive ownership over the state transition. Moving these
state transitions to Nexus requires introducing mutual exclusion.
This commit adds a distributed lock on instance state transitions to the
datastore. We add the following fields to the `instance` table:
- `updater_id`, which is the UUID of the saga currently holding the
update lock on the instance (or `NULL` if no saga has locked the
instance)
- `updater_gen`, a generation counter that is incremented each time the
lock is acquired by a new saga
Using these fields, we can add new datastore methods to try and acquire
an instance update lock by doing the following:
1. Generate a UUID for the saga, `saga_lock_id`. This will be performed
in the saga itself and isn't part of this PR.
2. Read the instance record and interpret the value of the `updater_id`
field as follows:
- `NULL`: lock not held, we can acquire it by incrementing the
`updater_gen` field and setting the `updater_id` field to the saga's
UUID.
- `updater_id == saga_id`: the saga already holds the lock, we can
proceed with the update.
- `updater_id != saga_id`: another saga holds the lock, we can't
proceed with the update. Fail the operation.
3. Attempt to write back the updated instance record with generation
incremented and the `updater_id` set to the saga's ID, conditional on
the `updater_gen` field being equal to the ID that was read when read
the instance record. This is equivalent to the atomic compare-and-swap
operation that one might use to implement a non-distributed lock in a
single address space.
- If this fails because the generation number is outdated, try again
(i.e. goto (2)).
- If this succeeds, the lock was acquired successfully.
Additionally, we can add a method for unlocking an instance record by
clearing the `updater_id` field and incrementing the `updater_gen`. This
query is conditional on the `updater_id` field being equal to the saga's
UUID, to prevent cases where we accidentally unlock an instance that was
locked by a different saga.
Introducing this distributed lock is considered fairly safe, as it will
only ever be acquired in a saga, and the reverse action for the saga
action that acquires the lock will ensure that the lock is released if
the saga unwinds. Essentially, this is equivalent to a RAII guard
releasing a lock when a thread panics in a single-threaded Rust program.
Presently, none of these methods are actually used. The saga that uses
them will be added in PR #5749. I've factored out this change into its
own PR so that we can merge the foundation needed for that branch.
Hopefully this makes the diff a bit smaller and easier to review, as
well as decreasing merge conflict churn with the schema changes.
0 commit comments