Skip to content

feat: add state change lock mechanism - #799

Merged
Segflow merged 9 commits into
mainfrom
meher/add-state-change-locked
Dec 10, 2025
Merged

feat: add state change lock mechanism#799
Segflow merged 9 commits into
mainfrom
meher/add-state-change-locked

Conversation

@Segflow

@Segflow Segflow commented Nov 25, 2025

Copy link
Copy Markdown
Contributor

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:

  • partition_ring_partition_state_change_locked{name, partition_id, state}
  • partition_ring_partition_state_change_locked_timestamp_seconds{name, partition_id, state}

Checklist

  • Tests updated

Note

Introduce a lock to prevent partition state changes, updating proto/model, editor, lifecycler, HTTP API/UI, watcher logging, and tests.

  • Ring Model/Proto:
    • Add PartitionDesc fields: stateChangeLocked and stateChangeLockedTimestamp (partition_ring_desc.proto + regenerated pb.go).
    • Enforce lock in UpdatePartitionState() (now returns (bool, error)) and add UpdatePartitionStateChangeLock().
    • Merge logic propagates lock fields based on timestamp.
  • Editor/API:
    • Add SetPartitionStateChangeLock() and helper SetPartitionStateChangeLock(...).
    • changePartitionState() and callers adapted to new return signature.
  • HTTP/UI:
    • Surface stateChangeLocked in page data; disable change buttons when locked.
    • New POST actions: state_change_lock (lock/unlock) and change_state_and_lock (change then lock).
  • Lifecycler:
    • Reconcile path updated to use new UpdatePartitionState signature.
  • Watcher:
    • Log partitions with locked state changes on updates.
  • Tests:
    • Add/extend tests for lock behavior across model, editor, HTTP handler, watcher, and ring tests; update existing calls for new signatures.

Written by Cursor Bugbot for commit fd822d0. This will update automatically on new commits. Configure here.

@CLAassistant

CLAassistant commented Nov 25, 2025

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@Segflow Segflow changed the title add state change lock mechanism Dec 5, 2025
@Segflow
Segflow marked this pull request as ready for review December 5, 2025 19:10
@Segflow
Segflow requested a review from pracucci December 5, 2025 19:10
@Segflow Segflow mentioned this pull request Dec 5, 2025
1 task

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

level.Info(l.logger).Log("msg", "switching partition state because enough owners have been registered and minimum waiting time has elapsed", "partition", l.cfg.PartitionID, "from_state", PartitionPending, "to_state", PartitionActive)
return ring.UpdatePartitionState(partitionID, PartitionActive, now), nil

ring/partition_ring_editor.go#L70-L73

if partition.StateChangeLocked {
return false, ErrPartitionStateChangeLocked
}

Fix in Cursor Fix in Web


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
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

Comment thread ring/partition_ring_editor.go

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

// have been added since more than the waiting period.
if partition.IsPending() && ring.PartitionOwnersCountUpdatedBefore(partitionID, now.Add(-l.cfg.WaitOwnersDurationOnPending)) >= l.cfg.WaitOwnersCountOnPending {
level.Info(l.logger).Log("msg", "switching partition state because enough owners have been registered and minimum waiting time has elapsed", "partition", l.cfg.PartitionID, "from_state", PartitionPending, "to_state", PartitionActive)
return ring.UpdatePartitionState(partitionID, PartitionActive, now), nil

Fix in Cursor Fix in Web


Comment thread ring/partition_ring_watcher.go

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

level.Info(l.logger).Log("msg", "switching partition state because enough owners have been registered and minimum waiting time has elapsed", "partition", l.cfg.PartitionID, "from_state", PartitionPending, "to_state", PartitionActive)
return ring.UpdatePartitionState(partitionID, PartitionActive, now), nil

Fix in Cursor Fix in Web


@Segflow
Segflow force-pushed the meher/add-state-change-locked branch from ed2b5be to aa2757b Compare December 5, 2025 21:49
Comment thread ring/partition_ring_model.go

@pracucci pracucci left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The changes LGTM. I'm not convinced the new metrics are required. I think logs better fit this use case.

Comment thread ring/partition_ring_editor.go Outdated
Comment thread ring/partition_ring_http.go Outdated
Comment thread ring/partition_ring_http.go Outdated
Comment thread ring/partition_ring_http_test.go
<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>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes the onlick prevent the change if cancelled, the browser will just not send the request.

Comment thread ring/partition_ring_watcher.go Outdated
Comment on lines +52 to +61
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"}),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@Segflow Segflow Dec 10, 2025

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Comment thread ring/partition_ring_watcher.go Outdated
Comment thread ring/partition_ring_model.go
if partition.StateChangeLocked {
level.Warn(w.logger).Log("msg", "partition state change is locked", "partition_id", partitionIDStr, "partition_state", state)
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

@Segflow
Segflow merged commit 41c7cf0 into main Dec 10, 2025
11 checks passed
@Segflow
Segflow deleted the meher/add-state-change-locked branch December 10, 2025 11:56
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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is extremely verbose. Why not just logging when it has changed compared to the oldRing?

pracucci added a commit to grafana/mimir that referenced this pull request Dec 11, 2025
#### 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>
Segflow added a commit that referenced this pull request Dec 12, 2025
…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 -->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

3 participants