@@ -447,27 +447,27 @@ func (db *DatabaseCollectionWithUser) GetCV(ctx context.Context, docid string, c
447447 return db .documentRevisionForRequest (ctx , docid , revision , nil , cv , maxHistory , nil )
448448}
449449
450- // GetDelta attempts to return the delta between fromRevId and toRevId. If the delta can't be generated,
451- // returns nil .
450+ // GetDelta attempts to return the delta between fromRevId and toRevId. If the delta can't be generated, returns nil.
451+ // Delta generation is synchronized per fromRev via a shared revision cache value lock to avoid multiple clients generating the same delta simultaneously .
452452func (db * DatabaseCollectionWithUser ) GetDelta (ctx context.Context , docID , fromRev , toRev string ) (delta * RevisionDelta , redactedRev * DocumentRevision , err error ) {
453-
454453 if docID == "" || fromRev == "" || toRev == "" {
455454 return nil , nil , nil
456455 }
457- var fromRevision DocumentRevision
456+
457+ var initialFromRevision DocumentRevision
458458 var fromRevVrs Version
459459 fromRevIsCV := ! base .IsRevTreeID (fromRev )
460460 if fromRevIsCV {
461461 fromRevVrs , err = ParseVersion (fromRev )
462462 if err != nil {
463463 return nil , nil , err
464464 }
465- fromRevision , err = db .revisionCache .GetWithCV (ctx , docID , & fromRevVrs , RevCacheIncludeDelta )
465+ initialFromRevision , err = db .revisionCache .GetWithCV (ctx , docID , & fromRevVrs , RevCacheIncludeDelta )
466466 if err != nil {
467467 return nil , nil , err
468468 }
469469 } else {
470- fromRevision , err = db .revisionCache .GetWithRev (ctx , docID , fromRev , RevCacheIncludeDelta )
470+ initialFromRevision , err = db .revisionCache .GetWithRev (ctx , docID , fromRev , RevCacheIncludeDelta )
471471 if err != nil {
472472 return nil , nil , err
473473 }
@@ -476,39 +476,60 @@ func (db *DatabaseCollectionWithUser) GetDelta(ctx context.Context, docID, fromR
476476 // If the fromRevision is a removal cache entry (no body), but the user has access to that removal, then just
477477 // return 404 missing to indicate that the body of the revision is no longer available.
478478 // Delta can't be generated if we don't have the fromRevision body.
479- if fromRevision .Removed {
479+ if initialFromRevision .Removed {
480480 return nil , nil , ErrMissing
481481 }
482482
483- // If the fromRevision was a tombstone, then return error to tell delta sync to send full body replication
484- if fromRevision .Deleted {
483+ // If the fromRevision was a tombstone, then return error to tell delta sync to send full body replication.
484+ if initialFromRevision .Deleted {
485485 return nil , nil , base .ErrDeltaSourceIsTombstone
486486 }
487487
488- // If both body and delta are not available for fromRevId, the delta can't be generated
489- if fromRevision .BodyBytes == nil && fromRevision .Delta == nil {
490- return nil , nil , err
488+ // If delta is found, check whether it is a delta for the toRevID we want.
489+ if initialFromRevision .Delta != nil && (initialFromRevision .Delta .ToCV == toRev || initialFromRevision .Delta .ToRevID == toRev ) {
490+ isAuthorized , redactedBody := db .authorizeUserForChannels (docID , toRev , initialFromRevision .CV , initialFromRevision .Delta .ToChannels , initialFromRevision .Delta .ToDeleted , encodeRevisions (ctx , docID , initialFromRevision .Delta .RevisionHistory ))
491+ if ! isAuthorized {
492+ return nil , & redactedBody , nil
493+ }
494+ db .dbStats ().DeltaSync ().DeltaCacheHit .Add (1 )
495+ return initialFromRevision .Delta , nil , nil
491496 }
492497
493- // If delta is found, check whether it is a delta for the toRevID we want
494- if fromRevision .Delta != nil {
495- if fromRevision .Delta .ToCV == toRev || fromRevision .Delta .ToRevID == toRev {
498+ if initialFromRevision .BodyBytes != nil {
499+ // Acquire a delta lock to generate delta (ensuring only one toRev unmarshalling/diff for this fromRev and allow racing clients to share the result)
500+ initialFromRevision .RevCacheValueDeltaLock .Lock ()
501+ defer initialFromRevision .RevCacheValueDeltaLock .Unlock ()
496502
497- isAuthorized , redactedBody := db .authorizeUserForChannels (docID , toRev , fromRevision .CV , fromRevision .Delta .ToChannels , fromRevision .Delta .ToDeleted , encodeRevisions (ctx , docID , fromRevision .Delta .RevisionHistory ))
503+ // fromRevisionForDiff is a version of the fromRevision that is guarded by the delta lock that we will use to generate the delta (or check again for a newly cached delta)
504+ var fromRevisionForDiff DocumentRevision
505+ if fromRevIsCV {
506+ fromRevisionForDiff , err = db .revisionCache .GetWithCV (ctx , docID , & fromRevVrs , RevCacheIncludeDelta )
507+ if err != nil {
508+ return nil , nil , err
509+ }
510+ } else {
511+ fromRevisionForDiff , err = db .revisionCache .GetWithRev (ctx , docID , fromRev , RevCacheIncludeDelta )
512+ if err != nil {
513+ return nil , nil , err
514+ }
515+ }
516+
517+ // Check if another writer beat us to generating the delta and caching it.
518+ if fromRevisionForDiff .Delta != nil && (fromRevisionForDiff .Delta .ToCV == toRev || fromRevisionForDiff .Delta .ToRevID == toRev ) {
519+ isAuthorized , redactedBody := db .authorizeUserForChannels (docID , toRev , fromRevisionForDiff .CV , fromRevisionForDiff .Delta .ToChannels , fromRevisionForDiff .Delta .ToDeleted , encodeRevisions (ctx , docID , fromRevisionForDiff .Delta .RevisionHistory ))
498520 if ! isAuthorized {
499521 return nil , & redactedBody , nil
500522 }
501-
502- // Case 2a. 'some rev' is the rev we're interested in - return the delta
503- // db.DbStats.StatsDeltaSync().Add(base.StatKeyDeltaCacheHits, 1)
504523 db .dbStats ().DeltaSync ().DeltaCacheHit .Add (1 )
505- return fromRevision .Delta , nil , nil
524+ return fromRevisionForDiff .Delta , nil , nil
506525 }
507- }
508526
509- // Delta is unavailable, but the body is available.
510- if fromRevision .BodyBytes != nil {
527+ // Delta can't be generated - returning nil forces a full body replication for toRevId.
528+ if fromRevisionForDiff .BodyBytes == nil {
529+ return nil , nil , nil
530+ }
511531
532+ // Need to generate delta and cache it for others.
512533 db .dbStats ().DeltaSync ().DeltaCacheMiss .Add (1 )
513534 var toRevision DocumentRevision
514535 if ! base .IsRevTreeID (toRev ) {
@@ -537,7 +558,7 @@ func (db *DatabaseCollectionWithUser) GetDelta(ctx context.Context, docID, fromR
537558 return nil , nil , ErrMissing
538559 }
539560
540- // If the revision we're generating a delta to is a tombstone, mark it as such and don't bother generating a delta
561+ // If the revision we're generating a delta to is a tombstone, mark it as such and don't bother generating a delta.
541562 if deleted {
542563 revCacheDelta := newRevCacheDelta ([]byte (base .EmptyDocument ), fromRev , toRevision , deleted , nil )
543564 if fromRevIsCV {
@@ -548,25 +569,25 @@ func (db *DatabaseCollectionWithUser) GetDelta(ctx context.Context, docID, fromR
548569 return & revCacheDelta , nil , nil
549570 }
550571
551- // We didn't unmarshal fromBody earlier (in case we could get by with just the delta), so need do it now
572+ // We didn't unmarshal fromBody earlier (in case we could get by with just the delta), so need do it now.
552573 var fromBodyCopy Body
553- if err := fromBodyCopy .Unmarshal (fromRevision .BodyBytes ); err != nil {
574+ if err := fromBodyCopy .Unmarshal (fromRevisionForDiff .BodyBytes ); err != nil {
554575 return nil , nil , err
555576 }
556577
557- // We didn't unmarshal toBody earlier (in case we could get by with just the delta), so need do it now
578+ // We didn't unmarshal toBody earlier (in case we could get by with just the delta), so need do it now.
558579 var toBodyCopy Body
559580 if err := toBodyCopy .Unmarshal (toRevision .BodyBytes ); err != nil {
560581 return nil , nil , err
561582 }
562583
563584 // If attachments have changed between these revisions, we'll stamp the metadata into the bodies before diffing
564- // so that the resulting delta also contains attachment metadata changes
565- if fromRevision .Attachments != nil {
585+ // so that the resulting delta also contains attachment metadata changes.
586+ if fromRevisionForDiff .Attachments != nil {
566587 // the delta library does not handle deltas in non builtin types,
567588 // so we need the map[string]interface{} type conversion here
568- DeleteAttachmentVersion (fromRevision .Attachments )
569- fromBodyCopy [BodyAttachments ] = map [string ]any (fromRevision .Attachments )
589+ DeleteAttachmentVersion (fromRevisionForDiff .Attachments )
590+ fromBodyCopy [BodyAttachments ] = map [string ]any (fromRevisionForDiff .Attachments )
570591 }
571592
572593 var toRevAttStorageMeta []AttachmentStorageMeta
@@ -583,7 +604,7 @@ func (db *DatabaseCollectionWithUser) GetDelta(ctx context.Context, docID, fromR
583604 }
584605 revCacheDelta := newRevCacheDelta (deltaBytes , fromRev , toRevision , deleted , toRevAttStorageMeta )
585606
586- // Write the newly calculated delta back into the cache before returning
607+ // Write the newly calculated delta back into the cache before returning.
587608 if fromRevIsCV {
588609 db .revisionCache .UpdateDeltaCV (ctx , docID , & fromRevVrs , revCacheDelta )
589610 } else {
@@ -592,6 +613,7 @@ func (db *DatabaseCollectionWithUser) GetDelta(ctx context.Context, docID, fromR
592613 return & revCacheDelta , nil , nil
593614 }
594615
616+ // If both body and delta are not available for fromRevId, the delta can't be generated.
595617 return nil , nil , nil
596618}
597619
0 commit comments