Skip to content

Commit daaa085

Browse files
authored
CBG-4841 write legacy rev versions with special encoding (#7732)
1 parent 480a3f0 commit daaa085

11 files changed

+382
-70
lines changed

db/blip_handler.go

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1347,10 +1347,25 @@ func (bh *blipHandler) processRev(rq *blip.Message, stats *processRevStats) (err
13471347
forceAllowConflictingTombstone := newDoc.Deleted && (!bh.conflictResolver.IsEmpty() || bh.clientType == BLIPClientTypeSGR2)
13481348
if bh.useHLV() && changeIsVector {
13491349
_, _, _, err = bh.collection.PutExistingCurrentVersion(bh.loggingCtx, newDoc, incomingHLV, rawBucketDoc, legacyRevList, isBlipRevTreeProperty, bh.conflictResolver)
1350-
} else if bh.conflictResolver.revTreeConflictResolver != nil {
1351-
_, _, err = bh.collection.PutExistingRevWithConflictResolution(bh.loggingCtx, newDoc, history, true, bh.conflictResolver.revTreeConflictResolver, forceAllowConflictingTombstone, rawBucketDoc, ExistingVersionWithUpdateToHLV)
13521350
} else {
1353-
_, _, err = bh.collection.PutExistingRev(bh.loggingCtx, newDoc, history, revNoConflicts, forceAllowConflictingTombstone, rawBucketDoc, ExistingVersionWithUpdateToHLV)
1351+
docUpdateEvent := ExistingVersionWithUpdateToHLV
1352+
if bh.useHLV() {
1353+
docUpdateEvent = ExistingVersionLegacyRev
1354+
}
1355+
opts := putDocOptions{
1356+
newDoc: newDoc,
1357+
revTreeHistory: history,
1358+
forceAllowConflictingTombstone: forceAllowConflictingTombstone,
1359+
existingDoc: rawBucketDoc,
1360+
docUpdateEvent: docUpdateEvent,
1361+
}
1362+
if bh.conflictResolver.revTreeConflictResolver != nil {
1363+
opts.conflictResolver = bh.conflictResolver.revTreeConflictResolver
1364+
opts.noConflicts = true
1365+
} else {
1366+
opts.noConflicts = revNoConflicts
1367+
}
1368+
_, _, err = bh.collection.PutExistingRevWithConflictResolution(bh.loggingCtx, opts)
13541369
}
13551370
if err != nil {
13561371
return err

db/crud.go

Lines changed: 43 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1016,9 +1016,22 @@ func (db *DatabaseCollectionWithUser) updateHLV(ctx context.Context, d *Document
10161016
}
10171017
// update the cvCAS on the SGWrite event too
10181018
d.HLV.CurrentVersionCAS = expandMacroCASValueUint64
1019+
case ExistingVersionLegacyRev:
1020+
revTreeEncodedCV, err := LegacyRevToRevTreeEncodedVersion(d.GetRevTreeID())
1021+
if err != nil {
1022+
return nil, err
1023+
}
1024+
err = d.HLV.AddVersion(revTreeEncodedCV)
1025+
if err != nil {
1026+
return nil, err
1027+
}
1028+
// update the cvCAS on the SGWrite event too
1029+
d.HLV.CurrentVersionCAS = expandMacroCASValueUint64
10191030
case NoHLVUpdateForTest:
10201031
// no hlv update event for testing purposes only (used to simulate pre upgraded write)
10211032
return d, nil
1033+
default:
1034+
return nil, base.RedactErrorf("Unexpected docUpdateEvent %v in updateHLV for doc %s", docUpdateEvent, base.UD(d.ID))
10221035
}
10231036
d.SyncData.SetCV(d.HLV)
10241037
return d, nil
@@ -1449,24 +1462,45 @@ func (db *DatabaseCollectionWithUser) PutExistingCurrentVersion(ctx context.Cont
14491462

14501463
// Adds an existing revision to a document along with its history (list of rev IDs.)
14511464
func (db *DatabaseCollectionWithUser) PutExistingRev(ctx context.Context, newDoc *Document, docHistory []string, noConflicts bool, forceAllConflicts bool, existingDoc *sgbucket.BucketDocument, docUpdateEvent DocUpdateType) (doc *Document, newRevID string, err error) {
1452-
return db.PutExistingRevWithConflictResolution(ctx, newDoc, docHistory, noConflicts, nil, forceAllConflicts, existingDoc, docUpdateEvent)
1465+
opts := putDocOptions{
1466+
newDoc: newDoc,
1467+
revTreeHistory: docHistory,
1468+
noConflicts: noConflicts,
1469+
forceAllowConflictingTombstone: forceAllConflicts,
1470+
existingDoc: existingDoc,
1471+
docUpdateEvent: docUpdateEvent,
1472+
}
1473+
return db.PutExistingRevWithConflictResolution(ctx, opts)
1474+
}
1475+
1476+
// putDocOptions encapsulates the options for putting a document revision.
1477+
type putDocOptions struct {
1478+
newDoc *Document // the next contents of the incoming document
1479+
revTreeHistory []string // list of rev tree IDs. The first entry must be the revtree ID that will be added.
1480+
docUpdateEvent DocUpdateType // new write, existing write, import etc
1481+
noConflicts bool // If true, return 409 on any conflict writes
1482+
forceAllowConflictingTombstone bool // If true, do not flag an incoming tombstone as a conflict if the existing document is a tombstone
1483+
conflictResolver *ConflictResolver // If provided, will be used to resolve conflicts if noConflicts is false and a conflict is detected
1484+
existingDoc *sgbucket.BucketDocument // optional, prevents fetching the document from the bucket
14531485
}
14541486

1455-
// PutExistingRevWithConflictResolution Adds an existing revision to a document along with its history (list of rev IDs.)
1487+
// PutExistingRevWithConflictResolution adds an existing revision to a document along with its history.
14561488
// If this new revision would result in a conflict:
14571489
// 1. If noConflicts == false, the revision will be added to the rev tree as a conflict
14581490
// 2. If noConflicts == true and a conflictResolverFunc is not provided, a 409 conflict error will be returned
14591491
// 3. If noConflicts == true and a conflictResolverFunc is provided, conflicts will be resolved and the result added to the document.
1460-
func (db *DatabaseCollectionWithUser) PutExistingRevWithConflictResolution(ctx context.Context, newDoc *Document, docHistory []string, noConflicts bool, conflictResolver *ConflictResolver, forceAllowConflictingTombstone bool, existingDoc *sgbucket.BucketDocument, docUpdateEvent DocUpdateType) (doc *Document, newRevID string, err error) {
1461-
newRev := docHistory[0]
1492+
func (db *DatabaseCollectionWithUser) PutExistingRevWithConflictResolution(ctx context.Context, opts putDocOptions) (doc *Document, newRevID string, err error) {
1493+
newRev := opts.revTreeHistory[0]
14621494
generation, _ := ParseRevID(ctx, newRev)
14631495
if generation < 0 {
14641496
return nil, "", base.HTTPErrorf(http.StatusBadRequest, "Invalid revision ID")
14651497
}
14661498

1499+
newDoc := opts.newDoc
1500+
docHistory := opts.revTreeHistory
14671501
allowImport := db.UseXattrs()
14681502
updateRevCache := true
1469-
doc, _, err = db.updateAndReturnDoc(ctx, newDoc.ID, allowImport, &newDoc.DocExpiry, nil, docUpdateEvent, existingDoc, false, updateRevCache, func(doc *Document) (resultDoc *Document, resultAttachmentData updatedAttachments, createNewRevIDSkipped bool, updatedExpiry *uint32, resultErr error) {
1503+
doc, _, err = db.updateAndReturnDoc(ctx, newDoc.ID, allowImport, &newDoc.DocExpiry, nil, opts.docUpdateEvent, opts.existingDoc, false, updateRevCache, func(doc *Document) (resultDoc *Document, resultAttachmentData updatedAttachments, createNewRevIDSkipped bool, updatedExpiry *uint32, resultErr error) {
14701504
// (Be careful: this block can be invoked multiple times if there are races!)
14711505

14721506
var isSgWrite bool
@@ -1507,13 +1541,13 @@ func (db *DatabaseCollectionWithUser) PutExistingRevWithConflictResolution(ctx c
15071541
// Conflict-free mode check
15081542

15091543
// We only bypass conflict resolution for incoming tombstones if the local doc is also a tombstone
1510-
allowConflictingTombstone := forceAllowConflictingTombstone && doc.IsDeleted()
1544+
allowConflictingTombstone := opts.forceAllowConflictingTombstone && doc.IsDeleted()
15111545

1512-
if !allowConflictingTombstone && db.IsIllegalConflict(ctx, doc, parent, newDoc.Deleted, noConflicts, docHistory) {
1513-
if conflictResolver == nil {
1546+
if !allowConflictingTombstone && db.IsIllegalConflict(ctx, doc, parent, newDoc.Deleted, opts.noConflicts, docHistory) {
1547+
if opts.conflictResolver == nil {
15141548
return nil, nil, false, nil, base.HTTPErrorf(http.StatusConflict, "Document revision conflict")
15151549
}
1516-
_, updatedHistory, err := db.resolveConflict(ctx, doc, newDoc, docHistory, conflictResolver)
1550+
_, updatedHistory, err := db.resolveConflict(ctx, doc, newDoc, docHistory, opts.conflictResolver)
15171551
if err != nil {
15181552
base.InfofCtx(ctx, base.KeyCRUD, "Error resolving conflict for %s: %v", base.UD(doc.ID), err)
15191553
return nil, nil, false, nil, err

db/database.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ const (
5353
Import DocUpdateType = iota
5454
NewVersion
5555
ExistingVersion
56+
ExistingVersionLegacyRev
5657
ExistingVersionWithUpdateToHLV
5758
NoHLVUpdateForTest
5859
)

db/hybrid_logical_vector.go

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"fmt"
1616
"iter"
1717
"maps"
18+
"math/bits"
1819
"slices"
1920
"sort"
2021
"strconv"
@@ -24,6 +25,10 @@ import (
2425
"github.com/couchbase/sync_gateway/base"
2526
)
2627

28+
// encodedRevTreeSourceID is a base64 encoded value representing a revision that came from a 4.x client with a
29+
// revtree ID.
30+
const encodedRevTreeSourceID = "Revision+Tree+Encoding"
31+
2732
type HLVVersions map[string]uint64 // map of source ID to version uint64 version value
2833

2934
// sorted will iterate through the map returning entries in a stable sorted order. Used by testing to make it easier
@@ -518,7 +523,7 @@ func extractHLVFromBlipString(versionVectorStr string) (*HybridLogicalVector, []
518523
return nil, nil, err
519524
}
520525
if legacyRevs != nil {
521-
return nil, nil, fmt.Errorf("invalid hlv in changes message received, legacy revID found in cv: %q", vectorFields[0])
526+
return nil, nil, fmt.Errorf("invalid hlv in changes message received, legacy revIDs found in cv %s", versionVectorStr)
522527
}
523528
for i, v := range cvmvList {
524529
switch i {
@@ -626,7 +631,7 @@ func isLegacyRev(rev string) bool {
626631
return true
627632
}
628633

629-
// Helper functions for version source and value encoding
634+
// EncodeSource encodes a source ID in base64 for storage.
630635
func EncodeSource(source string) string {
631636
return base64.StdEncoding.EncodeToString([]byte(source))
632637
}
@@ -901,3 +906,27 @@ func remoteWinsConflictResolutionForHLV(ctx context.Context, docID string, local
901906
base.DebugfCtx(ctx, base.KeyVV, "resolved conflict for doc %s in favour of remote wins, resulting HLV: %v", base.UD(docID), newHLV)
902907
return newHLV, nil
903908
}
909+
910+
// LegacyRevToRevTreeEncodedVersion creates a version that has a specific source ID that can be recognized. The version is made up of:
911+
//
912+
// - The upper 24 bits of the version are the generation.
913+
// - The lower 40 bits of the version are the first 40 bits of the digest, which is right padded.
914+
func LegacyRevToRevTreeEncodedVersion(legacyRev string) (Version, error) {
915+
generation, digest, err := parseRevID(legacyRev)
916+
if err != nil {
917+
return Version{}, err
918+
}
919+
// trim to 40 bits (10 hex characters)
920+
if len(digest) > 10 {
921+
digest = digest[:10]
922+
}
923+
value, err := strconv.ParseUint(digest, 16, 64)
924+
if err != nil {
925+
return Version{}, err
926+
}
927+
value = value << (40 - bits.Len64(value)) // right pad zeros
928+
return Version{
929+
SourceID: encodedRevTreeSourceID,
930+
Value: (uint64(generation) << 40) | value,
931+
}, nil
932+
}

db/hybrid_logical_vector_test.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1660,3 +1660,38 @@ func TestLocalWinsConflictResolutionForHLV(t *testing.T) {
16601660
})
16611661
}
16621662
}
1663+
1664+
func TestLegacyRevToVersion(t *testing.T) {
1665+
testCases := []struct {
1666+
legacyRev string
1667+
version string
1668+
}{
1669+
{
1670+
legacyRev: "1-abcd",
1671+
version: "1abcd000000@Revision+Tree+Encoding",
1672+
},
1673+
{
1674+
legacyRev: "12345-e0c8012361e94df6a1e1c2977169480e",
1675+
version: "3039e0c8012361@Revision+Tree+Encoding",
1676+
},
1677+
{
1678+
legacyRev: "16777215-abcd",
1679+
version: "ffffffabcd000000@Revision+Tree+Encoding",
1680+
},
1681+
{
1682+
legacyRev: "16777216-abcd",
1683+
version: "abcd000000@Revision+Tree+Encoding",
1684+
},
1685+
{
1686+
legacyRev: "16777217-abcd",
1687+
version: "1abcd000000@Revision+Tree+Encoding",
1688+
},
1689+
}
1690+
for _, tc := range testCases {
1691+
t.Run(tc.legacyRev, func(t *testing.T) {
1692+
version, err := LegacyRevToRevTreeEncodedVersion(tc.legacyRev)
1693+
require.NoError(t, err)
1694+
assert.Equal(t, tc.version, version.String())
1695+
})
1696+
}
1697+
}

db/revision.go

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -365,28 +365,38 @@ func genOfRevID(ctx context.Context, revid string) int {
365365
return generation
366366
}
367367

368-
// Splits a revision ID into generation number and hex digest.
369-
func ParseRevID(ctx context.Context, revid string) (int, string) {
370-
if revid == "" {
368+
// ParseRevID splits a revision ID into generation number and hex digest. Logs a warning and returns -1, "" if the revID is invalid.
369+
func ParseRevID(ctx context.Context, revID string) (int, string) {
370+
if revID == "" {
371371
return 0, ""
372372
}
373+
generation, digest, err := parseRevID(revID)
374+
if err != nil {
375+
base.WarnfCtx(ctx, "%s", err)
376+
return -1, ""
377+
}
378+
return generation, digest
379+
}
380+
381+
// parseRevID splits a revision ID into generation number and hex digest. Returns an error if the revID is invalid.
382+
func parseRevID(revid string) (int, string, error) {
383+
if revid == "" {
384+
return 0, "", errors.New("empty revID passed to parseRevID")
385+
}
373386

374387
idx := strings.Index(revid, "-")
375388
if idx == -1 {
376-
base.WarnfCtx(ctx, "parseRevID found no separator in rev %q", revid)
377-
return -1, ""
389+
return -1, "", fmt.Errorf("parseRevID found no separator in rev %q", revid)
378390
}
379391

380392
gen, err := strconv.Atoi(revid[:idx])
381393
if err != nil {
382-
base.WarnfCtx(ctx, "parseRevID unexpected generation in rev %q: %s", revid, err)
383-
return -1, ""
394+
return -1, "", fmt.Errorf("parseRevID unexpected generation in rev %q: %s", revid, err)
384395
} else if gen < 1 {
385-
base.WarnfCtx(ctx, "parseRevID unexpected generation in rev %q", revid)
386-
return -1, ""
396+
return -1, "", fmt.Errorf("parseRevID unexpected generation in rev %q", revid)
387397
}
388398

389-
return gen, revid[idx+1:]
399+
return gen, revid[idx+1:], nil
390400
}
391401

392402
// compareRevIDs compares the two rev IDs and returns:

0 commit comments

Comments
 (0)