• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

couchbase / sync_gateway / 520

01 Aug 2025 01:44PM UTC coverage: 64.997% (+0.02%) from 64.975%
520

push

jenkins

web-flow
CBG-4734: fix for race when removing a value that is being loaded into rev cache (#7640)

32 of 43 new or added lines in 5 files covered. (74.42%)

11 existing lines in 4 files now uncovered.

40054 of 61624 relevant lines covered (65.0%)

0.74 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

85.82
/db/revision_cache_interface.go
1
/*
2
Copyright 2019-Present Couchbase, Inc.
3

4
Use of this software is governed by the Business Source License included in
5
the file licenses/BSL-Couchbase.txt.  As of the Change Date specified in that
6
file, in accordance with the Business Source License, use of this software will
7
be governed by the Apache License, Version 2.0, included in the file
8
licenses/APL2.txt.
9
*/
10

11
package db
12

13
import (
14
        "context"
15
        "time"
16

17
        "github.com/couchbase/sync_gateway/base"
18
)
19

20
const (
21
        // DefaultRevisionCacheSize is the number of recently-accessed doc revisions to cache in RAM
22
        DefaultRevisionCacheSize uint32 = 5000
23

24
        // DefaultRevisionCacheShardCount is the default number of shards to use for the revision cache
25
        DefaultRevisionCacheShardCount uint16 = 16
26
)
27

28
// RevisionCache is an interface that can be used to fetch a DocumentRevision for a Doc ID and Rev ID pair.
29
type RevisionCache interface {
30

31
        // GetWithRev returns the given revision, and stores if not already cached.
32
        // When includeDelta=true, the returned DocumentRevision will include delta - requires additional locking during retrieval.
33
        GetWithRev(ctx context.Context, docID, revID string, collectionID uint32, includeDelta bool) (DocumentRevision, error)
34

35
        // GetWithCV returns the given revision by CV, and stores if not already cached.
36
        // When includeBody=true, the returned DocumentRevision will include a mutable shallow copy of the marshaled body.
37
        // When includeDelta=true, the returned DocumentRevision will include delta - requires additional locking during retrieval.
38
        GetWithCV(ctx context.Context, docID string, cv *Version, collectionID uint32, includeDelta bool) (DocumentRevision, error)
39

40
        // GetActive returns the current revision for the given doc ID, and stores if not already cached.
41
        GetActive(ctx context.Context, docID string, collectionID uint32) (docRev DocumentRevision, err error)
42

43
        // Peek returns the given revision if present in the cache
44
        Peek(ctx context.Context, docID, revID string, collectionID uint32) (docRev DocumentRevision, found bool)
45

46
        // Put will store the given docRev in the cache
47
        Put(ctx context.Context, docRev DocumentRevision, collectionID uint32)
48

49
        // Upsert will remove existing value and re-create new one
50
        Upsert(ctx context.Context, docRev DocumentRevision, collectionID uint32)
51

52
        // RemoveWithRev evicts a revision from the cache using its revID.
53
        RemoveWithRev(ctx context.Context, docID, revID string, collectionID uint32)
54

55
        // RemoveWithCV evicts a revision from the cache using its current version.
56
        RemoveWithCV(ctx context.Context, docID string, cv *Version, collectionID uint32)
57

58
        RemoveRevOnly(ctx context.Context, docID, revID string, collectionID uint32)
59

60
        // UpdateDelta stores the given toDelta value in the given rev if cached
61
        UpdateDelta(ctx context.Context, docID, revID string, collectionID uint32, toDelta RevisionDelta)
62

63
        // UpdateDeltaCV stores the given toDelta value in the given rev if cached but will look up in cache by cv
64
        UpdateDeltaCV(ctx context.Context, docID string, cv *Version, collectionID uint32, toDelta RevisionDelta)
65
}
66

67
const (
68
        RevCacheIncludeDelta = true
69
        RevCacheOmitDelta    = false
70
)
71

72
// Force compile-time check of all RevisionCache types for interface
73
var _ RevisionCache = &LRURevisionCache{}
74
var _ RevisionCache = &ShardedLRURevisionCache{}
75
var _ RevisionCache = &BypassRevisionCache{}
76

77
// NewRevisionCache returns a RevisionCache implementation for the given config options.
78
func NewRevisionCache(cacheOptions *RevisionCacheOptions, backingStores map[uint32]RevisionCacheBackingStore, cacheStats *base.CacheStats) RevisionCache {
1✔
79

1✔
80
        // If cacheOptions is not passed in, use defaults
1✔
81
        if cacheOptions == nil {
2✔
82
                cacheOptions = DefaultRevisionCacheOptions()
1✔
83
        }
1✔
84

85
        if cacheOptions.MaxItemCount == 0 {
2✔
86
                bypassStat := cacheStats.RevisionCacheBypass
1✔
87
                return NewBypassRevisionCache(backingStores, bypassStat)
1✔
88
        }
1✔
89

90
        cacheHitStat := cacheStats.RevisionCacheHits
1✔
91
        cacheMissStat := cacheStats.RevisionCacheMisses
1✔
92
        cacheNumItemsStat := cacheStats.RevisionCacheNumItems
1✔
93
        cacheMemoryStat := cacheStats.RevisionCacheTotalMemory
1✔
94
        if cacheNumItemsStat.Value() != 0 {
2✔
95
                cacheNumItemsStat.Set(0)
1✔
96
        }
1✔
97
        if cacheMemoryStat.Value() != 0 {
2✔
98
                cacheMemoryStat.Set(0)
1✔
99
        }
1✔
100

101
        if cacheOptions.ShardCount > 1 {
2✔
102
                return NewShardedLRURevisionCache(cacheOptions, backingStores, cacheHitStat, cacheMissStat, cacheNumItemsStat, cacheMemoryStat)
1✔
103
        }
1✔
104

105
        return NewLRURevisionCache(cacheOptions, backingStores, cacheHitStat, cacheMissStat, cacheNumItemsStat, cacheMemoryStat)
1✔
106
}
107

108
type RevisionCacheOptions struct {
109
        MaxItemCount uint32
110
        MaxBytes     int64
111
        ShardCount   uint16
112
}
113

114
func DefaultRevisionCacheOptions() *RevisionCacheOptions {
1✔
115
        return &RevisionCacheOptions{
1✔
116
                MaxItemCount: DefaultRevisionCacheSize,
1✔
117
                ShardCount:   DefaultRevisionCacheShardCount,
1✔
118
        }
1✔
119
}
1✔
120

121
// RevisionCacheBackingStore is the interface required to be passed into a RevisionCache constructor to provide a backing store for loading documents.
122
type RevisionCacheBackingStore interface {
123
        GetDocument(ctx context.Context, docid string, unmarshalLevel DocumentUnmarshalLevel) (doc *Document, err error)
124
        getRevision(ctx context.Context, doc *Document, revid string) ([]byte, AttachmentsMeta, error)
125
        getCurrentVersion(ctx context.Context, doc *Document, cv Version) ([]byte, AttachmentsMeta, error)
126
}
127

128
// collectionRevisionCache is a view of a revision cache for a collection.
129
type collectionRevisionCache struct {
130
        revCache     *RevisionCache
131
        collectionID uint32
132
}
133

134
// NewCollectionRevisionCache returns a view of a revision cache for a collection.
135
func newCollectionRevisionCache(revCache *RevisionCache, collectionID uint32) collectionRevisionCache {
1✔
136
        return collectionRevisionCache{
1✔
137
                revCache:     revCache,
1✔
138
                collectionID: collectionID,
1✔
139
        }
1✔
140
}
1✔
141

142
// Get is for per collection access to Get method
143
func (c *collectionRevisionCache) GetWithRev(ctx context.Context, docID, revID string, includeDelta bool) (DocumentRevision, error) {
1✔
144
        return (*c.revCache).GetWithRev(ctx, docID, revID, c.collectionID, includeDelta)
1✔
145
}
1✔
146

147
// Get is for per collection access to Get method
148
func (c *collectionRevisionCache) GetWithCV(ctx context.Context, docID string, cv *Version, includeDelta bool) (DocumentRevision, error) {
1✔
149
        return (*c.revCache).GetWithCV(ctx, docID, cv, c.collectionID, includeDelta)
1✔
150
}
1✔
151

152
// GetActive is for per collection access to GetActive method
153
func (c *collectionRevisionCache) GetActive(ctx context.Context, docID string) (DocumentRevision, error) {
1✔
154
        return (*c.revCache).GetActive(ctx, docID, c.collectionID)
1✔
155
}
1✔
156

157
// Peek is for per collection access to Peek method
158
func (c *collectionRevisionCache) Peek(ctx context.Context, docID, revID string) (DocumentRevision, bool) {
1✔
159
        return (*c.revCache).Peek(ctx, docID, revID, c.collectionID)
1✔
160
}
1✔
161

162
// Put is for per collection access to Put method
163
func (c *collectionRevisionCache) Put(ctx context.Context, docRev DocumentRevision) {
1✔
164
        (*c.revCache).Put(ctx, docRev, c.collectionID)
1✔
165
}
1✔
166

167
// Upsert is for per collection access to Upsert method
168
func (c *collectionRevisionCache) Upsert(ctx context.Context, docRev DocumentRevision) {
×
169
        (*c.revCache).Upsert(ctx, docRev, c.collectionID)
×
170
}
×
171

172
// RemoveWithRev is for per collection access to Remove method
173
func (c *collectionRevisionCache) RemoveWithRev(ctx context.Context, docID, revID string) {
1✔
174
        (*c.revCache).RemoveWithRev(ctx, docID, revID, c.collectionID)
1✔
175
}
1✔
176

NEW
177
func (c *collectionRevisionCache) RemoveRevOnly(ctx context.Context, docID, revID string) {
×
NEW
178
        (*c.revCache).RemoveRevOnly(ctx, docID, revID, c.collectionID)
×
NEW
179
}
×
180

181
// RemoveWithCV is for per collection access to Remove method
182
func (c *collectionRevisionCache) RemoveWithCV(ctx context.Context, docID string, cv *Version) {
1✔
183
        (*c.revCache).RemoveWithCV(ctx, docID, cv, c.collectionID)
1✔
184
}
1✔
185

186
// UpdateDelta is for per collection access to UpdateDelta method
187
func (c *collectionRevisionCache) UpdateDelta(ctx context.Context, docID, revID string, toDelta RevisionDelta) {
×
188
        (*c.revCache).UpdateDelta(ctx, docID, revID, c.collectionID, toDelta)
×
189
}
×
190

191
// UpdateDeltaCV is for per collection access to UpdateDeltaCV method
192
func (c *collectionRevisionCache) UpdateDeltaCV(ctx context.Context, docID string, cv *Version, toDelta RevisionDelta) {
×
193
        (*c.revCache).UpdateDeltaCV(ctx, docID, cv, c.collectionID, toDelta)
×
194
}
×
195

196
// DocumentRevision stored and returned by the rev cache
197
type DocumentRevision struct {
198
        DocID string
199
        RevID string
200
        // BodyBytes contains the raw document, with no special properties.
201
        BodyBytes   []byte
202
        History     Revisions
203
        Channels    base.Set
204
        Expiry      *time.Time
205
        Attachments AttachmentsMeta
206
        Delta       *RevisionDelta
207
        Deleted     bool
208
        Removed     bool  // True if the revision is a removal.
209
        MemoryBytes int64 // storage of the doc rev bytes measurement, includes size of delta when present too
210
        CV          *Version
211
        hlvHistory  string
212
}
213

214
// MutableBody returns a deep copy of the given document revision as a plain body (without any special properties)
215
// Callers are free to modify any of this body without affecting the document revision.
216
func (rev *DocumentRevision) MutableBody() (b Body, err error) {
1✔
217
        if err := b.Unmarshal(rev.BodyBytes); err != nil {
1✔
218
                return nil, err
×
219
        }
×
220

221
        return b, nil
1✔
222
}
223

224
// Body returns an unmarshalled body that is kept in the document revision to produce shallow copies.
225
// If an unmarshalled copy is not available in the document revision, it makes a copy from the raw body
226
// bytes and stores it in document revision itself before returning the body.
227
func (rev *DocumentRevision) Body() (b Body, err error) {
1✔
228

1✔
229
        if err := b.Unmarshal(rev.BodyBytes); err != nil {
1✔
230
                return nil, err
×
231
        }
×
232

233
        return b, nil
1✔
234
}
235

236
// Inject1xBodyProperties will inject special properties (_rev etc) into document body avoiding unnecessary marshal work
237
func (rev *DocumentRevision) Inject1xBodyProperties(ctx context.Context, db *DatabaseCollectionWithUser, requestedHistory Revisions, attachmentsSince []string, showExp bool) ([]byte, error) {
1✔
238

1✔
239
        kvPairs := []base.KVPair{
1✔
240
                {Key: BodyId, Val: rev.DocID},
1✔
241
                {Key: BodyRev, Val: rev.RevID},
1✔
242
        }
1✔
243

1✔
244
        if requestedHistory != nil {
2✔
245
                kvPairs = append(kvPairs, base.KVPair{Key: BodyRevisions, Val: requestedHistory})
1✔
246
        }
1✔
247

248
        if showExp && rev.Expiry != nil && !rev.Expiry.IsZero() {
2✔
249
                kvPairs = append(kvPairs, base.KVPair{Key: BodyExpiry, Val: rev.Expiry.Format(time.RFC3339)})
1✔
250
        }
1✔
251

252
        if rev.Deleted {
2✔
253
                kvPairs = append(kvPairs, base.KVPair{Key: BodyDeleted, Val: rev.Deleted})
1✔
254
        }
1✔
255

256
        if attachmentsSince != nil {
2✔
257
                if len(rev.Attachments) > 0 {
2✔
258
                        minRevpos := 1
1✔
259
                        if len(attachmentsSince) > 0 {
2✔
260
                                ancestor := rev.History.findAncestor(attachmentsSince)
1✔
261
                                if ancestor != "" {
2✔
262
                                        minRevpos, _ = ParseRevID(ctx, ancestor)
1✔
263
                                        minRevpos++
1✔
264
                                }
1✔
265
                        }
266
                        bodyAtts, err := db.loadAttachmentsData(rev.Attachments, minRevpos, rev.DocID)
1✔
267
                        if err != nil {
1✔
268
                                return nil, err
×
269
                        }
×
270
                        DeleteAttachmentVersion(bodyAtts)
1✔
271
                        kvPairs = append(kvPairs, base.KVPair{Key: BodyAttachments, Val: bodyAtts})
1✔
272
                }
273
        } else if rev.Attachments != nil {
2✔
274
                // Stamp attachment metadata back into the body
1✔
275
                DeleteAttachmentVersion(rev.Attachments)
1✔
276
                kvPairs = append(kvPairs, base.KVPair{Key: BodyAttachments, Val: rev.Attachments})
1✔
277
        }
1✔
278

279
        newBytes, err := base.InjectJSONProperties(rev.BodyBytes, kvPairs...)
1✔
280
        if err != nil {
2✔
281
                return nil, err
1✔
282
        }
1✔
283
        return newBytes, nil
1✔
284
}
285

286
// Mutable1xBody returns a copy of the given document revision as a 1.x style body (with special properties)
287
// Callers are free to modify this body without affecting the document revision.
288
func (rev *DocumentRevision) Mutable1xBody(ctx context.Context, db *DatabaseCollectionWithUser, requestedHistory Revisions, attachmentsSince []string, showExp bool, showCV bool) (b Body, err error) {
1✔
289
        b, err = rev.Body()
1✔
290
        if err != nil {
1✔
291
                return nil, err
×
292
        }
×
293
        if b == nil {
2✔
294
                return nil, base.RedactErrorf("null doc body for docID: %s revID: %s", base.UD(rev.DocID), base.UD(rev.RevID))
1✔
295
        }
1✔
296

297
        b[BodyId] = rev.DocID
1✔
298
        b[BodyRev] = rev.RevID
1✔
299

1✔
300
        // Add revision metadata:
1✔
301
        if requestedHistory != nil {
2✔
302
                b[BodyRevisions] = requestedHistory
1✔
303
        }
1✔
304

305
        if showExp && rev.Expiry != nil && !rev.Expiry.IsZero() {
2✔
306
                b[BodyExpiry] = rev.Expiry.Format(time.RFC3339)
1✔
307
        }
1✔
308

309
        if showCV && rev.CV != nil {
2✔
310
                b[BodyCV] = rev.CV.String()
1✔
311
        }
1✔
312

313
        if rev.Deleted {
2✔
314
                b[BodyDeleted] = true
1✔
315
        }
1✔
316

317
        // Add attachment data if requested:
318
        if attachmentsSince != nil {
2✔
319
                if len(rev.Attachments) > 0 {
2✔
320
                        minRevpos := 1
1✔
321
                        if len(attachmentsSince) > 0 {
2✔
322
                                ancestor := rev.History.findAncestor(attachmentsSince)
1✔
323
                                if ancestor != "" {
2✔
324
                                        minRevpos, _ = ParseRevID(ctx, ancestor)
1✔
325
                                        minRevpos++
1✔
326
                                }
1✔
327
                        }
328
                        bodyAtts, err := db.loadAttachmentsData(rev.Attachments, minRevpos, rev.DocID)
1✔
329
                        if err != nil {
1✔
330
                                return nil, err
×
331
                        }
×
332
                        DeleteAttachmentVersion(bodyAtts)
1✔
333
                        b[BodyAttachments] = bodyAtts
1✔
334
                }
335
        } else if rev.Attachments != nil {
2✔
336
                // Stamp attachment metadata back into the body
1✔
337
                DeleteAttachmentVersion(rev.Attachments)
1✔
338
                b[BodyAttachments] = rev.Attachments
1✔
339
        }
1✔
340

341
        return b, nil
1✔
342
}
343

344
// As1xBytes returns a byte slice representing the 1.x style body, containing special properties (i.e. _id, _rev, _attachments, etc.)
345
func (rev *DocumentRevision) As1xBytes(ctx context.Context, db *DatabaseCollectionWithUser, requestedHistory Revisions, attachmentsSince []string, showExp bool) (b []byte, err error) {
1✔
346
        // inject the special properties
1✔
347
        body1x, err := rev.Inject1xBodyProperties(ctx, db, requestedHistory, attachmentsSince, showExp)
1✔
348
        if err != nil {
1✔
349
                return nil, err
×
350
        }
×
351

352
        return body1x, nil
1✔
353
}
354

355
type IDAndRev struct {
356
        DocID        string
357
        RevID        string
358
        CollectionID uint32
359
}
360

361
type IDandCV struct {
362
        DocID        string
363
        Version      uint64
364
        Source       string
365
        CollectionID uint32
366
}
367

368
// RevisionDelta stores data about a delta between a revision and ToRevID.
369
type RevisionDelta struct {
370
        ToRevID               string                  // Target revID for the delta
371
        ToCV                  string                  // Target CV for the delta
372
        DeltaBytes            []byte                  // The actual delta
373
        AttachmentStorageMeta []AttachmentStorageMeta // Storage metadata of all attachments present on ToRevID
374
        ToChannels            base.Set                // Full list of channels for the to revision
375
        RevisionHistory       []string                // Revision history from parent of ToRevID to source revID, in descending order
376
        HlvHistory            string                  // HLV History in CBL format
377
        ToDeleted             bool                    // Flag if ToRevID is a tombstone
378
        totalDeltaBytes       int64                   // totalDeltaBytes is the total bytes for channels, revisions and body on the delta itself
379
}
380

381
func newRevCacheDelta(deltaBytes []byte, fromRevID string, toRevision DocumentRevision, deleted bool, toRevAttStorageMeta []AttachmentStorageMeta) RevisionDelta {
1✔
382
        revDelta := RevisionDelta{
1✔
383
                ToRevID:               toRevision.RevID,
1✔
384
                ToCV:                  toRevision.CV.String(),
1✔
385
                DeltaBytes:            deltaBytes,
1✔
386
                AttachmentStorageMeta: toRevAttStorageMeta,
1✔
387
                ToChannels:            toRevision.Channels,
1✔
388
                RevisionHistory:       toRevision.History.parseAncestorRevisions(fromRevID),
1✔
389
                HlvHistory:            toRevision.hlvHistory,
1✔
390
                ToDeleted:             deleted,
1✔
391
        }
1✔
392
        revDelta.CalculateDeltaBytes()
1✔
393
        return revDelta
1✔
394
}
1✔
395

396
// This is the RevisionCacheLoaderFunc callback for the context's RevisionCache.
397
// Its job is to load a revision from the bucket when there's a cache miss.
398
func revCacheLoader(ctx context.Context, backingStore RevisionCacheBackingStore, id IDAndRev) (bodyBytes []byte, history Revisions, channels base.Set, removed bool, attachments AttachmentsMeta, deleted bool, expiry *time.Time, hlv *HybridLogicalVector, err error) {
1✔
399
        var doc *Document
1✔
400
        if doc, err = backingStore.GetDocument(ctx, id.DocID, DocUnmarshalSync); doc == nil {
2✔
401
                return bodyBytes, history, channels, removed, attachments, deleted, expiry, hlv, err
1✔
402
        }
1✔
403
        return revCacheLoaderForDocument(ctx, backingStore, doc, id.RevID)
1✔
404
}
405

406
// revCacheLoaderForCv will load a document from the bucket using the CV, compare the fetched doc and the CV specified in the function,
407
// and will still return revid for purpose of populating the Rev ID lookup map on the cache
408
func revCacheLoaderForCv(ctx context.Context, backingStore RevisionCacheBackingStore, id IDandCV) (bodyBytes []byte, history Revisions, channels base.Set, removed bool, attachments AttachmentsMeta, deleted bool, expiry *time.Time, revid string, hlv *HybridLogicalVector, err error) {
1✔
409
        cv := Version{
1✔
410
                Value:    id.Version,
1✔
411
                SourceID: id.Source,
1✔
412
        }
1✔
413
        var doc *Document
1✔
414
        if doc, err = backingStore.GetDocument(ctx, id.DocID, DocUnmarshalSync); doc == nil {
2✔
415
                return bodyBytes, history, channels, removed, attachments, deleted, expiry, revid, hlv, err
1✔
416
        }
1✔
417

418
        return revCacheLoaderForDocumentCV(ctx, backingStore, doc, cv)
1✔
419
}
420

421
// Common revCacheLoader functionality used either during a cache miss (from revCacheLoader), or directly when retrieving current rev from cache
422

423
func revCacheLoaderForDocument(ctx context.Context, backingStore RevisionCacheBackingStore, doc *Document, revid string) (bodyBytes []byte, history Revisions, channels base.Set, removed bool, attachments AttachmentsMeta, deleted bool, expiry *time.Time, hlv *HybridLogicalVector, err error) {
1✔
424
        if bodyBytes, attachments, err = backingStore.getRevision(ctx, doc, revid); err != nil {
2✔
425
                // If we can't find the revision (either as active or conflicted body from the document, or as old revision body backup), check whether
1✔
426
                // the revision was a channel removal. If so, we want to store as removal in the revision cache
1✔
427
                removalBodyBytes, removalHistory, activeChannels, isRemoval, isDelete, isRemovalErr := doc.IsChannelRemoval(ctx, revid)
1✔
428
                if isRemovalErr != nil {
1✔
429
                        return bodyBytes, history, channels, isRemoval, nil, isDelete, nil, hlv, isRemovalErr
×
430
                }
×
431

432
                if isRemoval {
2✔
433
                        return removalBodyBytes, removalHistory, activeChannels, isRemoval, nil, isDelete, nil, hlv, nil
1✔
434
                } else {
2✔
435
                        // If this wasn't a removal, return the original error from getRevision
1✔
436
                        return bodyBytes, history, channels, removed, nil, isDelete, nil, hlv, err
1✔
437
                }
1✔
438
        }
439
        deleted = doc.History[revid].Deleted
1✔
440

1✔
441
        validatedHistory, getHistoryErr := doc.History.getHistory(revid)
1✔
442
        if getHistoryErr != nil {
1✔
443
                return bodyBytes, history, channels, removed, nil, deleted, nil, hlv, getHistoryErr
×
444
        }
×
445
        history = encodeRevisions(ctx, doc.ID, validatedHistory)
1✔
446
        channels = doc.History[revid].Channels
1✔
447
        if doc.HLV != nil {
2✔
448
                hlv = doc.HLV
1✔
449
        }
1✔
450

451
        return bodyBytes, history, channels, removed, attachments, deleted, doc.Expiry, hlv, err
1✔
452
}
453

454
// revCacheLoaderForDocumentCV used either during cache miss (from revCacheLoaderForCv), or used directly when getting current active CV from cache
455
// nolint:staticcheck
456
func revCacheLoaderForDocumentCV(ctx context.Context, backingStore RevisionCacheBackingStore, doc *Document, cv Version) (bodyBytes []byte, history Revisions, channels base.Set, removed bool, attachments AttachmentsMeta, deleted bool, expiry *time.Time, revid string, hlv *HybridLogicalVector, err error) {
1✔
457
        if bodyBytes, attachments, err = backingStore.getCurrentVersion(ctx, doc, cv); err != nil {
2✔
458
                // TODO: CBG-3814 - pending support of channel removal for CV
1✔
459
                base.ErrorfCtx(ctx, "pending CBG-3814 support of channel removal for CV: %v", err)
1✔
460
        }
1✔
461

462
        deleted = doc.Deleted
1✔
463
        channels = doc.SyncData.getCurrentChannels()
1✔
464
        revid = doc.CurrentRev
1✔
465
        hlv = doc.HLV
1✔
466
        validatedHistory, getHistoryErr := doc.History.getHistory(revid)
1✔
467
        if getHistoryErr != nil {
1✔
468
                return bodyBytes, history, channels, removed, attachments, deleted, doc.Expiry, revid, hlv, err
×
469
        }
×
470
        history = encodeRevisions(ctx, doc.ID, validatedHistory)
1✔
471

1✔
472
        return bodyBytes, history, channels, removed, attachments, deleted, doc.Expiry, revid, hlv, err
1✔
473
}
474

475
func (c *DatabaseCollection) getCurrentVersion(ctx context.Context, doc *Document, cv Version) (bodyBytes []byte, attachments AttachmentsMeta, err error) {
1✔
476
        if err = doc.HasCurrentVersion(ctx, cv); err != nil {
2✔
477
                bodyBytes, err = c.getOldRevisionJSON(ctx, doc.ID, base.Crc32cHashString([]byte(cv.String())))
1✔
478
                if err != nil || bodyBytes == nil {
2✔
479
                        return nil, nil, err
1✔
480
                }
1✔
481
        } else {
1✔
482
                bodyBytes, err = doc.BodyBytes(ctx)
1✔
483
                if err != nil {
1✔
484
                        base.WarnfCtx(ctx, "Marshal error when retrieving active current version body: %v", err)
×
485
                        return nil, nil, err
×
486
                }
×
487
        }
488

489
        attachments = doc.Attachments()
1✔
490

1✔
491
        // handle backup revision inline attachments, or pre-2.5 meta
1✔
492
        if inlineAtts, cleanBodyBytes, _, err := extractInlineAttachments(bodyBytes); err != nil {
1✔
493
                return nil, nil, err
×
494
        } else if len(inlineAtts) > 0 {
1✔
495
                // we found some inline attachments, so merge them with attachments, and update the bodies
×
496
                attachments = mergeAttachments(inlineAtts, attachments)
×
497
                bodyBytes = cleanBodyBytes
×
498
        }
×
499
        return bodyBytes, attachments, err
1✔
500
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc