Skip to content

Commit 84dd492

Browse files
committed
CBG-5372 make sure that principals are invalidated on one node
1 parent d2ffa7a commit 84dd492

4 files changed

Lines changed: 419 additions & 8 deletions

db/background_mgr.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -543,6 +543,13 @@ func (b *BackgroundManager) setStartTime(startTime time.Time) {
543543
b.status.StartTime = startTime
544544
}
545545

546+
// getLastError returns the last error recorded by the manager.
547+
func (b *BackgroundManager) getLastError() error {
548+
b.lock.Lock()
549+
defer b.lock.Unlock()
550+
return b.lastError
551+
}
552+
546553
// SetError sets the last known error, transitions the state to BackgroundManagerStateError and terminates the process.
547554
func (b *BackgroundManager) SetError(err error) {
548555
b.lock.Lock()
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
// Copyright 2012-Present Couchbase, Inc.
2+
//
3+
// Use of this software is governed by the Business Source License included
4+
// in the file licenses/BSL-Couchbase.txt. As of the Change Date specified
5+
// in that file, in accordance with the Business Source License, use of this
6+
// software will be governed by the Apache License, Version 2.0, included in
7+
// the file licenses/APL2.txt.
8+
9+
package db
10+
11+
import (
12+
"context"
13+
14+
"github.com/couchbase/sync_gateway/base"
15+
)
16+
17+
// =====================================================================
18+
// InvalidatePrincipals Background Manager
19+
// =====================================================================
20+
21+
// InvalidatePrincipalsProcess implements the principal invalidation and syncInfo update steps that follow a resync.
22+
type InvalidatePrincipalsProcess struct {
23+
db *DatabaseContext
24+
regenerateSequences bool
25+
hasAllCollections bool
26+
docsChanged int64
27+
collectionIDs []uint32
28+
}
29+
30+
var _ BackgroundManagerProcessI = &InvalidatePrincipalsProcess{}
31+
32+
// newInvalidatePrincipalsManager returns a local BackgroundManager that runs the principal invalidation process
33+
// with the given parameters.
34+
func newInvalidatePrincipalsManager(db *DatabaseContext, regenerateSequences bool, hasAllCollections bool, docsChanged int64, collectionIDs []uint32) *BackgroundManager {
35+
return &BackgroundManager{
36+
name: "invalidate_principals",
37+
Process: &InvalidatePrincipalsProcess{
38+
db: db,
39+
regenerateSequences: regenerateSequences,
40+
hasAllCollections: hasAllCollections,
41+
docsChanged: docsChanged,
42+
collectionIDs: collectionIDs,
43+
},
44+
terminator: base.NewSafeTerminator(),
45+
}
46+
}
47+
48+
func (p *InvalidatePrincipalsProcess) Init(_ context.Context, _ map[string]any, _ []byte) error {
49+
return nil
50+
}
51+
52+
func (p *InvalidatePrincipalsProcess) Run(ctx context.Context, _ map[string]any, _ updateStatusCallbackFunc, terminator *base.SafeTerminator) error {
53+
if terminator.IsClosed() {
54+
return nil
55+
}
56+
if err := invalidatePrincipals(ctx, p.db, p.regenerateSequences, p.hasAllCollections, p.docsChanged); err != nil {
57+
return err
58+
}
59+
if terminator.IsClosed() {
60+
return nil
61+
}
62+
if p.regenerateSequences {
63+
updateSyncInfo(ctx, p.db, p.collectionIDs)
64+
}
65+
return nil
66+
}
67+
68+
func (p *InvalidatePrincipalsProcess) GetProcessStatus(status BackgroundManagerStatus, _ []byte) ([]byte, []byte, error) {
69+
out, err := base.JSONMarshal(status)
70+
return out, nil, err
71+
}
72+
73+
func (p *InvalidatePrincipalsProcess) SetProcessStatus(_ context.Context, _, _ []byte) {}
74+
75+
func (p *InvalidatePrincipalsProcess) ResetStatus() {}
Lines changed: 276 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,276 @@
1+
// Copyright 2012-Present Couchbase, Inc.
2+
//
3+
// Use of this software is governed by the Business Source License included
4+
// in the file licenses/BSL-Couchbase.txt. As of the Change Date specified
5+
// in that file, in accordance with the Business Source License, use of this
6+
// software will be governed by the Apache License, Version 2.0, included in
7+
// the file licenses/APL2.txt.
8+
9+
package db
10+
11+
import (
12+
"context"
13+
"sync/atomic"
14+
"testing"
15+
"time"
16+
17+
"github.com/couchbase/sync_gateway/base"
18+
"github.com/stretchr/testify/assert"
19+
"github.com/stretchr/testify/require"
20+
)
21+
22+
// blockableProcess is a BackgroundManagerProcessI whose Run blocks until released or the terminator fires.
23+
// After the first run, subsequent runs complete immediately to simulate a successful restart.
24+
type blockableProcess struct {
25+
runCount atomic.Int32
26+
blockCh chan struct{} // close to unblock the first run
27+
}
28+
29+
func newBlockableProcess() *blockableProcess {
30+
return &blockableProcess{blockCh: make(chan struct{})}
31+
}
32+
33+
func (b *blockableProcess) Init(_ context.Context, _ map[string]any, _ []byte) error { return nil }
34+
35+
func (b *blockableProcess) Run(_ context.Context, _ map[string]any, _ updateStatusCallbackFunc, terminator *base.SafeTerminator) error {
36+
count := b.runCount.Add(1)
37+
if count == 1 {
38+
select {
39+
case <-b.blockCh:
40+
case <-terminator.Done():
41+
}
42+
}
43+
return nil
44+
}
45+
46+
func (b *blockableProcess) GetProcessStatus(status BackgroundManagerStatus, _ []byte) ([]byte, []byte, error) {
47+
out, err := base.JSONMarshal(status)
48+
return out, nil, err
49+
}
50+
51+
func (b *blockableProcess) SetProcessStatus(_ context.Context, _, _ []byte) {}
52+
func (b *blockableProcess) ResetStatus() {}
53+
54+
// newBlockableManager returns a local BackgroundManager backed by a blockableProcess.
55+
func newBlockableManager(p *blockableProcess) *BackgroundManager {
56+
return &BackgroundManager{
57+
name: "test_invalidate_principals",
58+
Process: p,
59+
terminator: base.NewSafeTerminator(),
60+
}
61+
}
62+
63+
// TestInvalidatePrincipalsManagerCompletes verifies that a manager whose process completes quickly transitions
64+
// to BackgroundProcessStateCompleted.
65+
func TestInvalidatePrincipalsManagerCompletes(t *testing.T) {
66+
ctx := base.TestCtx(t)
67+
68+
proc := newBlockableProcess()
69+
close(proc.blockCh) // unblock immediately so Run returns right away
70+
mgr := newBlockableManager(proc)
71+
72+
require.NoError(t, mgr.Start(ctx, nil))
73+
74+
RequireBackgroundManagerState(t, mgr, BackgroundProcessStateCompleted)
75+
}
76+
77+
// TestRunInvalidatePrincipalsLoopCompletesNormally verifies that invalidatePrincipals returns nil
78+
// when the manager completes successfully.
79+
func TestRunInvalidatePrincipalsLoopCompletesNormally(t *testing.T) {
80+
ctx := base.TestCtx(t)
81+
82+
proc := newBlockableProcess()
83+
close(proc.blockCh) // unblock immediately so the first Run returns right away
84+
resync := &ResyncManagerDCP{
85+
invalidatePrincipalsManager: newBlockableManager(proc),
86+
invalidatePrincipalsPollWait: time.Millisecond,
87+
}
88+
89+
terminator := base.NewSafeTerminator()
90+
doneCh := make(chan error, 1)
91+
go func() {
92+
doneCh <- resync.invalidatePrincipals(ctx, nil, false, false, terminator)
93+
}()
94+
95+
require.NoError(t, base.RequireChanRecv(t, doneCh))
96+
}
97+
98+
// TestRunInvalidatePrincipalsLoopNoOpIfAlreadyStarted verifies that the loop does not fail when Start
99+
// returns errBackgroundManagerProcessAlreadyRunning; it waits and then proceeds once the manager completes.
100+
func TestRunInvalidatePrincipalsLoopNoOpIfAlreadyStarted(t *testing.T) {
101+
ctx := base.TestCtx(t)
102+
103+
proc := newBlockableProcess()
104+
mgr := newBlockableManager(proc)
105+
106+
resync := &ResyncManagerDCP{
107+
invalidatePrincipalsManager: mgr,
108+
invalidatePrincipalsPollWait: time.Millisecond,
109+
}
110+
terminator := base.NewSafeTerminator()
111+
112+
// Start the manager externally so it is already running when the loop begins.
113+
require.NoError(t, mgr.Start(ctx, nil))
114+
RequireBackgroundManagerState(t, mgr, BackgroundProcessStateRunning)
115+
116+
doneCh := make(chan error, 1)
117+
go func() {
118+
doneCh <- resync.invalidatePrincipals(ctx, nil, false, false, terminator)
119+
}()
120+
121+
// Loop should be polling (not failed) while the manager is running.
122+
select {
123+
case err := <-doneCh:
124+
t.Fatalf("loop returned prematurely: %v", err)
125+
case <-time.After(100 * time.Millisecond):
126+
}
127+
128+
// Unblock the process — manager transitions to Completed and the loop returns.
129+
close(proc.blockCh)
130+
131+
require.NoError(t, base.RequireChanRecv(t, doneCh))
132+
}
133+
134+
// TestRunInvalidatePrincipalsLoopRestartsAfterStop verifies that when the manager is stopped before
135+
// completing, the loop restarts it and waits for the subsequent run to complete.
136+
func TestRunInvalidatePrincipalsLoopRestartsAfterStop(t *testing.T) {
137+
ctx := base.TestCtx(t)
138+
139+
proc := newBlockableProcess()
140+
mgr := newBlockableManager(proc)
141+
142+
resync := &ResyncManagerDCP{
143+
invalidatePrincipalsManager: mgr,
144+
invalidatePrincipalsPollWait: time.Millisecond,
145+
}
146+
terminator := base.NewSafeTerminator()
147+
148+
doneCh := make(chan error, 1)
149+
go func() {
150+
doneCh <- resync.invalidatePrincipals(ctx, nil, false, false, terminator)
151+
}()
152+
153+
// Wait for the manager to start.
154+
RequireBackgroundManagerState(t, mgr, BackgroundProcessStateRunning)
155+
156+
// Loop should still be polling.
157+
select {
158+
case err := <-doneCh:
159+
t.Fatalf("loop returned prematurely: %v", err)
160+
case <-time.After(100 * time.Millisecond):
161+
}
162+
163+
// Stop the manager while it is blocked — simulates a crash or external stop.
164+
require.NoError(t, mgr.Stop(ctx))
165+
166+
// The loop must detect the Stopped state and restart the manager. The second run (count >= 2)
167+
// completes immediately, so the loop should finish successfully. We do not assert Stopped state
168+
// here because the fast poll interval means the loop may have already restarted the manager by
169+
// the time we check.
170+
require.NoError(t, base.RequireChanRecv(t, doneCh))
171+
assert.GreaterOrEqual(t, int(proc.runCount.Load()), 2, "process should have been run at least twice")
172+
}
173+
174+
// slowBlockableProcess is a BlockableProcess whose first Run signals readyCh when it starts and then
175+
// waits for blockCh to be closed before completing. This lets a test interleave an invalidatePrincipals
176+
// call between "process is known to be running" and "process completes".
177+
type slowBlockableProcess struct {
178+
blockableProcess
179+
readyCh chan struct{} // closed by Run once the first goroutine is inside the select
180+
}
181+
182+
func newSlowBlockableProcess() *slowBlockableProcess {
183+
return &slowBlockableProcess{
184+
blockableProcess: blockableProcess{blockCh: make(chan struct{})},
185+
readyCh: make(chan struct{}),
186+
}
187+
}
188+
189+
func (s *slowBlockableProcess) Run(ctx context.Context, opts map[string]any, cb updateStatusCallbackFunc, terminator *base.SafeTerminator) error {
190+
count := s.runCount.Add(1)
191+
if count == 1 {
192+
close(s.readyCh) // signal that we are inside Run and blocking
193+
select {
194+
case <-s.blockCh:
195+
case <-terminator.Done():
196+
}
197+
}
198+
return nil
199+
}
200+
201+
// TestRunInvalidatePrincipalsLoopAlreadyRunningCompletesBeforeWait reproduces the bug where Start()
202+
// returns errBackgroundManagerProcessAlreadyRunning but the manager's process finishes before
203+
// waitInvalidatePrincipals begins polling.
204+
//
205+
// The old implementation blocked on the manager's internal terminator channel. When the process
206+
// completes between the Start() call and the select statement, the terminator fires immediately and
207+
// the subsequent RetryLoop may see GetRunState() == Running (the state is updated after Terminate()
208+
// closes the terminator), causing it to spin for up to 500 ms.
209+
//
210+
// The new implementation polls GetRunState on a ticker, so the terminal state is detected on the
211+
// next tick regardless of how GetRunState looked at the moment the terminator fired.
212+
func TestRunInvalidatePrincipalsLoopAlreadyRunningCompletesBeforeWait(t *testing.T) {
213+
ctx := base.TestCtx(t)
214+
215+
proc := newSlowBlockableProcess()
216+
mgr := newBlockableManager(&proc.blockableProcess)
217+
// Swap the Run implementation to the slow version that signals readyCh.
218+
mgr.Process = proc
219+
220+
resync := &ResyncManagerDCP{
221+
invalidatePrincipalsManager: mgr,
222+
invalidatePrincipalsPollWait: time.Millisecond,
223+
}
224+
terminator := base.NewSafeTerminator()
225+
226+
// Start the manager externally; the process blocks until we close blockCh.
227+
require.NoError(t, mgr.Start(ctx, nil))
228+
229+
// Wait until the goroutine is inside Run so we know state == Running.
230+
select {
231+
case <-proc.readyCh:
232+
case <-time.After(5 * time.Second):
233+
t.Fatal("process goroutine did not start")
234+
}
235+
236+
doneCh := make(chan error, 1)
237+
go func() {
238+
doneCh <- resync.invalidatePrincipals(ctx, nil, false, false, terminator)
239+
}()
240+
241+
// invalidatePrincipals calls Start(), receives errBackgroundManagerProcessAlreadyRunning, and is
242+
// about to call waitInvalidatePrincipals. Unblock the process now so that the manager transitions
243+
// to Completed before (or just as) waitInvalidatePrincipals begins. The loop must correctly detect
244+
// the completed state and return nil rather than hanging.
245+
close(proc.blockCh)
246+
247+
require.NoError(t, base.RequireChanRecv(t, doneCh))
248+
}
249+
250+
// TestRunInvalidatePrincipalsLoopExitsOnTerminator verifies that the loop exits cleanly when the resync
251+
// terminator fires while waiting for the manager to complete.
252+
func TestRunInvalidatePrincipalsLoopExitsOnTerminator(t *testing.T) {
253+
ctx := base.TestCtx(t)
254+
255+
proc := newBlockableProcess()
256+
mgr := newBlockableManager(proc)
257+
258+
resync := &ResyncManagerDCP{
259+
invalidatePrincipalsManager: mgr,
260+
invalidatePrincipalsPollWait: time.Millisecond,
261+
}
262+
terminator := base.NewSafeTerminator()
263+
264+
doneCh := make(chan error, 1)
265+
go func() {
266+
doneCh <- resync.invalidatePrincipals(ctx, nil, false, false, terminator)
267+
}()
268+
269+
// Wait for the manager to start.
270+
RequireBackgroundManagerState(t, mgr, BackgroundProcessStateRunning)
271+
272+
// Close the resync terminator — the loop should exit without error.
273+
terminator.Close()
274+
275+
require.NoError(t, base.RequireChanRecv(t, doneCh))
276+
}

0 commit comments

Comments
 (0)