Skip to content

Commit 3e5fe2a

Browse files
committed
NCBC-3985: Add ReviveDocument to MutateInOptions
Motivation ========== Added in 7.1, this is a server feature. It is important for the extReplaceBodyWithXattr extension to transactions, which is a efficiency improvement. So lets do it. Modification ============ We added it as a bucket capability, and added it as another option in the MutateInOption class. Then, in MutateInAsync, we check for it in the BucketCapabilities when using it. Also - this expects the AccessDeleted flag, so we add it when we add this one. Also a new exception if you try to revive a document when it is not a tombstone. Results ======= Added tests, they pass. in MutateInAsync. Change-Id: I2e1158936aafd6f01396eec0fc56782f61a924e5 Reviewed-on: https://review.couchbase.org/c/couchbase-net-client/+/225219 Reviewed-by: Jeffry Morris <jeffrymorris@gmail.com> Tested-by: Build Bot <build@couchbase.com>
1 parent 7b44597 commit 3e5fe2a

File tree

9 files changed

+134
-4
lines changed

9 files changed

+134
-4
lines changed

src/Couchbase/Core/Configuration/Server/BucketCapabilities.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ public static class BucketCapabilities
1818
public const string SUBDOC_REPLICA_READ = "subdoc.ReplicaRead";
1919
public const string NON_DEDUPED_HISTORY = "nonDedupedHistory";
2020
public const string SUBDOC_REPLACE_BODY_WITH_XATTR = "subdoc.ReplaceBodyWithXattr";
21+
public const string SUBDOC_REVIVE_DOCUMENT = "subdoc.ReviveDocument";
2122
}
2223
}
2324

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
using System;
2+
3+
namespace Couchbase.Core.Exceptions.KeyValue
4+
{
5+
public class DocumentAlreadyAliveException : KeyValueException
6+
{
7+
public DocumentAlreadyAliveException()
8+
{
9+
}
10+
11+
public DocumentAlreadyAliveException(IErrorContext context) : base(context)
12+
{
13+
}
14+
15+
public DocumentAlreadyAliveException(IKeyValueErrorContext context) : base(context)
16+
{
17+
}
18+
19+
public DocumentAlreadyAliveException(string message) : base(message)
20+
{
21+
}
22+
23+
public DocumentAlreadyAliveException(string message, Exception innerException) : base(message, innerException)
24+
{
25+
}
26+
}
27+
}
28+
29+
30+
/* ************************************************************
31+
*
32+
* @author Couchbase <info@couchbase.com>
33+
* @copyright 2021 Couchbase, Inc.
34+
*
35+
* Licensed under the Apache License, Version 2.0 (the "License");
36+
* you may not use this file except in compliance with the License.
37+
* You may obtain a copy of the License at
38+
*
39+
* http://www.apache.org/licenses/LICENSE-2.0
40+
*
41+
* Unless required by applicable law or agreed to in writing, software
42+
* distributed under the License is distributed on an "AS IS" BASIS,
43+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
44+
* See the License for the specific language governing permissions and
45+
* limitations under the License.
46+
*
47+
* ************************************************************/

src/Couchbase/Core/IO/Operations/ResponseStatus.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,11 @@ client level failures. They are not supported by all SDKs. */
302302
/// </summary>
303303
SubdocInvalidXattrOrder = 0xd4,
304304

305+
/// <summary>
306+
/// Attempted to ReviveDocument on a document which was not currently a tombstone.
307+
/// </summary>
308+
SubdocCanOnlyReviveDeletedDocuments = 0xd6,
309+
305310
/// <summary>
306311
/// Collection does not exist/Collection Outdated
307312
/// </summary>

src/Couchbase/Core/IO/ResponseStatusExtensions.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,8 @@ public static Exception CreateException(this ResponseStatus status, KeyValueErr
182182
return new DocumentTooDeepException { Context = ctx };
183183
case ResponseStatus.SubDocInvalidCombo:
184184
return new InvalidArgumentException { Context = ctx };
185+
case ResponseStatus.SubdocCanOnlyReviveDeletedDocuments:
186+
return new DocumentAlreadyAliveException { Context = ctx };
185187
case ResponseStatus.DocumentMutationDetected: //maps to nothing
186188
case ResponseStatus.NoReplicasFound: //maps to nothing
187189
case ResponseStatus.InvalidRange: //maps to nothing

src/Couchbase/KeyValue/CouchbaseCollection.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -917,6 +917,13 @@ public async Task<IMutateInResult> MutateInAsync(string id, IEnumerable<MutateIn
917917

918918
docFlags |= SubdocDocFlags.CreateAsDeleted;
919919
}
920+
if (options.ReviveDocumentValue)
921+
{
922+
// We insist on AccessDeleted being set whenever we set ReviveDocument.
923+
if (!_bucket.CurrentConfig?.BucketCapabilities.Contains(BucketCapabilities.SUBDOC_REVIVE_DOCUMENT) == true)
924+
throw new FeatureNotAvailableException(nameof(BucketCapabilities.SUBDOC_REVIVE_DOCUMENT));
925+
docFlags |= SubdocDocFlags.ReviveDocument | SubdocDocFlags.AccessDeleted;
926+
}
920927

921928
if (options.AccessDeletedValue) docFlags |= SubdocDocFlags.AccessDeleted;
922929

src/Couchbase/KeyValue/Options.cs

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2629,6 +2629,8 @@ public class MutateInOptions : ITranscoderOverrideOptions, IKeyValueOptions, ITi
26292629

26302630
internal bool CreateAsDeletedValue { get; private set; }
26312631

2632+
internal bool ReviveDocumentValue { get; private set; }
2633+
26322634
internal bool AccessDeletedValue { get; private set; }
26332635

26342636
internal IRetryStrategy? RetryStrategyValue { get; private set; }
@@ -2791,6 +2793,13 @@ public MutateInOptions CancellationToken(CancellationToken token)
27912793
return this;
27922794
}
27932795

2796+
public MutateInOptions ReviveDocument(bool reviveDocument)
2797+
{
2798+
Debug.Assert(!ReferenceEquals(this, Default), "Default should be immutable");
2799+
ReviveDocumentValue = reviveDocument;
2800+
return this;
2801+
}
2802+
27942803
public MutateInOptions CreateAsDeleted(bool createAsDeleted)
27952804
{
27962805
Debug.Assert(!ReferenceEquals(this, Default), "Default should be immutable");
@@ -2809,7 +2818,7 @@ public MutateInOptions AccessDeleted(bool accessDeleted)
28092818
return this;
28102819
}
28112820

2812-
public void Deconstruct(out TimeSpan expiry, out StoreSemantics storeSemantics, out ulong cas, out (PersistTo, ReplicateTo) durability, out DurabilityLevel durabilityLevel, out TimeSpan? timeout, out CancellationToken token, out ITypeSerializer? serializer, out bool createAsDeleted, out bool accessDeleted, out IRetryStrategy? retryStrategy, out IRequestSpan? requestSpan, out bool preserveTtl, out ITypeTranscoder? transcoder)
2821+
public void Deconstruct(out TimeSpan expiry, out StoreSemantics storeSemantics, out ulong cas, out (PersistTo, ReplicateTo) durability, out DurabilityLevel durabilityLevel, out TimeSpan? timeout, out CancellationToken token, out ITypeSerializer? serializer, out bool createAsDeleted, out bool reviveDocument, out bool accessDeleted, out IRetryStrategy? retryStrategy, out IRequestSpan? requestSpan, out bool preserveTtl, out ITypeTranscoder? transcoder)
28132822
{
28142823
expiry = ExpiryValue;
28152824
storeSemantics = StoreSemanticsValue;
@@ -2820,6 +2829,7 @@ public void Deconstruct(out TimeSpan expiry, out StoreSemantics storeSemantics,
28202829
token = TokenValue;
28212830
serializer = TranscoderValue?.Serializer;
28222831
createAsDeleted = CreateAsDeletedValue;
2832+
reviveDocument = ReviveDocumentValue;
28232833
accessDeleted = AccessDeletedValue;
28242834
retryStrategy = RetryStrategyValue;
28252835
requestSpan = RequestSpanValue;
@@ -2829,8 +2839,8 @@ public void Deconstruct(out TimeSpan expiry, out StoreSemantics storeSemantics,
28292839

28302840
public ReadOnly AsReadOnly()
28312841
{
2832-
this.Deconstruct(out TimeSpan expiry, out StoreSemantics storeSemantics, out ulong cas, out (PersistTo, ReplicateTo) durability, out DurabilityLevel durabilityLevel, out TimeSpan? timeout, out CancellationToken token, out ITypeSerializer? serializer, out bool createAsDeleted, out bool accessDeleted, out IRetryStrategy? retryStrategy, out IRequestSpan? requestSpan, out bool preserveTtl, out ITypeTranscoder? transcoder);
2833-
return new ReadOnly(expiry, storeSemantics, cas, durability, durabilityLevel, timeout, token, serializer, createAsDeleted, accessDeleted, retryStrategy, requestSpan, preserveTtl, transcoder);
2842+
this.Deconstruct(out TimeSpan expiry, out StoreSemantics storeSemantics, out ulong cas, out (PersistTo, ReplicateTo) durability, out DurabilityLevel durabilityLevel, out TimeSpan? timeout, out CancellationToken token, out ITypeSerializer? serializer, out bool createAsDeleted, out bool reviveDocument, out bool accessDeleted, out IRetryStrategy? retryStrategy, out IRequestSpan? requestSpan, out bool preserveTtl, out ITypeTranscoder? transcoder);
2843+
return new ReadOnly(expiry, storeSemantics, cas, durability, durabilityLevel, timeout, token, serializer, createAsDeleted, accessDeleted, reviveDocument, retryStrategy, requestSpan, preserveTtl, transcoder);
28342844
}
28352845

28362846
public record ReadOnly(
@@ -2844,6 +2854,7 @@ public record ReadOnly(
28442854
ITypeSerializer? Serializer,
28452855
bool CreateAsDeleted,
28462856
bool AccessDeleted,
2857+
bool ReviveDocument,
28472858
IRetryStrategy? RetryStrategy,
28482859
IRequestSpan? RequestSpan,
28492860
bool PreserveTtl,

src/Couchbase/KeyValue/StoreSemantics.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ public enum StoreSemantics : byte
2828
/// Allows access to a deleted document's attributes section.
2929
/// Only for internal diagnostic use only and is an unsupported feature.
3030
/// </summary>
31-
AccessDeleted = OpCode.Delete
31+
AccessDeleted = OpCode.Delete,
32+
3233
}
3334
}
3435

src/Couchbase/KeyValue/SubDocFlags.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,13 @@ public enum SubdocDocFlags : byte
3636
/// </summary>
3737
CreateAsDeleted = 0x08,
3838

39+
/// <summary>
40+
/// This will allow the document to no longer be a tombstone. Note that the server
41+
/// will respond with "ReviveDocument cannot be used without AccessDeletec", so we should
42+
/// always send AccessDeleted as well.
43+
///</summary>
44+
ReviveDocument = 0x10,
45+
3946
/// <summary>
4047
/// Specifies that the request is being sent to a replica.
4148
/// </summary>

tests/Couchbase.IntegrationTests/SubdocTests.cs

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -569,6 +569,55 @@ public async Task MutateIn_ReplaceBodyWithXattr_Succeeds()
569569
Assert.Equal(getResult.ContentAs<string>(), JsonConvert.SerializeObject(newDocBody));
570570
}
571571

572+
[Fact]
573+
public async Task MutateIn_ReviveDocument_Succeeds()
574+
{
575+
var collection = await _fixture.GetDefaultCollectionAsync().ConfigureAwait(false);
576+
var documentKey = nameof(MutateIn_ReviveDocument_Succeeds) + Guid.NewGuid();
577+
// first, lets create a tombstone
578+
using (await collection.MutateInAsync(documentKey, builder =>
579+
{
580+
builder.Upsert("test_attr", "foo", isXattr: true);
581+
}, options => options.StoreSemantics(StoreSemantics.Upsert).CreateAsDeleted(true))
582+
.ConfigureAwait(false));
583+
584+
// verify it is a tombstone
585+
var lookupInResult = await collection.LookupInAsync(documentKey, builder =>
586+
{
587+
builder.GetFull();
588+
}, options => options.AccessDeleted(true)).ConfigureAwait(false);
589+
Assert.True(lookupInResult.IsDeleted);
590+
591+
// now revive (we can use the ReplaceBodyWithXattr since we do that with txns)...
592+
using (await collection.MutateInAsync(documentKey, builder =>
593+
{
594+
builder.ReplaceBodyWithXattr("test_attr");
595+
}, options => options.ReviveDocument(true)).ConfigureAwait(false));
596+
// verify not a tombstone
597+
var lookupInResult2 = await collection.LookupInAsync(documentKey, builder =>
598+
{
599+
builder.GetFull();
600+
}, options => options.AccessDeleted(true)).ConfigureAwait(false);
601+
Assert.False(lookupInResult2.IsDeleted);
602+
}
603+
604+
[Fact]
605+
public async Task MutateIn_ReviveDocument_FailsIfDocumentExists()
606+
{
607+
var collection = await _fixture.GetDefaultCollectionAsync().ConfigureAwait(false);
608+
var documentKey = nameof(MutateIn_ReviveDocument_FailsIfDocumentExists);
609+
610+
// regular old non-tombstone document
611+
await collection.UpsertAsync(documentKey, new { foo = "bar" }).ConfigureAwait(false);
612+
613+
using var task = collection.MutateInAsync(documentKey, builder =>
614+
{
615+
builder.Upsert("test_attr", "foo", isXattr: true);
616+
}, options => options.ReviveDocument(true));
617+
618+
await Assert.ThrowsAsync<DocumentAlreadyAliveException>(async () => await task.ConfigureAwait(false));
619+
}
620+
572621
#region Helpers
573622

574623
private class TestDoc

0 commit comments

Comments
 (0)