mirror of
https://github.com/pgsty/minio.git
synced 2026-10-31 12:52:27 +03:00
fix(iam): resume site healing after leadership changes
Keep one healing loop per process across replication configuration reloads. Reacquire leadership after a lease is canceled and allow shutdown while waiting, so temporary quorum loss cannot permanently stop revocation propagation. Signed-off-by: Feng Ruohang <rh@vonng.com>
This commit is contained in:
@@ -0,0 +1,75 @@
|
|||||||
|
// Copyright (c) 2026 PGSTY
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestIAMHealingResumesAfterLeadershipLoss(t *testing.T) {
|
||||||
|
previous := globalLeaderLock
|
||||||
|
locks := make(chan LockContext)
|
||||||
|
globalLeaderLock = &sharedLock{lockContext: locks}
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
done := make(chan struct{})
|
||||||
|
c := &SiteReplicationSys{}
|
||||||
|
go func() { c.startHealRoutine(ctx, nil); close(done) }()
|
||||||
|
t.Cleanup(func() {
|
||||||
|
cancel()
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
case locks <- LockContext{ctx: ctx}:
|
||||||
|
}
|
||||||
|
<-done
|
||||||
|
globalLeaderLock = previous
|
||||||
|
})
|
||||||
|
first, loseFirst := context.WithCancel(ctx)
|
||||||
|
defer loseFirst()
|
||||||
|
select {
|
||||||
|
case locks <- LockContext{ctx: first}:
|
||||||
|
case <-time.After(time.Second):
|
||||||
|
t.Fatal("healer did not acquire its first leader context")
|
||||||
|
}
|
||||||
|
loseFirst() // A transient quorum loss cancels the distributed lease.
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
t.Fatal("healer permanently exited after temporary leadership loss")
|
||||||
|
case locks <- LockContext{ctx: ctx}:
|
||||||
|
case <-time.After(time.Second):
|
||||||
|
t.Fatal("healer did not wait for reacquired leadership")
|
||||||
|
}
|
||||||
|
// Shutdown must also interrupt the wait for leadership after lease loss.
|
||||||
|
cancel()
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
case <-time.After(time.Second):
|
||||||
|
t.Fatal("healer did not stop with its owning context")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIAMHealingLeadershipWaitCancels(t *testing.T) {
|
||||||
|
previous := globalLeaderLock
|
||||||
|
locks := make(chan LockContext)
|
||||||
|
globalLeaderLock = &sharedLock{lockContext: locks}
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
done := make(chan struct{})
|
||||||
|
c := &SiteReplicationSys{}
|
||||||
|
go func() { c.startHealRoutine(ctx, nil); close(done) }()
|
||||||
|
cancel()
|
||||||
|
t.Cleanup(func() {
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
case locks <- LockContext{ctx: ctx}:
|
||||||
|
}
|
||||||
|
<-done
|
||||||
|
globalLeaderLock = previous
|
||||||
|
})
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
case <-time.After(time.Second):
|
||||||
|
t.Fatal("healer ignored shutdown while waiting for leadership")
|
||||||
|
}
|
||||||
|
}
|
||||||
+20
-3
@@ -211,6 +211,7 @@ type SiteReplicationSys struct {
|
|||||||
state srState
|
state srState
|
||||||
|
|
||||||
iamMetaCache srIAMCache
|
iamMetaCache srIAMCache
|
||||||
|
healOnce sync.Once // Configuration reloads must not spawn more healing loops.
|
||||||
iamHealMu sync.Mutex
|
iamHealMu sync.Mutex
|
||||||
iamRevisionProgress map[string]iamRevisionProgress
|
iamRevisionProgress map[string]iamRevisionProgress
|
||||||
iamRevisionMetrics iamRevisionMetrics
|
iamRevisionMetrics iamRevisionMetrics
|
||||||
@@ -238,7 +239,7 @@ type srStateData struct {
|
|||||||
|
|
||||||
// Init - initialize the site replication manager.
|
// Init - initialize the site replication manager.
|
||||||
func (c *SiteReplicationSys) Init(ctx context.Context, objAPI ObjectLayer) error {
|
func (c *SiteReplicationSys) Init(ctx context.Context, objAPI ObjectLayer) error {
|
||||||
go c.startHealRoutine(ctx, objAPI)
|
c.healOnce.Do(func() { go c.startHealRoutine(ctx, objAPI) })
|
||||||
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||||
for {
|
for {
|
||||||
err := c.loadFromDisk(ctx, objAPI)
|
err := c.loadFromDisk(ctx, objAPI)
|
||||||
@@ -4577,9 +4578,25 @@ func (c *SiteReplicationSys) PeerStateEditReq(ctx context.Context, arg madmin.SR
|
|||||||
const siteHealTimeInterval = 30 * time.Second
|
const siteHealTimeInterval = 30 * time.Second
|
||||||
|
|
||||||
func (c *SiteReplicationSys) startHealRoutine(ctx context.Context, objAPI ObjectLayer) {
|
func (c *SiteReplicationSys) startHealRoutine(ctx context.Context, objAPI ObjectLayer) {
|
||||||
ctx, cancel := globalLeaderLock.GetLock(ctx)
|
for ctx.Err() == nil {
|
||||||
defer cancel()
|
var leadership LockContext
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case leadership = <-globalLeaderLock.lockContext:
|
||||||
|
}
|
||||||
|
if leadership.Context().Err() != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
leaderCtx, cancel := mergeContext(leadership.Context(), ctx)
|
||||||
|
c.healWithLeadership(leaderCtx, objAPI)
|
||||||
|
cancel()
|
||||||
|
// Quorum loss cancels a leadership lease, not the subsystem. Wait
|
||||||
|
// for a new lease so revocations can still reach offline sites.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *SiteReplicationSys) healWithLeadership(ctx context.Context, objAPI ObjectLayer) {
|
||||||
healTimer := time.NewTimer(siteHealTimeInterval)
|
healTimer := time.NewTimer(siteHealTimeInterval)
|
||||||
defer healTimer.Stop()
|
defer healTimer.Stop()
|
||||||
|
|
||||||
|
|||||||
@@ -114,6 +114,12 @@ cache.
|
|||||||
|
|
||||||
## Healing, failures and operational cost
|
## Healing, failures and operational cost
|
||||||
|
|
||||||
|
Each process starts one healing loop. Losing its distributed leadership lease
|
||||||
|
pauses work until leadership is reacquired; it does not permanently terminate
|
||||||
|
healing. Configuration reloads do not create extra loops. The 30-second interval
|
||||||
|
starts after leadership is acquired and after each completed pass. Initial lock
|
||||||
|
retries and endpoint recovery can add further delay; it is not a convergence SLA.
|
||||||
|
|
||||||
The normal IAM loaders maintain an in-memory index of deletion records and
|
The normal IAM loaders maintain an in-memory index of deletion records and
|
||||||
retained boundaries, without secrets. Healing uses this index; it does not add a
|
retained boundaries, without secrets. Healing uses this index; it does not add a
|
||||||
second full walk of `config/iam/` every cycle. Existing full IAM loading still
|
second full walk of `config/iam/` every cycle. Existing full IAM loading still
|
||||||
|
|||||||
Reference in New Issue
Block a user