From 5ed5cd1d3819f9e4326c2bebf9719d3e1c4f3504 Mon Sep 17 00:00:00 2001 From: Christophe Chevalier Date: Tue, 26 May 2020 15:23:51 +0200 Subject: [PATCH] Promote value-chekcs as a first class operation supported by the transaction object. - tr.CheckValueAsync(key, expected) will read the actual value and compare it to expected value. Will return a pair with the FdbValueCheckResult and actual value. - This command is the logical equivalent of "(await GetAsync(key)) == expected" but is optimized to reduce memory allocation in the case where the check passes. - Rewrote deferred value-checks to use this command (cleans up the logs) - Renamed ValueCheckFailedInPreviousAttempt(..) into TestValueCheckFromPreviousAttempt(..) since we now return an enum - Renamed ListValueChecksFromPreviousAttempt(..) into GetValueChecksFromPreviousAttempt(..) - Removed the singleton flag HasAtLeastOneFailedValueCheck because it is the equivalent of a giant lock, and each layer should use its own tag anyway. - Added a few more annotation in the transaction logs to help troubleshoot value checks. --- .../Core/IFdbTransactionHandler.cs | 8 + FoundationDB.Client/FdbOperationContext.cs | 81 ++++----- .../FdbTransaction.Snapshot.cs | 14 ++ FoundationDB.Client/FdbTransaction.cs | 30 ++++ .../FdbTransactionExtensions.cs | 19 ++ .../IFdbReadOnlyTransaction.cs | 11 ++ .../Layers/Directories/FdbDirectoryLayer.cs | 2 +- .../Native/FdbNativeTransaction.cs | 30 ++++ .../Tracing/FdbTransactionLog.Commands.cs | 33 ++++ .../Tracing/FdbTransactionLog.cs | 1 + FoundationDB.Tests/TransactionFacts.cs | 166 ++++++++++++++---- 11 files changed, 316 insertions(+), 79 deletions(-) diff --git a/FoundationDB.Client/Core/IFdbTransactionHandler.cs b/FoundationDB.Client/Core/IFdbTransactionHandler.cs index 712883b39..4b443b0da 100644 --- a/FoundationDB.Client/Core/IFdbTransactionHandler.cs +++ b/FoundationDB.Client/Core/IFdbTransactionHandler.cs @@ -111,6 +111,14 @@ public interface IFdbTransactionHandler : IDisposable /// Task GetRangeAsync(KeySelector beginInclusive, KeySelector endExclusive, int limit, bool reverse, int targetBytes, FdbStreamingMode mode, FdbReadMode read, int iteration, bool snapshot, CancellationToken ct); + /// Check the the value of a key in the database snapshot is equal to the expected value. + /// Key to check + /// Expected value of the key + /// Set to true for snapshot reads + /// Token used to cancel the operation from the outside + /// Task that will return a pair of and the actual value of the key in the database. + Task<(FdbValueCheckResult Result, Slice Actual)> CheckValueAsync(ReadOnlySpan key, Slice expected, bool snapshot, CancellationToken ct); + /// Returns a list of public network addresses as strings, one for each of the storage servers responsible for storing and its associated value /// Name of the key whose location is to be queried. /// Token used to cancel the operation from the outside diff --git a/FoundationDB.Client/FdbOperationContext.cs b/FoundationDB.Client/FdbOperationContext.cs index 352cd7462..1d835f4c6 100644 --- a/FoundationDB.Client/FdbOperationContext.cs +++ b/FoundationDB.Client/FdbOperationContext.cs @@ -444,25 +444,21 @@ public TState GetOrCreateLocalData(TToken key, Func fact #region Value Checkers... /// List of outstanding checks that must match in order for the transaction to commit successfully - private List<(Slice Key, Slice ExpectedValue, string Tag, Task ActualValue)>? ValueChecks { get; set; } + private List<(Slice Key, Slice ExpectedValue, string Tag, Task<(FdbValueCheckResult Result, Slice Actual)> ActualValue)>? ValueChecks { get; set; } - /// If true, then a value check was unsuccessfull during the previous iteration of the retry loop. Defaults to false on the first attempt, and is reset to false if the previous attempt failed for an unrelated reason. - /// - /// This can be used by layers wanting to implement deferred cache validation. - /// The layer implementation can short-circuit any cache validation if this is true, and reload everything from the database. - /// Please note that this flag is shared by all the layers that were touched by this transaction, so there can be false-positives (a check from layer A can invalidate the cache from layer B). - /// - public bool HasAtLeastOneFailedValueCheck { get; private set; } - - /// Test if a value check with the specified tag has failed during the previous attempt + /// Return the result of all value checks performed with the specified tag in the previous attempt /// Tag that was passed to a call to (or similar overloads) in the previous attempt of this context, that failed (meaning the expected value did not match with the database) - /// If true there is a very high probability that the cached key value has changed in the database. If false there is a low probabilty that the value has changed in the database. + /// Combined result of all value checks with this tag. + /// If no check was performed with this tag in the previous attempt, and the application is left to decide the odds of the value having changed in the database. + /// If then at least on check with this tag failed, and there is a very high probability that the cached values have changed in the database. + /// If then all checks with this tag passed, and there is a very low probability that the cached values have changed in the database. /// - /// The caller should use this as a hint that any previously cached data is highly likely to be stale, and should be discarded. + /// The caller should use this as a hint about the likelyhood that previously cached data is invalid, and should be discarded. /// Note that this method can fall victim to the ABA pattern, meaning that a subsquent read of the checked key could return the expected value (changed back to its original value by another transaction). /// To reduce the chances of ABA, checked keys should only be updated using atomic increment operations, or use versionstamps if possible. + /// If you need to get more precise results about which key/value pair passed or failed, you can also call with the same tag, which will returns all key/value pairs with their individual results. /// - public FdbValueCheckResult ValueCheckFailedInPreviousAttempt(string tag) + public FdbValueCheckResult TestValueCheckFromPreviousAttempt(string tag) { Dictionary)>? checks; lock (this) @@ -476,7 +472,7 @@ public FdbValueCheckResult ValueCheckFailedInPreviousAttempt(string tag) /// If not-null, only return the checks with the specified tag. /// If not-null, only return the checks with the specified result /// List of value-checks that match the specified filters. - public List<(string Tag, FdbValueCheckResult Result, Slice Key, Slice Expected, Slice Actual)> ListValueChecksFromPreviousAttempt(string? tag = null, FdbValueCheckResult? result = null) + public List<(string Tag, FdbValueCheckResult Result, Slice Key, Slice Expected, Slice Actual)> GetValueChecksFromPreviousAttempt(string? tag = null, FdbValueCheckResult? result = null) { Dictionary Checks)>? checks; lock (this) @@ -513,7 +509,7 @@ public FdbValueCheckResult ValueCheckFailedInPreviousAttempt(string tag) /// Expected value of the key. A value of means the key is expected to NOT exist. /// /// If key does not have the expected value, the transaction will fail to commit, with an error (simulating a conflict), which should trigger a retry, - /// and the property will then be set to true, for the next iteration of the retry-loop. A call to () will also return true. + /// and a call to () will return for the next iteration of the retry-loop. /// Any change to the value of the key _after_ this call, in the same transaction, will not be seen by this check. Only the value seen by this transaction at call time is considered. /// public void AddValueCheck(string tag, Slice key, Slice expectedValue) @@ -522,11 +518,11 @@ public void AddValueCheck(string tag, Slice key, Slice expectedValue) var tr = this.Transaction; if (tr == null) throw new InvalidOperationException(); - var task = tr.GetAsync(key); + var task = tr.CheckValueAsync(key.Span, expectedValue); lock (this) { - (this.ValueChecks ??= new List<(Slice, Slice, string, Task)>()) + (this.ValueChecks ??= new List<(Slice, Slice, string, Task<(FdbValueCheckResult, Slice)>)>()) .Add((key, expectedValue, tag, task)); } } @@ -536,7 +532,7 @@ public void AddValueCheck(string tag, Slice key, Slice expectedValue) /// List of keys to check, and their expected values. A value of means the corresponding key is expected to NOT exist. /// /// If any of the keys does not have the expected value, the transaction will fail to commit, with an error (simulating a conflict), which should trigger a retry, - /// and the property will then be set to true, for the next iteration of the retry-loop. + /// and a call to () will return for the next iteration of the retry-loop. /// Any change to the value of these keys _after_ this call, in the same transaction, will not be seen by this check. Only values seen by this transaction at call time are considered. /// public void AddValueChecks(string tag, IEnumerable> items) @@ -550,7 +546,7 @@ public void AddValueChecks(string tag, IEnumerable> i /// List of keys to check, and their expected values. A value of means the corresponding key is expected to NOT exist. /// /// If any of the keys does not have the expected value, the transaction will fail to commit, with an error (simulating a conflict), which should trigger a retry, - /// and the property will then be set to true, for the next iteration of the retry-loop. + /// and a call to () will return for the next iteration of the retry-loop. /// Any change to the value of these keys _after_ this call, in the same transaction, will not be seen by this check. Only values seen by this transaction at call time are considered. /// public void AddValueChecks(string tag, KeyValuePair[] items) @@ -564,7 +560,7 @@ public void AddValueChecks(string tag, KeyValuePair[] items) /// List of keys to check, and their expected values. A value of means the corresponding key is expected to NOT exist. /// /// If any of the keys does not have the expected value, the transaction will fail to commit, with an error (simulating a conflict), which should trigger a retry, - /// and the property will then be set to true, for the next iteration of the retry-loop. + /// and a call to () will return for the next iteration of the retry-loop. /// Any change to the value of these keys _after_ this call, in the same transaction, will not be seen by this check. Only values seen by this transaction at call time are considered. /// public void AddValueChecks(string tag, ReadOnlySpan> items) @@ -580,17 +576,17 @@ public void AddValueChecks(string tag, ReadOnlySpan> return; } - var taskBuffer = ArrayPool>.Shared.Rent(items.Length); + var taskBuffer = ArrayPool>.Shared.Rent(items.Length); try { for (int i = 0; i < items.Length; i++) { - taskBuffer[i] = tr.GetAsync(items[i].Key); + taskBuffer[i] = tr.CheckValueAsync(items[i].Key.Span, items[i].Value); } lock (this) { - var checks = this.ValueChecks ??= new List<(Slice, Slice, string, Task)>(); + var checks = this.ValueChecks ??= new List<(Slice, Slice, string, Task<(FdbValueCheckResult Result, Slice Actual)>)>(); int capa = checked(checks.Count + items.Length); if (capa > checks.Capacity) checks.Capacity = capa; for (int i = 0; i < items.Length; i++) @@ -601,11 +597,11 @@ public void AddValueChecks(string tag, ReadOnlySpan> } finally { - ArrayPool>.Shared.Return(taskBuffer, clearArray: true); + ArrayPool>.Shared.Return(taskBuffer, clearArray: true); } } - private bool EnsureValueCheck(string tag, Slice key, Slice expectedValue, Slice actualResult) + private bool ObserveValueCheckResult(string tag, Slice key, Slice expectedValue, Slice actualResult, FdbValueCheckResult result) { var tags = (this.FailedValueCheckTags ??= new Dictionary Checks)>(StringComparer.Ordinal)); @@ -616,17 +612,16 @@ private bool EnsureValueCheck(string tag, Slice key, Slice expectedValue, Slice } bool pass; - if (!expectedValue.Equals(actualResult)) + previous.Checks.Add((result, key, expectedValue, actualResult)); + if (result == FdbValueCheckResult.Failed) { - this.HasAtLeastOneFailedValueCheck = true; previous.Result = FdbValueCheckResult.Failed; - previous.Checks.Add((FdbValueCheckResult.Failed, key, expectedValue, actualResult)); if (this.Transaction?.IsLogged() == true) this.Transaction.Annotate($"Failed value-check '{tag}' for {FdbKey.Dump(key)}: expected {expectedValue:V}, actual {actualResult:V}"); pass = false; } else { - previous.Checks.Add((FdbValueCheckResult.Success, key, expectedValue, actualResult)); + Contract.Assert(result == FdbValueCheckResult.Success); pass = true; } @@ -637,7 +632,7 @@ private bool EnsureValueCheck(string tag, Slice key, Slice expectedValue, Slice private ValueTask ValidateValueChecks(bool ignoreFailedTasks) { - List<(Slice, Slice, string, Task)>? checks; + List<(Slice, Slice, string, Task<(FdbValueCheckResult, Slice)>)>? checks; lock (this) { checks = this.ValueChecks; @@ -648,16 +643,18 @@ private ValueTask ValidateValueChecks(bool ignoreFailedTasks) return ValidateValueChecksSlow(checks, ignoreFailedTasks); } - private async ValueTask ValidateValueChecksSlow(List<(Slice Key, Slice ExpectedValue, string Tag, Task ActualValue)> checks, bool ignoreFailedTasks) + private async ValueTask ValidateValueChecksSlow(List<(Slice Key, Slice ExpectedValue, string Tag, Task<(FdbValueCheckResult Result, Slice Actual)> ActualValue)> checks, bool ignoreFailedTasks) { //note: even if it looks like we are sequentially await all tasks, by the time the first one is complete, // the rest of the tasks will probably be already completed as well. Anyway, we need to inspect each individual task // to check for any failed tasks anyway, so we can't use Task.WhenAll(...) here + if (this.Transaction?.IsLogged() == true) this.Transaction.Annotate($"Verifying {checks.Count} pending value-check(s)"); + bool pass = true; foreach (var check in checks) { - Slice result; + (FdbValueCheckResult Result, Slice Actual) result; try { result = await check.ActualValue.ConfigureAwait(false); @@ -666,7 +663,12 @@ private async ValueTask ValidateValueChecksSlow(List<(Slice Key, Slice Exp { continue; } - pass &= EnsureValueCheck(check.Tag, check.Key, check.ExpectedValue, result); + pass &= ObserveValueCheckResult(check.Tag, check.Key, check.ExpectedValue, result.Actual, result.Result); + } + + if (this.Transaction?.IsLogged() == true) + { + this.Transaction.Annotate(pass ? "All value-checks passed" : "At least ony value-check failed!"); } return pass; @@ -978,11 +980,9 @@ internal static async Task ExecuteInternal ExecuteInternal we will copy the result in both fields! if (hasResult && typeof(TIntermediate) == typeof(TResult)) { - intermediate = (TIntermediate) (object?) result; + intermediate = (TIntermediate) (object) result!; } switch (success) @@ -1180,7 +1180,7 @@ internal static async Task ExecuteInternal ExecuteInternal ExecuteInternal GetAddressesForKeyAsync(ReadOnlySpan key) return m_parent.PerformGetAddressesForKeyOperation(key); } + /// + public Task<(FdbValueCheckResult Result, Slice Actual)> CheckValueAsync(ReadOnlySpan key, Slice expected) + { + EnsureCanRead(); + + FdbKey.EnsureKeyIsValid(key); + +#if DEBUG + if (Logging.On && Logging.IsVerbose) Logging.Verbose(this, "ValueCheckAsync", $"Checking the value for '{key.ToString()}'"); +#endif + + return m_parent.PerformValueCheckOperation(key, expected, snapshot: true); + } + void IFdbReadOnlyTransaction.Cancel() { throw new NotSupportedException("You cannot cancel the Snapshot view of a transaction."); diff --git a/FoundationDB.Client/FdbTransaction.cs b/FoundationDB.Client/FdbTransaction.cs index 50e94487c..3c799b8ee 100644 --- a/FoundationDB.Client/FdbTransaction.cs +++ b/FoundationDB.Client/FdbTransaction.cs @@ -593,6 +593,36 @@ private Task PerformGetOperation(ReadOnlySpan key, bool snapshot) } } + /// + public Task<(FdbValueCheckResult Result, Slice Actual)> CheckValueAsync(ReadOnlySpan key, Slice expected) + { + EnsureCanRead(); + + FdbKey.EnsureKeyIsValid(key); + +#if DEBUG + if (Logging.On && Logging.IsVerbose) Logging.Verbose(this, "ValueCheckAsync", $"Checking the value for '{key.ToString()}'"); +#endif + + return PerformValueCheckOperation(key, expected, snapshot: false); + } + + private Task<(FdbValueCheckResult Result, Slice Actual)> PerformValueCheckOperation(ReadOnlySpan key, Slice expected, bool snapshot) + { + if (m_log != null) + { + return m_log.ExecuteAsync( + this, + new FdbTransactionLog.CheckValueCommand(m_log.Grab(key), m_log.Grab(expected)) { Snapshot = snapshot }, + (tr, cmd) => tr.m_handler.CheckValueAsync(cmd.Key.Span, cmd.Expected, cmd.Snapshot, tr.m_cancellation) + ); + } + else + { + return m_handler.CheckValueAsync(key, expected, snapshot: snapshot, m_cancellation); + } + } + #endregion #region GetValues... diff --git a/FoundationDB.Client/FdbTransactionExtensions.cs b/FoundationDB.Client/FdbTransactionExtensions.cs index fd77d361a..69235534e 100644 --- a/FoundationDB.Client/FdbTransactionExtensions.cs +++ b/FoundationDB.Client/FdbTransactionExtensions.cs @@ -1584,6 +1584,25 @@ public static Task GetAddressesForKeyAsync(this IFdbReadOnlyTransactio #endregion + #region CheckValueAsync... + + /// Check if the value from the database snapshot represented by the current transaction is equal to some value. + /// Key to be looked up in the database + /// Expected value for this key + /// Task that will return the value of the key if it is found, Slice.Nil if the key does not exist, or an exception + /// If the is null + /// If the cancellation token is already triggered + /// If the transaction has already been completed + /// If the operation method is called from the Network Thread + /// Return the result of the check, plus the actual value of the key. + public static Task<(FdbValueCheckResult Result, Slice Actual)> CheckValueAsync(this IFdbReadOnlyTransaction trans, Slice key, Slice expected) + { + if (key.IsNull) throw Fdb.Errors.KeyCannotBeNull(); + return trans.CheckValueAsync(key.Span, expected); + } + + #endregion + #region Clear... /// diff --git a/FoundationDB.Client/IFdbReadOnlyTransaction.cs b/FoundationDB.Client/IFdbReadOnlyTransaction.cs index 1e96334fd..a15a5aa8d 100644 --- a/FoundationDB.Client/IFdbReadOnlyTransaction.cs +++ b/FoundationDB.Client/IFdbReadOnlyTransaction.cs @@ -134,6 +134,17 @@ Task GetRangeAsync( [Pure, LinqTunnel] FdbRangeQuery GetRange(KeySelector beginInclusive, KeySelector endExclusive, Func, TResult> selector, FdbRangeOptions? options = null); + /// Check if the value from the database snapshot represented by the current transaction is equal to some value. + /// Key to be looked up in the database + /// Expected value for this key + /// Task that will return the value of the key if it is found, Slice.Nil if the key does not exist, or an exception + /// If the is null + /// If the cancellation token is already triggered + /// If the transaction has already been completed + /// If the operation method is called from the Network Thread + /// Return the result of the check, plus the actual value of the key. + Task<(FdbValueCheckResult Result, Slice Actual)> CheckValueAsync(ReadOnlySpan key, Slice expected); + /// Returns a list of public network addresses as strings, one for each of the storage servers responsible for storing and its associated value /// Name of the key whose location is to be queried. /// Task that will return an array of strings, or an exception diff --git a/FoundationDB.Client/Layers/Directories/FdbDirectoryLayer.cs b/FoundationDB.Client/Layers/Directories/FdbDirectoryLayer.cs index f11253136..b984a7c5f 100644 --- a/FoundationDB.Client/Layers/Directories/FdbDirectoryLayer.cs +++ b/FoundationDB.Client/Layers/Directories/FdbDirectoryLayer.cs @@ -1246,7 +1246,7 @@ internal async ValueTask GetContext(IFdbReadOnlyTransaction trans) var context = Volatile.Read(ref this.Context) ?? this.Layer.Cache; if (context != null) { - if (trans.Context.ValueCheckFailedInPreviousAttempt("DirectoryLayer") != FdbValueCheckResult.Failed) + if (trans.Context.TestValueCheckFromPreviousAttempt("DirectoryLayer") != FdbValueCheckResult.Failed) { // all good! if (AnnotateTransactions) trans.Annotate($"{this.Layer} cache context #{context.ReadVersion} likely still valid (no failed value-checks at attempt #{trans.Context.Retries})"); return context; diff --git a/FoundationDB.Client/Native/FdbNativeTransaction.cs b/FoundationDB.Client/Native/FdbNativeTransaction.cs index df1dd09e9..cee50a17f 100644 --- a/FoundationDB.Client/Native/FdbNativeTransaction.cs +++ b/FoundationDB.Client/Native/FdbNativeTransaction.cs @@ -143,6 +143,18 @@ public void SetReadVersion(long version) FdbNative.TransactionSetReadVersion(m_handle, version); } + private static bool TryPeekValueResultBytes(FutureHandle h, out ReadOnlySpan result) + { + Contract.Requires(h != null); + var err = FdbNative.FutureGetValue(h, out bool present, out result); +#if DEBUG_TRANSACTIONS + Debug.WriteLine("FdbTransaction[].TryPeekValueResultBytes() => err=" + err + ", present=" + present + ", valueLength=" + result.Count); +#endif + Fdb.DieOnError(err); + + return present; + } + private static Slice GetValueResultBytes(FutureHandle h) { Contract.Requires(h != null); @@ -328,7 +340,25 @@ public Task GetKeysAsync(KeySelector[] selectors, bool snapshot, Cancel throw; } return FdbFuture.CreateTaskFromHandleArray(futures, (h) => GetKeyResult(h), ct); + } + public Task<(FdbValueCheckResult Result, Slice Actual)> CheckValueAsync(ReadOnlySpan key, Slice expected, bool snapshot, CancellationToken ct) + { + return FdbFuture.CreateTaskFromHandle( + FdbNative.TransactionGet(m_handle, key, snapshot), + (h) => + { + if (TryPeekValueResultBytes(h, out var actual)) + { // key exists + return !expected.IsNull && expected.Span.SequenceEqual(actual) ? (FdbValueCheckResult.Success, expected) : (FdbValueCheckResult.Failed, Slice.Copy(actual)); + } + else + { // key does not exist, pass only if expected is Nil + return expected.IsNull ? (FdbValueCheckResult.Success, Slice.Nil) : (FdbValueCheckResult.Failed, Slice.Nil); + } + }, + ct + ); } #endregion diff --git a/FoundationDB.Client/Tracing/FdbTransactionLog.Commands.cs b/FoundationDB.Client/Tracing/FdbTransactionLog.Commands.cs index d83a07d5d..d95e465b6 100644 --- a/FoundationDB.Client/Tracing/FdbTransactionLog.Commands.cs +++ b/FoundationDB.Client/Tracing/FdbTransactionLog.Commands.cs @@ -838,6 +838,39 @@ public override string GetResult(KeyResolver resolver) } + public sealed class CheckValueCommand : Command<(FdbValueCheckResult Result, Slice Actual)> + { + /// Selector to a key in the database + public Slice Key { get; } + + /// Selector to a key in the database + public Slice Expected { get; } + + public override Operation Op => Operation.CheckValue; + + public CheckValueCommand(Slice key, Slice expected) + { + this.Key = key; + this.Expected = expected; + } + + public override int? ArgumentBytes => this.Key.Count + this.Expected.Count; + + public override int? ResultBytes => !this.Result.HasValue ? default(int?) : this.Result.Value.Actual.Count; + + public override string GetArguments(KeyResolver resolver) + { + return resolver.Resolve(this.Key) + " =?= " + (this.Expected.IsNull ? "" : this.Expected.ToString("V")); + } + + protected override string Dump((FdbValueCheckResult Result, Slice Actual) value) + { + return value.Actual.IsNull ? $" [{value.Result}]" : $"{value.Actual:V} [{value.Result}]"; + } + + } + + public sealed class GetVersionStampCommand : Command { public override Operation Op => Operation.GetVersionStamp; diff --git a/FoundationDB.Client/Tracing/FdbTransactionLog.cs b/FoundationDB.Client/Tracing/FdbTransactionLog.cs index 8b3bcf6c2..f9d6b9b0e 100644 --- a/FoundationDB.Client/Tracing/FdbTransactionLog.cs +++ b/FoundationDB.Client/Tracing/FdbTransactionLog.cs @@ -707,6 +707,7 @@ public enum Operation GetValues, GetKeys, GetRange, + CheckValue, Watch, GetReadVersion, diff --git a/FoundationDB.Tests/TransactionFacts.cs b/FoundationDB.Tests/TransactionFacts.cs index 8e151065b..e1c8fc537 100644 --- a/FoundationDB.Tests/TransactionFacts.cs +++ b/FoundationDB.Tests/TransactionFacts.cs @@ -672,6 +672,86 @@ public async Task Test_Get_Multiple_Keys() } } + [Test] + public async Task Test_Can_Check_Value() + { + using (var db = await OpenTestDatabaseAsync()) + { + var location = db.Root.ByKey("test").AsTyped(); + await CleanLocation(db, location); + + db.SetDefaultLogHandler(log => Log(log.GetTimingsReport(true))); + + // write a bunch of keys + await db.WriteAsync(async tr => + { + var subspace = await location.Resolve(tr); + tr.Set(subspace["hello"], Value("World!")); + tr.Set(subspace["foo"], Slice.Empty); + }, this.Cancellation); + + async Task Check(IFdbReadOnlyTransaction tr, Slice key, Slice expected, FdbValueCheckResult result, Slice actual) + { + Log($"Check {key} == {expected} ?"); + var res = await tr.CheckValueAsync(key, expected); + Log($"> [{res.Result}], {res.Actual:V}"); + Assert.That(res.Actual, Is.EqualTo(actual), "Check({0} == {1}) => ({2}, {3}).Actual was {4}", key, expected, result, actual, res.Actual); + Assert.That(res.Result, Is.EqualTo(result), "Check({0} == {1}) => ({2}, {3}).Result was {4}", key, expected, result, actual, res.Result); + } + + // hello should only be equal to 'World!', not any other value, empty or nil + using (var tr = await db.BeginTransactionAsync(this.Cancellation)) + { + var subspace = await location.Resolve(tr); + + // hello should only be equal to 'World!', not any other value, empty or nil + await Check(tr, subspace["hello"], Value("World!"), FdbValueCheckResult.Success, Value("World!")); + await Check(tr, subspace["hello"], Value("Le Monde!"), FdbValueCheckResult.Failed, Value("World!")); + await Check(tr, subspace["hello"], Slice.Nil, FdbValueCheckResult.Failed, Value("World!")); + await Check(tr, subspace["hello"], subspace["hello"], FdbValueCheckResult.Failed, Value("World!")); + } + + // foo should only be equal to Empty, *not* Nil or any other value + using (var tr = await db.BeginTransactionAsync(this.Cancellation)) + { + var subspace = await location.Resolve(tr); + await Check(tr, subspace["foo"], Slice.Empty, FdbValueCheckResult.Success, Slice.Empty); + await Check(tr, subspace["foo"], Value("bar"), FdbValueCheckResult.Failed, Slice.Empty); + await Check(tr, subspace["foo"], Slice.Nil, FdbValueCheckResult.Failed, Slice.Empty); + await Check(tr, subspace["foo"], subspace["foo"], FdbValueCheckResult.Failed, Slice.Empty); + } + + // not_found should only be equal to Nil, *not* Empty or any other value + using (var tr = await db.BeginTransactionAsync(this.Cancellation)) + { + var subspace = await location.Resolve(tr); + await Check(tr, subspace["not_found"], Slice.Nil, FdbValueCheckResult.Success, Slice.Nil); + await Check(tr, subspace["not_found"], Slice.Empty, FdbValueCheckResult.Failed, Slice.Nil); + await Check(tr, subspace["not_found"], subspace["not_found"], FdbValueCheckResult.Failed, Slice.Nil); + } + + // checking, changing and checking again: 2nd check should see the modified value! + // not_found should only be equal to Nil, *not* Empty or any other value + using (var tr = await db.BeginTransactionAsync(this.Cancellation)) + { + var subspace = await location.Resolve(tr); + + await Check(tr, subspace["hello"], Value("World!"), FdbValueCheckResult.Success, Value("World!")); + await Check(tr, subspace["not_found"], Slice.Nil, FdbValueCheckResult.Success, Slice.Nil); + + tr.Set(subspace["hello"], Value("Le Monde!")); + await Check(tr, subspace["hello"], Value("Le Monde!"), FdbValueCheckResult.Success, Value("Le Monde!")); + await Check(tr, subspace["hello"], Value("World!"), FdbValueCheckResult.Failed, Value("Le Monde!")); + + tr.Set(subspace["not_found"], Value("Surprise!")); + await Check(tr, subspace["not_found"], Value("Surprise!"), FdbValueCheckResult.Success, Value("Surprise!")); + await Check(tr, subspace["not_found"], Slice.Nil, FdbValueCheckResult.Failed, Value("Surprise!")); + + //note: don't commit! + } + } + } + /// Performs (x OP y) and ensure that the result is correct private async Task PerformAtomicOperationAndCheck(IFdbDatabase db, Slice key, int x, FdbMutationType type, int y) { @@ -2999,6 +3079,7 @@ async Task RunCheck(Func test, Func { + tr.StopLogging(); var subspace = (await location.Resolve(tr))!; tr.ClearRange(subspace.ToRange()); @@ -3010,7 +3091,12 @@ await db.WriteAsync(async tr => await db.WriteAsync(async tr => { - Log($"- Retry #{tr.Context.Retries}: prev={tr.Context.PreviousError}, checkFailed={tr.Context.HasAtLeastOneFailedValueCheck}"); + var checks = tr.Context.GetValueChecksFromPreviousAttempt(result: FdbValueCheckResult.Failed); + Log($"- Retry #{tr.Context.Retries}: prev={tr.Context.PreviousError}, checksFromPrevious={checks.Count}"); + foreach (var check in checks) + { + Log($" > [{check.Tag}]: {check.Result}, {FdbKey.Dump(check.Key)} => {check.Expected:V} vs {check.Actual:V}"); + } if (tr.Context.Retries > 10) Assert.Fail("Too many retries!"); if (!test(tr)) return; @@ -3024,6 +3110,7 @@ await db.WriteAsync(async tr => // read back the witness key to see if commit happened or not. var actual = await db.ReadAsync(async tr => { + tr.StopLogging(); var subspace = await location.Resolve(tr); return await tr.GetAsync(subspace.Encode("Witness")); }, this.Cancellation); @@ -3041,9 +3128,9 @@ await RunCheck( (tr) => { if (tr.Context.Retries == 0) - { // first attepmpt: all should be default - Assert.That(tr.Context.HasAtLeastOneFailedValueCheck, Is.False, "Should be false on first attempt"); - Assert.That(tr.Context.ValueCheckFailedInPreviousAttempt("fooCheck"), Is.Null); + { // first attempt: all should be default + Assert.That(tr.Context.TestValueCheckFromPreviousAttempt("fooCheck"), Is.EqualTo(FdbValueCheckResult.Unknown)); + Assert.That(tr.Context.GetValueChecksFromPreviousAttempt("fooCheck"), Is.Empty); return true; } else @@ -3068,9 +3155,9 @@ await RunCheck( (tr) => { if (tr.Context.Retries == 0) - { // first attepmpt: all should be default - Assert.That(tr.Context.HasAtLeastOneFailedValueCheck, Is.False, "Should be false on first attempt"); - Assert.That(tr.Context.ValueCheckFailedInPreviousAttempt("fooCheck"), Is.Null); + { // first attempt: all should be default + Assert.That(tr.Context.TestValueCheckFromPreviousAttempt("fooCheck"), Is.EqualTo(FdbValueCheckResult.Unknown)); + Assert.That(tr.Context.GetValueChecksFromPreviousAttempt("fooCheck"), Is.Empty); return true; } else @@ -3096,9 +3183,10 @@ await RunCheck( { if (tr.Context.Retries == 0) { // first attepmpt: all should be default - Assert.That(tr.Context.HasAtLeastOneFailedValueCheck, Is.False, "Should be false on first attempt"); - Assert.That(tr.Context.ValueCheckFailedInPreviousAttempt("fooCheck"), Is.Null); - Assert.That(tr.Context.ValueCheckFailedInPreviousAttempt("barCheck"), Is.Null); + Assert.That(tr.Context.TestValueCheckFromPreviousAttempt("fooCheck"), Is.EqualTo(FdbValueCheckResult.Unknown)); + Assert.That(tr.Context.GetValueChecksFromPreviousAttempt("fooCheck"), Is.Empty); + Assert.That(tr.Context.TestValueCheckFromPreviousAttempt("barCheck"), Is.EqualTo(FdbValueCheckResult.Unknown)); + Assert.That(tr.Context.GetValueChecksFromPreviousAttempt("barCheck"), Is.Empty); return true; } else @@ -3127,14 +3215,16 @@ await RunCheck( { case 0: // on first attempt, everything should be default - Assert.That(tr.Context.HasAtLeastOneFailedValueCheck, Is.False, "Should be false on first attempt"); - Assert.That(tr.Context.ValueCheckFailedInPreviousAttempt("fooCheck"), Is.Null); + Assert.That(tr.Context.TestValueCheckFromPreviousAttempt("fooCheck"), Is.EqualTo(FdbValueCheckResult.Unknown)); + Assert.That(tr.Context.GetValueChecksFromPreviousAttempt("fooCheck"), Is.Empty); return true; case 1: // on second attempt, value-check "fooCheck" should be triggered - Assert.That(tr.Context.HasAtLeastOneFailedValueCheck, Is.True, "Should be true on second attempt"); - Assert.That(tr.Context.ValueCheckFailedInPreviousAttempt("fooCheck"), Is.True); - Assert.That(tr.Context.ValueCheckFailedInPreviousAttempt("unrelated"), Is.Null); + Assert.That(tr.Context.TestValueCheckFromPreviousAttempt("fooCheck"), Is.EqualTo(FdbValueCheckResult.Failed)); + Assert.That(tr.Context.GetValueChecksFromPreviousAttempt("fooCheck"), Has.Count.EqualTo(1)); + Assert.That(tr.Context.GetValueChecksFromPreviousAttempt("fooCheck")[0].Result, Is.EqualTo(FdbValueCheckResult.Failed)); + Assert.That(tr.Context.TestValueCheckFromPreviousAttempt("unrelated"), Is.EqualTo(FdbValueCheckResult.Unknown)); + Assert.That(tr.Context.GetValueChecksFromPreviousAttempt("unrelated"), Is.Empty); Assert.That(tr.Context.PreviousError, Is.EqualTo(FdbError.NotCommitted), "Should emulate a 'not_committed'"); return false; // stop default: @@ -3161,14 +3251,16 @@ await RunCheck( { case 0: // on first attempt, everything should be default - Assert.That(tr.Context.HasAtLeastOneFailedValueCheck, Is.False, "Should be false on first attempt"); - Assert.That(tr.Context.ValueCheckFailedInPreviousAttempt("fooCheck"), Is.Null); + Assert.That(tr.Context.TestValueCheckFromPreviousAttempt("fooCheck"), Is.EqualTo(FdbValueCheckResult.Unknown)); + Assert.That(tr.Context.GetValueChecksFromPreviousAttempt("fooCheck"), Is.Empty); return true; case 1: // on second attempt, value-check "fooCheck" should be triggered - Assert.That(tr.Context.HasAtLeastOneFailedValueCheck, Is.True, "Should be true on second attempt"); - Assert.That(tr.Context.ValueCheckFailedInPreviousAttempt("fooCheck"), Is.True); - Assert.That(tr.Context.ValueCheckFailedInPreviousAttempt("unrelated"), Is.Null); + Assert.That(tr.Context.TestValueCheckFromPreviousAttempt("fooCheck"), Is.EqualTo(FdbValueCheckResult.Failed)); + Assert.That(tr.Context.GetValueChecksFromPreviousAttempt("fooCheck"), Has.Count.EqualTo(1)); + Assert.That(tr.Context.GetValueChecksFromPreviousAttempt("fooCheck")[0].Result, Is.EqualTo(FdbValueCheckResult.Failed)); + Assert.That(tr.Context.TestValueCheckFromPreviousAttempt("unrelated"), Is.EqualTo(FdbValueCheckResult.Unknown)); + Assert.That(tr.Context.GetValueChecksFromPreviousAttempt("unrelated"), Is.Empty); Assert.That(tr.Context.PreviousError, Is.EqualTo(FdbError.NotCommitted), "Should emulate a 'not_committed'"); return false; // stop default: @@ -3195,8 +3287,8 @@ await RunCheck( { case 0: // on first attempt, everything should be default - Assert.That(tr.Context.HasAtLeastOneFailedValueCheck, Is.False, "Should be false on first attempt"); - Assert.That(tr.Context.ValueCheckFailedInPreviousAttempt("fooCheck"), Is.Null); + Assert.That(tr.Context.TestValueCheckFromPreviousAttempt("fooCheck"), Is.EqualTo(FdbValueCheckResult.Unknown)); + Assert.That(tr.Context.GetValueChecksFromPreviousAttempt("fooCheck"), Is.Empty); return true; default: // should not fire twice! @@ -3226,8 +3318,8 @@ await RunCheck( { case 0: // on first attempt, everything should be default - Assert.That(tr.Context.HasAtLeastOneFailedValueCheck, Is.False, "Should be false on first attempt"); - Assert.That(tr.Context.ValueCheckFailedInPreviousAttempt("fooCheck"), Is.Null); + Assert.That(tr.Context.TestValueCheckFromPreviousAttempt("fooCheck"), Is.EqualTo(FdbValueCheckResult.Unknown)); + Assert.That(tr.Context.GetValueChecksFromPreviousAttempt("fooCheck"), Is.Empty); return true; default: // should not fire twice! @@ -3257,14 +3349,16 @@ await RunCheck( { case 0: // on first attempt, everything should be default - Assert.That(tr.Context.HasAtLeastOneFailedValueCheck, Is.False, "Should be false on first attempt"); - Assert.That(tr.Context.ValueCheckFailedInPreviousAttempt("fooCheck"), Is.Null); + Assert.That(tr.Context.TestValueCheckFromPreviousAttempt("fooCheck"), Is.EqualTo(FdbValueCheckResult.Unknown)); + Assert.That(tr.Context.GetValueChecksFromPreviousAttempt("fooCheck"), Is.Empty); return true; case 1: // on second attempt, value-check "fooCheck" should be triggered - Assert.That(tr.Context.HasAtLeastOneFailedValueCheck, Is.True, "Should be true on second attempt"); - Assert.That(tr.Context.ValueCheckFailedInPreviousAttempt("fooCheck"), Is.True); - Assert.That(tr.Context.ValueCheckFailedInPreviousAttempt("unrelated"), Is.Null); + Assert.That(tr.Context.TestValueCheckFromPreviousAttempt("fooCheck"), Is.EqualTo(FdbValueCheckResult.Failed)); + Assert.That(tr.Context.GetValueChecksFromPreviousAttempt("fooCheck"), Has.Count.EqualTo(1)); + Assert.That(tr.Context.GetValueChecksFromPreviousAttempt("fooCheck")[0].Result, Is.EqualTo(FdbValueCheckResult.Failed)); + Assert.That(tr.Context.TestValueCheckFromPreviousAttempt("unrelated"), Is.EqualTo(FdbValueCheckResult.Unknown)); + Assert.That(tr.Context.GetValueChecksFromPreviousAttempt("unrelated"), Is.Empty); Assert.That(tr.Context.PreviousError, Is.EqualTo(FdbError.NotCommitted), "Should emulate a 'not_committed'"); return false; // stop default: @@ -3294,14 +3388,16 @@ await RunCheck( { case 0: // on first attempt, everything should be default - Assert.That(tr.Context.HasAtLeastOneFailedValueCheck, Is.False, "Should be false on first attempt"); - Assert.That(tr.Context.ValueCheckFailedInPreviousAttempt("fooCheck"), Is.Null); + Assert.That(tr.Context.TestValueCheckFromPreviousAttempt("fooCheck"), Is.EqualTo(FdbValueCheckResult.Unknown)); + Assert.That(tr.Context.GetValueChecksFromPreviousAttempt("fooCheck"), Is.Empty); return true; case 1: // on second attempt, value-check "fooCheck" should be triggered - Assert.That(tr.Context.HasAtLeastOneFailedValueCheck, Is.True, "Should be true on second attempt"); - Assert.That(tr.Context.ValueCheckFailedInPreviousAttempt("fooCheck"), Is.True); - Assert.That(tr.Context.ValueCheckFailedInPreviousAttempt("unrelated"), Is.Null); + Assert.That(tr.Context.TestValueCheckFromPreviousAttempt("fooCheck"), Is.EqualTo(FdbValueCheckResult.Failed)); + Assert.That(tr.Context.GetValueChecksFromPreviousAttempt("fooCheck"), Has.Count.EqualTo(1)); + Assert.That(tr.Context.GetValueChecksFromPreviousAttempt("fooCheck")[0].Result, Is.EqualTo(FdbValueCheckResult.Failed)); + Assert.That(tr.Context.TestValueCheckFromPreviousAttempt("unrelated"), Is.EqualTo(FdbValueCheckResult.Unknown)); + Assert.That(tr.Context.GetValueChecksFromPreviousAttempt("unrelated"), Is.Empty); Assert.That(tr.Context.PreviousError, Is.EqualTo(FdbError.NotCommitted), "Should emulate a 'not_committed'"); return false; // stop default: @@ -3362,7 +3458,7 @@ await db.WriteAsync(async tr => //note: this subspace does not use the DL so it does not introduce any value checks! var subspace = await location.Resolve(tr); - if (true == tr.Context.ValueCheckFailedInPreviousAttempt("foo")) + if (tr.Context.TestValueCheckFromPreviousAttempt("foo") == FdbValueCheckResult.Failed) { Log("# Oh, no! 'foo' check failed previously, check and initialze the db if required...");