Skip to content

Commit 75b8bd0

Browse files
authored
CBG-5494 Attachment Compaction: remove data race (#8395)
1 parent 16589b0 commit 75b8bd0

2 files changed

Lines changed: 102 additions & 37 deletions

File tree

db/attachment_compaction_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -300,10 +300,10 @@ func TestAttachmentCleanupRollback(t *testing.T) {
300300
vbUUID := base.GetVBUUIDs(dcpClient.GetMetadata())
301301
vbUUID[0] = uint64(garbageVBUUID)
302302

303-
metadataKeys := base.NewMetadataKeys(testDb.Options.MetadataID)
304-
testDb.AttachmentCompactionManager = NewAttachmentCompactionManager(dataStore, metadataKeys)
305-
manager := AttachmentCompactionManager{CompactID: t.Name(), Phase: string(CleanupPhase), VBUUIDs: vbUUID}
306-
testDb.AttachmentCompactionManager.Process = &manager
303+
testDb.AttachmentCompactionManager.Process.(*AttachmentCompactionManager).initializeFromPreviousStatus(AttachmentManagerStatusDoc{
304+
AttachmentManagerResponse: AttachmentManagerResponse{CompactID: t.Name(), Phase: string(CleanupPhase)},
305+
AttachmentManagerMeta: AttachmentManagerMeta{VBUUIDs: vbUUID},
306+
})
307307

308308
terminator := base.NewSafeTerminator()
309309
err = testDb.AttachmentCompactionManager.Process.Run(ctx, map[string]any{"database": testDb}, testDb.AttachmentCompactionManager.UpdateStatusClusterAware, terminator)

db/background_mgr_attachment_compaction.go

Lines changed: 98 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,23 @@ import (
2727
// runFunctionStartedCallbackFunc is a test seam called at the top of Run before any compaction phases execute.
2828
type runFunctionStartedCallbackFunc func(context.Context, map[string]any, updateStatusCallbackFunc, *base.SafeTerminator)
2929

30+
// AttachmentCompactionManager implements the attachment compaction background process. Compaction
31+
// runs in three sequential phases — mark, sweep, cleanup.
32+
//
33+
// Fields _compactID, _phase, _vbuuids, _dryRun are protected by lock.
3034
type AttachmentCompactionManager struct {
31-
MarkedAttachments base.AtomicInt
32-
PurgedAttachments base.AtomicInt
33-
CompactID string
34-
Phase string
35-
VBUUIDs []uint64
36-
dryRun bool
37-
lock sync.Mutex
35+
// MarkedAttachments counts attachments marked as live during the mark phase.
36+
MarkedAttachments base.AtomicInt
37+
// PurgedAttachments counts attachments deleted during the sweep phase.
38+
PurgedAttachments base.AtomicInt
39+
40+
// The following fields are protected by lock and must be accessed via their getters/setters.
41+
_compactID string // unique identifier for the current compaction run, used to tag marked attachments
42+
_phase string // current phase: "mark", "sweep", "cleanup", or "" when idle
43+
_vbuuids []uint64 // vBucket UUIDs captured after the mark phase, used to detect DCP rollbacks in cleanup
44+
_dryRun bool // when true, sweep phase reports but does not delete attachments
45+
46+
lock sync.RWMutex
3847
runFunctionStartedCallback atomic.Pointer[runFunctionStartedCallbackFunc]
3948
}
4049

@@ -68,9 +77,9 @@ func (a *AttachmentCompactionManager) Init(ctx context.Context, options map[stri
6877
base.InfofCtx(ctx, base.KeyAll, "Attachment Compaction: Running as dry run. No attachments will be purged")
6978
}
7079

71-
a.dryRun = dryRun
72-
a.CompactID = uniqueUUID.String()
73-
base.InfofCtx(ctx, base.KeyAll, "Attachment Compaction: Starting new compaction run with compact ID: %q", a.CompactID)
80+
compactID := uniqueUUID.String()
81+
a.initializeNewRun(compactID, dryRun)
82+
base.InfofCtx(ctx, base.KeyAll, "Attachment Compaction: Starting new compaction run with compact ID: %q", compactID)
7483
return nil
7584
}
7685

@@ -90,14 +99,8 @@ func (a *AttachmentCompactionManager) Init(ctx context.Context, options map[stri
9099
if statusDoc.State == BackgroundProcessStateCompleted || err != nil || (reset && ok) {
91100
return backgroundManagerInitReset, newRunInit()
92101
} else {
93-
a.CompactID = statusDoc.CompactID
94-
a.Phase = statusDoc.Phase
95-
a.dryRun = statusDoc.DryRun
96-
a.MarkedAttachments.Set(statusDoc.MarkedAttachments)
97-
a.PurgedAttachments.Set(statusDoc.PurgedAttachments)
98-
a.VBUUIDs = statusDoc.VBUUIDs
99-
100-
base.InfofCtx(ctx, base.KeyAll, "Attachment Compaction: Attempting to resume compaction with compact ID: %q phase %q", a.CompactID, a.Phase)
102+
compactID, phase := a.initializeFromPreviousStatus(statusDoc)
103+
base.InfofCtx(ctx, base.KeyAll, "Attachment Compaction: Attempting to resume compaction with compact ID: %q phase %q", compactID, phase)
101104
}
102105

103106
return backgroundManagerInitResume, nil
@@ -136,14 +139,14 @@ func (a *AttachmentCompactionManager) Run(ctx context.Context, options map[strin
136139
// Need to check the current phase in the event we are resuming - No need to run mark again if we got as far as
137140
// cleanup last time...
138141
var err error
139-
switch a.Phase {
142+
switch a.getPhase() {
140143
case "mark", "":
141144
a.SetPhase("mark")
142145
worker := func() (shouldRetry bool, err error, value any) {
143146
persistClusterStatus()
144-
_, dcpClient, err := attachmentCompactMarkPhase(ctx, dataStore, collectionID, database, a.CompactID, terminator, &a.MarkedAttachments)
147+
_, dcpClient, err := attachmentCompactMarkPhase(ctx, dataStore, collectionID, database, a.getCompactID(), terminator, &a.MarkedAttachments)
145148
if dcpClient != nil {
146-
a.VBUUIDs = base.GetVBUUIDs(dcpClient.GetMetadata())
149+
a.setVBUUIDs(base.GetVBUUIDs(dcpClient.GetMetadata()))
147150
}
148151
if err != nil {
149152
// if dcpClient is nil, then dcpClient.GetMetadataKeyPrefix() will panic. This isn't a rollback
@@ -169,7 +172,7 @@ func (a *AttachmentCompactionManager) Run(ctx context.Context, options map[strin
169172
case "sweep":
170173
a.SetPhase("sweep")
171174
persistClusterStatus()
172-
_, _, err := attachmentCompactSweepPhase(ctx, dataStore, collectionID, database, a.CompactID, a.VBUUIDs, a.dryRun, terminator, &a.PurgedAttachments)
175+
_, _, err := attachmentCompactSweepPhase(ctx, dataStore, collectionID, database, a.getCompactID(), a.getVBUUIDs(), a.getDryRun(), terminator, &a.PurgedAttachments)
173176
if err != nil || terminator.IsClosed() {
174177
return err
175178
}
@@ -178,7 +181,7 @@ func (a *AttachmentCompactionManager) Run(ctx context.Context, options map[strin
178181
a.SetPhase("cleanup")
179182
worker := func() (shouldRetry bool, err error, value any) {
180183
persistClusterStatus()
181-
metadataKeyPrefix, err := attachmentCompactCleanupPhase(ctx, dataStore, collectionID, database, a.CompactID, a.VBUUIDs, terminator)
184+
metadataKeyPrefix, err := attachmentCompactCleanupPhase(ctx, dataStore, collectionID, database, a.getCompactID(), a.getVBUUIDs(), terminator)
182185
if err != nil {
183186
shouldRetry, err = a.handleAttachmentCompactionRollbackError(ctx, options, dataStore, database, err, CleanupPhase, metadataKeyPrefix)
184187
}
@@ -214,7 +217,7 @@ func (a *AttachmentCompactionManager) handleAttachmentCompactionRollbackError(ct
214217
if errors.As(err, &rollbackErr) || errors.Is(err, base.ErrVbUUIDMismatch) {
215218
base.InfofCtx(ctx, base.KeyDCP, "rollback indicated on %s phase of attachment compaction, resetting the task", phase)
216219
// to rollback any phase for attachment compaction we need to purge all persisted dcp metadata
217-
base.InfofCtx(ctx, base.KeyDCP, "Purging invalid checkpoints for background task run %s", a.CompactID)
220+
base.InfofCtx(ctx, base.KeyDCP, "Purging invalid checkpoints for background task run %s", a.getCompactID())
218221
err = a.purgeCheckpoints(ctx, database, keyPrefix)
219222
if err != nil {
220223
base.WarnfCtx(ctx, "error occurred during purging of dcp metadata: %s", err)
@@ -231,7 +234,7 @@ func (a *AttachmentCompactionManager) handleAttachmentCompactionRollbackError(ct
231234
// we only handle rollback for mark and cleanup so if we call here it will be for cleanup phase
232235
// we need to clear the vbUUID's on the manager for cleanup phase otherwise we will end up in loop of constant rollback
233236
// as these are used for the initial metadata on the client
234-
a.VBUUIDs = nil
237+
a.setVBUUIDs(nil)
235238
}
236239
// we should try again if it is rollback error
237240
return true, nil
@@ -244,7 +247,69 @@ func (a *AttachmentCompactionManager) SetPhase(phase string) {
244247
a.lock.Lock()
245248
defer a.lock.Unlock()
246249

247-
a.Phase = phase
250+
a._phase = phase
251+
}
252+
253+
// setVBUUIDs updates the VBUUIDs stored on the manager.
254+
func (a *AttachmentCompactionManager) setVBUUIDs(vbuuids []uint64) {
255+
a.lock.Lock()
256+
defer a.lock.Unlock()
257+
258+
a._vbuuids = vbuuids
259+
}
260+
261+
// initializeNewRun sets the compactID and dryRun fields for a new compaction run.
262+
func (a *AttachmentCompactionManager) initializeNewRun(compactID string, dryRun bool) {
263+
a.lock.Lock()
264+
defer a.lock.Unlock()
265+
266+
a._compactID = compactID
267+
a._dryRun = dryRun
268+
}
269+
270+
// initializeFromPreviousStatus restores in-memory state from a previously persisted status document
271+
// so that a resumed run starts with the correct accumulated counts and identifiers.
272+
// Returns the compactID and phase.
273+
func (a *AttachmentCompactionManager) initializeFromPreviousStatus(statusDoc AttachmentManagerStatusDoc) (compactID, phase string) {
274+
a.lock.Lock()
275+
defer a.lock.Unlock()
276+
277+
a._compactID = statusDoc.CompactID
278+
a._phase = statusDoc.Phase
279+
a._dryRun = statusDoc.DryRun
280+
a.MarkedAttachments.Set(statusDoc.MarkedAttachments)
281+
a.PurgedAttachments.Set(statusDoc.PurgedAttachments)
282+
a._vbuuids = statusDoc.VBUUIDs
283+
return a._compactID, a._phase
284+
}
285+
286+
// getCompactID returns the unique identifier for the current compaction run.
287+
func (a *AttachmentCompactionManager) getCompactID() string {
288+
a.lock.RLock()
289+
defer a.lock.RUnlock()
290+
return a._compactID
291+
}
292+
293+
// getPhase returns the current compaction phase ("mark", "sweep", "cleanup", or "" when idle).
294+
func (a *AttachmentCompactionManager) getPhase() string {
295+
a.lock.RLock()
296+
defer a.lock.RUnlock()
297+
return a._phase
298+
}
299+
300+
// getDryRun returns whether the compaction is running in dry-run mode, where attachments are
301+
// identified but not deleted.
302+
func (a *AttachmentCompactionManager) getDryRun() bool {
303+
a.lock.RLock()
304+
defer a.lock.RUnlock()
305+
return a._dryRun
306+
}
307+
308+
// getVBUUIDs returns the vBucket UUIDs recorded after the mark phase.
309+
func (a *AttachmentCompactionManager) getVBUUIDs() []uint64 {
310+
a.lock.RLock()
311+
defer a.lock.RUnlock()
312+
return a._vbuuids
248313
}
249314

250315
type AttachmentManagerResponse struct {
@@ -268,20 +333,20 @@ type AttachmentManagerStatusDoc struct {
268333
func (a *AttachmentCompactionManager) SetProcessStatus(context.Context, []byte, []byte) {}
269334

270335
func (a *AttachmentCompactionManager) GetProcessStatus(status BackgroundManagerStatus, _ []byte) ([]byte, []byte, error) {
271-
a.lock.Lock()
272-
defer a.lock.Unlock()
336+
a.lock.RLock()
337+
defer a.lock.RUnlock()
273338

274339
response := AttachmentManagerResponse{
275340
BackgroundManagerStatus: status,
276341
MarkedAttachments: a.MarkedAttachments.Value(),
277342
PurgedAttachments: a.PurgedAttachments.Value(),
278-
CompactID: a.CompactID,
279-
Phase: a.Phase,
280-
DryRun: a.dryRun,
343+
CompactID: a._compactID,
344+
Phase: a._phase,
345+
DryRun: a._dryRun,
281346
}
282347

283348
meta := AttachmentManagerMeta{
284-
VBUUIDs: a.VBUUIDs,
349+
VBUUIDs: a._vbuuids,
285350
}
286351

287352
statusJSON, err := base.JSONMarshal(response)
@@ -303,5 +368,5 @@ func (a *AttachmentCompactionManager) ResetStatus() {
303368

304369
a.MarkedAttachments.Set(0)
305370
a.PurgedAttachments.Set(0)
306-
a.dryRun = false
371+
a._dryRun = false
307372
}

0 commit comments

Comments
 (0)