From 8e202be6e2182afbbd526f8f9cf7ab239675438c Mon Sep 17 00:00:00 2001 From: Shannon Klaus Date: Thu, 5 Dec 2024 11:26:25 -0700 Subject: [PATCH] CLIENT-3168: Support Touched method (#139) --- AerospikeClient/Async/AsyncClient.cs | 48 +++++++++++++++++-- AerospikeClient/Async/AsyncTouch.cs | 34 ++++++++++++++ AerospikeClient/Async/IAsyncClient.cs | 33 +++++++++++-- AerospikeClient/Command/TouchCommand.cs | 24 ++++++++++ AerospikeClient/Main/AerospikeClient.cs | 24 +++++++++- AerospikeClient/Main/IAerospikeClient.cs | 17 ++++++- AerospikeTest/Async/TestAsyncTouch.cs | 56 ++++++++++++++++++++++ AerospikeTest/Sync/Basic/TestTouch.cs | 60 +++++++++++++++++++++--- 8 files changed, 280 insertions(+), 16 deletions(-) create mode 100644 AerospikeTest/Async/TestAsyncTouch.cs diff --git a/AerospikeClient/Async/AsyncClient.cs b/AerospikeClient/Async/AsyncClient.cs index cd196d49..a60e2b70 100644 --- a/AerospikeClient/Async/AsyncClient.cs +++ b/AerospikeClient/Async/AsyncClient.cs @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 Aerospike, Inc. + * Copyright 2012-2024 Aerospike, Inc. * * Portions may be licensed to Aerospike, Inc. under one or more contributor * license agreements. @@ -453,7 +453,7 @@ public void Delete(BatchPolicy batchPolicy, BatchDeletePolicy deletePolicy, Batc new AsyncBatchOperateRecordSequenceExecutor(cluster, batchPolicy, listener, keys, null, attr); } - + //------------------------------------------------------- // Touch Operations //------------------------------------------------------- @@ -461,7 +461,7 @@ public void Delete(BatchPolicy batchPolicy, BatchDeletePolicy deletePolicy, Batc /// /// Asynchronously reset record's time to expiration using the policy's expiration. /// Create listener, call asynchronous touch and return task monitor. - /// Fail if the record does not exist. + /// If the record does not exist, it can't be created because the server deletes empty records. /// /// write configuration parameters, pass in null for defaults /// cancellation token @@ -478,7 +478,7 @@ public Task Touch(WritePolicy policy, CancellationToken token, Key key) /// Asynchronously reset record's time to expiration using the policy's expiration. /// Schedule the touch command with a channel selector and return. /// Another thread will process the command and send the results to the listener. - /// Fail if the record does not exist. + /// If the record does not exist, it can't be created because the server deletes empty records. /// /// write configuration parameters, pass in null for defaults /// where to send results, pass in null for fire and forget @@ -494,6 +494,46 @@ public void Touch(WritePolicy policy, WriteListener listener, Key key) async.Execute(); } + /// + /// Asynchronously reset record's time to expiration using the policy's expiration. + /// Create listener, call asynchronous touched and return task monitor. + /// If the record does not exist, it can't be created because the server deletes empty records. + /// + /// write configuration parameters, pass in null for defaults + /// cancellation token + /// unique record identifier + /// if queue is full + public Task Touched(WritePolicy policy, CancellationToken token, Key key) + { + ExistsListenerAdapter listener = new(token); + Touched(policy, listener, key); + return listener.Task; + } + + /// + /// Asynchronously reset record's time to expiration using the policy's expiration. + /// Schedule the touched command with a channel selector and return. + /// Another thread will process the command and send the results to the listener. + /// If the record does not exist, it can't be created because the server deletes empty records. + /// + /// If the record does not exist, send a value of false to + /// + /// + /// + /// write configuration parameters, pass in null for defaults + /// where to send results, pass in null for fire and forget + /// unique record identifier + /// if queue is full + public void Touched(WritePolicy policy, ExistsListener listener, Key key) + { + if (policy == null) + { + policy = writePolicyDefault; + } + AsyncTouch async = new(cluster, policy, listener, key); + async.Execute(); + } + //------------------------------------------------------- // Existence-Check Operations //------------------------------------------------------- diff --git a/AerospikeClient/Async/AsyncTouch.cs b/AerospikeClient/Async/AsyncTouch.cs index d38f5251..6ca83d1b 100644 --- a/AerospikeClient/Async/AsyncTouch.cs +++ b/AerospikeClient/Async/AsyncTouch.cs @@ -21,14 +21,28 @@ public sealed class AsyncTouch : AsyncSingleCommand { private readonly WritePolicy writePolicy; private readonly WriteListener listener; + private readonly ExistsListener existsListener; private readonly Key key; private readonly Partition partition; + private bool touched; public AsyncTouch(AsyncCluster cluster, WritePolicy writePolicy, WriteListener listener, Key key) : base(cluster, writePolicy) { this.writePolicy = writePolicy; this.listener = listener; + this.existsListener = null; + this.key = key; + this.partition = Partition.Write(cluster, policy, key); + cluster.AddTran(); + } + + public AsyncTouch(AsyncCluster cluster, WritePolicy writePolicy, ExistsListener listener, Key key) + : base(cluster, writePolicy) + { + this.writePolicy = writePolicy; + this.listener = null; + this.existsListener = listener; this.key = key; this.partition = Partition.Write(cluster, policy, key); cluster.AddTran(); @@ -39,6 +53,7 @@ public AsyncTouch(AsyncTouch other) { this.writePolicy = other.writePolicy; this.listener = other.listener; + this.existsListener = other.existsListener; this.key = other.key; this.partition = other.partition; } @@ -74,6 +89,17 @@ protected internal override void ParseResult() if (resultCode == 0) { + touched = true; + return; + } + + touched = false; + if (resultCode == ResultCode.KEY_NOT_FOUND_ERROR) + { + if (existsListener == null) + { + throw new AerospikeException(resultCode); + } return; } @@ -101,6 +127,10 @@ protected internal override void OnSuccess() { listener.OnSuccess(key); } + else if (existsListener != null) + { + existsListener.OnSuccess(key, touched); + } } protected internal override void OnFailure(AerospikeException e) @@ -109,6 +139,10 @@ protected internal override void OnFailure(AerospikeException e) { listener.OnFailure(e); } + else if (existsListener != null) + { + existsListener.OnFailure(e); + } } } } diff --git a/AerospikeClient/Async/IAsyncClient.cs b/AerospikeClient/Async/IAsyncClient.cs index 3b82b58d..90153356 100644 --- a/AerospikeClient/Async/IAsyncClient.cs +++ b/AerospikeClient/Async/IAsyncClient.cs @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 Aerospike, Inc. + * Copyright 2012-2024 Aerospike, Inc. * * Portions may be licensed to Aerospike, Inc. under one or more contributor * license agreements. @@ -265,7 +265,7 @@ public interface IAsyncClient : IAerospikeClient /// /// Asynchronously reset record's time to expiration using the policy's expiration. /// Create listener, call asynchronous touch and return task monitor. - /// Fail if the record does not exist. + /// If the record does not exist, it can't be created because the server deletes empty records. /// /// write configuration parameters, pass in null for defaults /// cancellation token @@ -277,7 +277,7 @@ public interface IAsyncClient : IAerospikeClient /// Asynchronously reset record's time to expiration using the policy's expiration. /// Schedule the touch command with a channel selector and return. /// Another thread will process the command and send the results to the listener. - /// Fail if the record does not exist. + /// If the record does not exist, it can't be created because the server deletes empty records. /// /// write configuration parameters, pass in null for defaults /// where to send results, pass in null for fire and forget @@ -285,6 +285,33 @@ public interface IAsyncClient : IAerospikeClient /// if queue is full void Touch(WritePolicy policy, WriteListener listener, Key key); + /// + /// Asynchronously reset record's time to expiration using the policy's expiration. + /// Create listener, call asynchronous touched and return task monitor. + /// If the record does not exist, it can't be created because the server deletes empty records. + /// + /// write configuration parameters, pass in null for defaults + /// cancellation token + /// unique record identifier + /// if queue is full + Task Touched(WritePolicy policy, CancellationToken token, Key key); + + /// + /// Asynchronously reset record's time to expiration using the policy's expiration. + /// Schedule the touched command with a channel selector and return. + /// Another thread will process the command and send the results to the listener. + /// If the record does not exist, it can't be created because the server deletes empty records. + /// + /// If the record does not exist, send a value of false to + /// + /// + /// + /// write configuration parameters, pass in null for defaults + /// where to send results, pass in null for fire and forget + /// unique record identifier + /// if queue is full + void Touched(WritePolicy policy, ExistsListener listener, Key key); + //------------------------------------------------------- // Existence-Check Operations //------------------------------------------------------- diff --git a/AerospikeClient/Command/TouchCommand.cs b/AerospikeClient/Command/TouchCommand.cs index 245c9b9a..675f0e2a 100644 --- a/AerospikeClient/Command/TouchCommand.cs +++ b/AerospikeClient/Command/TouchCommand.cs @@ -22,6 +22,8 @@ public sealed class TouchCommand : SyncCommand private readonly WritePolicy writePolicy; private readonly Key key; private readonly Partition partition; + private readonly bool failOnNotFound; + internal bool Touched { get; private set; } public TouchCommand(Cluster cluster, WritePolicy writePolicy, Key key) : base(cluster, writePolicy) @@ -29,6 +31,17 @@ public TouchCommand(Cluster cluster, WritePolicy writePolicy, Key key) this.writePolicy = writePolicy; this.key = key; this.partition = Partition.Write(cluster, writePolicy, key); + this.failOnNotFound = true; + cluster.AddTran(); + } + + public TouchCommand(Cluster cluster, WritePolicy writePolicy, Key key, bool failOnNotFound) + : base(cluster, writePolicy) + { + this.writePolicy = writePolicy; + this.key = key; + this.partition = Partition.Write(cluster, writePolicy, key); + this.failOnNotFound = failOnNotFound; cluster.AddTran(); } @@ -58,6 +71,17 @@ protected internal override void ParseResult(Connection conn) if (resultCode == 0) { + Touched = true; + return; + } + + Touched = false; + if (resultCode == ResultCode.KEY_NOT_FOUND_ERROR) + { + if (failOnNotFound) + { + throw new AerospikeException(resultCode); + } return; } diff --git a/AerospikeClient/Main/AerospikeClient.cs b/AerospikeClient/Main/AerospikeClient.cs index 003e8606..617369a4 100644 --- a/AerospikeClient/Main/AerospikeClient.cs +++ b/AerospikeClient/Main/AerospikeClient.cs @@ -661,7 +661,8 @@ public void Truncate(InfoPolicy policy, string ns, string set, DateTime? beforeL /// /// Reset record's time to expiration using the policy's expiration. - /// Fail if the record does not exist. + /// If the record does not exist, it can't be created because the server deletes empty records. + /// Throw an exception if the record does not exist. /// /// write configuration parameters, pass in null for defaults /// unique record identifier @@ -676,6 +677,27 @@ public void Touch(WritePolicy policy, Key key) command.Execute(); } + /// + /// Reset record's time to expiration using the policy's expiration. + /// If the record does not exist, it can't be created because the server deletes empty records. + /// Return true if the record exists and is touched.Return false if the record does not exist. + /// + /// write configuration parameters, pass in null for defaults + /// unique record identifier + /// true if record was touched, false otherwise + /// if touch fails + public bool Touched(WritePolicy policy, Key key) + { + if (policy == null) + { + policy = writePolicyDefault; + } + TouchCommand command = new(cluster, policy, key, false); + command.Execute(); + + return command.Touched; + } + //------------------------------------------------------- // Existence-Check Operations //------------------------------------------------------- diff --git a/AerospikeClient/Main/IAerospikeClient.cs b/AerospikeClient/Main/IAerospikeClient.cs index f8180f88..e941a20e 100644 --- a/AerospikeClient/Main/IAerospikeClient.cs +++ b/AerospikeClient/Main/IAerospikeClient.cs @@ -230,15 +230,30 @@ public interface IAerospikeClient // Touch Operations //------------------------------------------------------- + /// /// Reset record's time to expiration using the policy's expiration. - /// Fail if the record does not exist. + /// If the record does not exist, it can't be created because the server deletes empty records. + /// Throw an exception if the record does not exist. /// /// write configuration parameters, pass in null for defaults /// unique record identifier /// if touch fails void Touch(WritePolicy policy, Key key); + + + /// + /// Reset record's time to expiration using the policy's expiration. + /// If the record does not exist, it can't be created because the server deletes empty records. + /// Return true if the record exists and is touched.Return false if the record does not exist. + /// + /// write configuration parameters, pass in null for defaults + /// unique record identifier + /// true if record was touched, false otherwise + /// if touch fails + bool Touched(WritePolicy policy, Key key); + //------------------------------------------------------- // Existence-Check Operations //------------------------------------------------------- diff --git a/AerospikeTest/Async/TestAsyncTouch.cs b/AerospikeTest/Async/TestAsyncTouch.cs new file mode 100644 index 00000000..a058da48 --- /dev/null +++ b/AerospikeTest/Async/TestAsyncTouch.cs @@ -0,0 +1,56 @@ +/* + * Copyright 2012-2024 Aerospike, Inc. + * + * Portions may be licensed to Aerospike, Inc. under one or more contributor + * license agreements. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +using Aerospike.Client; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Aerospike.Test +{ + [TestClass] + public class TestAsyncTouch : TestAsync + { + [TestMethod] + public void AsyncTouched() + { + Key key = new(args.ns, args.set, "doesNotExistAsyncTouch"); + + client.Touched(null, new TouchListener(this), key); + WaitTillComplete(); + } + + private class TouchListener : ExistsListener + { + private readonly TestAsyncTouch parent; + + public TouchListener(TestAsyncTouch parent) + { + this.parent = parent; + } + + public void OnSuccess(Key key, bool exists) + { + Assert.IsFalse(exists); + parent.NotifyCompleted(); + } + + public void OnFailure(AerospikeException e) + { + parent.SetError(e); + parent.NotifyCompleted(); + } + } + } +} diff --git a/AerospikeTest/Sync/Basic/TestTouch.cs b/AerospikeTest/Sync/Basic/TestTouch.cs index 54fe497e..2d873080 100644 --- a/AerospikeTest/Sync/Basic/TestTouch.cs +++ b/AerospikeTest/Sync/Basic/TestTouch.cs @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 Aerospike, Inc. + * Copyright 2012-2024 Aerospike, Inc. * * Portions may be licensed to Aerospike, Inc. under one or more contributor * license agreements. @@ -23,31 +23,77 @@ namespace Aerospike.Test public class TestTouch : TestSync { [TestMethod] - public void Touch() + public void TouchOperate() { - Key key = new Key(args.ns, args.set, "touchkey"); + Key key = new Key(args.ns, args.set, "TouchOperate"); Bin bin = new Bin(args.GetBinName("touchbin"), "touchvalue"); WritePolicy writePolicy = new WritePolicy(); - writePolicy.expiration = 2; + writePolicy.expiration = 1; client.Put(writePolicy, key, bin); - writePolicy.expiration = 5; + writePolicy.expiration = 2; Record record = client.Operate(writePolicy, key, Operation.Touch(), Operation.GetHeader()); AssertRecordFound(key, record); Assert.AreNotEqual(0, record.expiration); - Util.Sleep(3000); + Util.Sleep(1000); record = client.Get(null, key, bin.name); AssertRecordFound(key, record); - Util.Sleep(4000); + Util.Sleep(3000); record = client.Get(null, key, bin.name); Assert.IsNull(record); } + + [TestMethod] + public void Touch() + { + Key key = new Key(args.ns, args.set, "touch"); + Bin bin = new Bin(args.GetBinName("touchbin"), "touchvalue"); + + WritePolicy writePolicy = new WritePolicy(); + writePolicy.expiration = 1; + + client.Put(writePolicy, key, bin); + + writePolicy.expiration = 2; + client.Touch(writePolicy, key); + + Util.Sleep(1000); + + var record = client.GetHeader(writePolicy, key); + AssertRecordFound(key, record); + Assert.AreNotEqual(0, record.expiration); + + Util.Sleep(3000); + + record = client.GetHeader(null, key); + Assert.IsNull(record); + } + + [TestMethod] + public void Touched() + { + Key key = new(args.ns, args.set, "touched"); + + client.Delete(null, key); + + WritePolicy writePolicy = new(); + writePolicy.expiration = 10; + + bool touched = client.Touched(writePolicy, key); + Assert.IsFalse(touched); + + Bin bin = new("touchBin", "touchValue"); + client.Put(writePolicy, key, bin); + + touched = client.Touched(writePolicy, key); + Assert.IsTrue(touched); + } } }