feat: add state change lock mechanism - #799
Conversation
There was a problem hiding this comment.
Bug: State lock bypassed by automatic partition reconciliation
The new state change lock mechanism only protects state changes through changePartitionState, but reconcileOwnedPartition directly calls ring.UpdatePartitionState() bypassing the lock check. This means even when a partition has StateChangeLocked set to true, the automatic Pending → Active transition during reconciliation will still occur, defeating the purpose of the lock mechanism.
ring/partition_instance_lifecycler.go#L367-L368
dskit/ring/partition_instance_lifecycler.go
Lines 367 to 368 in e55c331
ring/partition_ring_editor.go#L70-L73
dskit/ring/partition_ring_editor.go
Lines 70 to 73 in e55c331
| if err := h.updater.LockPartitionStateChange(req.Context(), int32(partitionID), true); err != nil { | ||
| http.Error(w, fmt.Sprintf("failed to lock partition state change: %s", err.Error()), http.StatusBadRequest) | ||
| return | ||
| } |
There was a problem hiding this comment.
Bug: Non-atomic state change and lock creates race condition
The change_state_and_lock action performs two separate CAS operations (ChangePartitionState followed by LockPartitionStateChange) that are not atomic. Between these two operations, another concurrent request could change the partition state, resulting in locking an unintended state. The intent is to atomically change state and lock, but a concurrent state change between lines 165-173 could lead to the wrong state being locked.
There was a problem hiding this comment.
Bug: Lifecycler bypasses state lock during automatic reconciliation
The reconcileOwnedPartition function calls ring.UpdatePartitionState() directly without checking the StateChangeLocked flag. This means when a partition is locked and its state transitions automatically from Pending to Active (when enough owners join), the lock is bypassed. The PR adds state lock enforcement in changePartitionState via ChangePartitionState, but this automatic reconciliation path doesn't use that function, defeating the purpose of the lock for Pending→Active transitions.
ring/partition_instance_lifecycler.go#L365-L368
dskit/ring/partition_instance_lifecycler.go
Lines 365 to 368 in 56874a1
There was a problem hiding this comment.
Bug: State lock bypassed in automatic partition reconciliation
The reconcileOwnedPartition function directly calls ring.UpdatePartitionState() to automatically transition partitions from Pending to Active when enough owners register. This bypasses the new StateChangeLocked check that was added in changePartitionState(). As a result, even if an operator locks a partition to prevent state changes, the automatic lifecycle reconciliation will still change a Pending partition to Active, defeating the purpose of the lock mechanism for this transition path.
ring/partition_instance_lifecycler.go#L367-L368
dskit/ring/partition_instance_lifecycler.go
Lines 367 to 368 in 84605bc
ed2b5be to
aa2757b
Compare
pracucci
left a comment
There was a problem hiding this comment.
The changes LGTM. I'm not convinced the new metrics are required. I think logs better fit this use case.
| <input type="hidden" name="partition_state" value="{{ $toState.String }}"> | ||
|
|
||
| <button name="action" value="change_state" type="submit">Change state to {{ $toState.CleanName }}</button> | ||
| <button name="action" value="change_state" type="submit" {{ if .StateChangeLocked }}disabled title="State change is locked"{{ end }} onclick="return confirm('Do you confirm you want to change the state to {{ $toState.CleanName }}?');">Change state to {{ $toState.CleanName }}</button> |
There was a problem hiding this comment.
Does the onclick here prevents the change if canceled? I'm wondering if the safest option would just be creating a <form> for each different action (in this case it would be 1 more form).
There was a problem hiding this comment.
Yes the onlick prevent the change if cancelled, the browser will just not send the request.
| partitionLockStateGaugeVec: promauto.With(reg).NewGaugeVec(prometheus.GaugeOpts{ | ||
| Name: "partition_ring_partition_state_change_locked", | ||
| Help: "Whether the partition state change is locked. 1 if locked, 0 if unlocked.", | ||
| ConstLabels: map[string]string{"name": name}, | ||
| }, []string{"partition_id", "state"}), | ||
| partitionLockTimestampGaugeVec: promauto.With(reg).NewGaugeVec(prometheus.GaugeOpts{ | ||
| Name: "partition_ring_partition_state_change_locked_timestamp_seconds", | ||
| Help: "Unix timestamp (seconds) of when the partition state change lock was last modified. 0 if unlocked.", | ||
| ConstLabels: map[string]string{"name": name}, | ||
| }, []string{"partition_id", "state"}), |
There was a problem hiding this comment.
These metrics are going to be high cardinality because exposed for each partition by each pod. Are they really required? Looks like useful only once in a lifetime (maybe when investigating an incident) and maybe can be replaced by a log when the lock change? It could be logged here by the watcher.
There was a problem hiding this comment.
I preferred metrics so I can have a dashboard likes the one we have for flux ignore dashboard
But since this is only an emergency lever, I agree that this high cardinality metric is not necessary, I replaced it with a simple warn log:
level.Warn(w.logger).Log("msg", "partition state change is locked", "partition_id", partitionIDStr, "partition_state", state)
Co-authored-by: Marco Pracucci <marco@pracucci.com>
| if partition.StateChangeLocked { | ||
| level.Warn(w.logger).Log("msg", "partition state change is locked", "partition_id", partitionIDStr, "partition_state", state) | ||
| } | ||
| } |
There was a problem hiding this comment.
Bug: Warning logs emitted on every ring update
The warning log for locked partition state changes is emitted every time updatePartitionRing is called, which happens on every ring update from the KV store watcher. For long-lived locked partitions in busy clusters with frequent ring updates, this will cause significant log spam. The intent appears to be logging when the lock changes, not on every ring update. Consider tracking previously locked partitions and only logging when the lock state actually changes.
| state := partition.GetState().CleanName() | ||
| partitionIDStr := strconv.Itoa(int(partitionID)) | ||
| if partition.StateChangeLocked { | ||
| level.Warn(w.logger).Log("msg", "partition state change is locked", "partition_id", partitionIDStr, "partition_state", state) |
There was a problem hiding this comment.
This is extremely verbose. Why not just logging when it has changed compared to the oldRing?
#### What this PR does Update dskit to get grafana/dskit#799: it adds support to lock a partition state change through the UI. When the partition is locked, the prepare downscale API should return 409 status code (rollout-operator has been updated to correctly handle the 409). #### Which issue(s) this PR fixes or relates to N/A #### Checklist - [x] Tests updated. - [ ] Documentation added. - [x] `CHANGELOG.md` updated - the order of entries should be `[CHANGE]`, `[FEATURE]`, `[ENHANCEMENT]`, `[BUGFIX]`. If changelog entry is not needed, please add the `changelog-not-needed` label to the PR. - [ ] [`about-versioning.md`](https://github.com/grafana/mimir/blob/main/docs/sources/mimir/configure/about-versioning.md) updated with experimental features. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > Adds UI and APIs to lock partition state changes in the partitions ring and updates ingester to return 409 when locked; bumps dskit. > > - **Partitions ring (UI/API)**: > - Add lock/unlock controls and “change state & lock” action; display lock status. > - Log partitions with `stateChangeLocked`. > - **Ring model/editor (dskit)**: > - Introduce `stateChangeLocked` (+ timestamp) in `PartitionDesc` and merge logic. > - Make `UpdatePartitionState()` return error and block when locked. > - Add `SetPartitionStateChangeLock()` API. > - **Ingester**: > - `/ingester/prepare-partition-downscale` returns `409 Conflict` when partition state change is locked (POST/DELETE). > - **Tests**: > - Extend ingest-storage and owned-series tests for new API and locked-state behavior. > - **Deps**: > - Bump `github.com/grafana/dskit` version. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 5e3c0f8. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Signed-off-by: Marco Pracucci <marco@pracucci.com>
…855) **What this PR does**: In the previous PR #799 we added logging when we see a partition state change locked. Turns out it's too verbose. In this PR we only log when previous partition does not exist or the lock state changed. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > Logs partition state-change lock/unlock only when a partition is new or its lock status changes, reducing log verbosity. > > - **Ring watcher (`ring/partition_ring_watcher.go`)**: > - Log partition state-change lock status only when a partition is new or the `StateChangeLocked` flag changes. > - Emit warn on lock and info on unlock, including `partition_id` and current `partition_state`. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 6b6a294. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
What this PR does:
This PR adds the ability to lock partition state change. If a state change is locked we cannot change the state of a partition ring.
This is useful if we want to deactivate a partition ring and prevent it from being activated again.
We also add these 2 metrics:
Checklist
Note
Introduce a lock to prevent partition state changes, updating proto/model, editor, lifecycler, HTTP API/UI, watcher logging, and tests.
PartitionDescfields:stateChangeLockedandstateChangeLockedTimestamp(partition_ring_desc.proto+ regeneratedpb.go).UpdatePartitionState()(now returns(bool, error)) and addUpdatePartitionStateChangeLock().SetPartitionStateChangeLock()and helperSetPartitionStateChangeLock(...).changePartitionState()and callers adapted to new return signature.stateChangeLockedin page data; disable change buttons when locked.state_change_lock(lock/unlock) andchange_state_and_lock(change then lock).UpdatePartitionStatesignature.Written by Cursor Bugbot for commit fd822d0. This will update automatically on new commits. Configure here.