diff --git a/AerospikeClient/Async/AsyncClient.cs b/AerospikeClient/Async/AsyncClient.cs index 48f3469a..e1647a5b 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. @@ -563,7 +563,7 @@ public void Delete(BatchPolicy batchPolicy, BatchDeletePolicy deletePolicy, Batc AsyncBatchOperateRecordSequenceExecutor executor = new(cluster, batchPolicy, listener, keys, null, attr); AsyncTxnMonitor.ExecuteBatch(batchPolicy, executor, keys); } - + //------------------------------------------------------- // Touch Operations //------------------------------------------------------- @@ -571,7 +571,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 @@ -588,7 +588,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 @@ -604,6 +604,46 @@ public void Touch(WritePolicy policy, WriteListener listener, Key key) AsyncTxnMonitor.Execute(cluster, policy, async); } + /// + /// 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); + AsyncTxnMonitor.Execute(cluster, policy, async); + } + //------------------------------------------------------- // Existence-Check Operations //------------------------------------------------------- diff --git a/AerospikeClient/Async/AsyncTouch.cs b/AerospikeClient/Async/AsyncTouch.cs index 9e8ed5bf..0d0a64b8 100644 --- a/AerospikeClient/Async/AsyncTouch.cs +++ b/AerospikeClient/Async/AsyncTouch.cs @@ -20,17 +20,28 @@ namespace Aerospike.Client public sealed class AsyncTouch : AsyncWriteBase { private readonly WriteListener listener; + private readonly ExistsListener existsListener; + private bool touched; public AsyncTouch(AsyncCluster cluster, WritePolicy writePolicy, WriteListener listener, Key key) : base(cluster, writePolicy, key) { this.listener = listener; + this.existsListener = null; + } + + public AsyncTouch(AsyncCluster cluster, WritePolicy writePolicy, ExistsListener listener, Key key) + : base(cluster, writePolicy, key) + { + this.listener = null; + this.existsListener = listener; } public AsyncTouch(AsyncTouch other) : base(other) { this.listener = other.listener; + this.existsListener = other.existsListener; } protected internal override AsyncCommand CloneCommand() @@ -50,6 +61,17 @@ protected internal override bool ParseResult() if (resultCode == ResultCode.OK) { + touched = true; + return true; + } + + touched = false; + if (resultCode == ResultCode.KEY_NOT_FOUND_ERROR) + { + if (existsListener == null) + { + throw new AerospikeException(resultCode); + } return true; } @@ -71,6 +93,10 @@ protected internal override void OnSuccess() { listener.OnSuccess(Key); } + else if (existsListener != null) + { + existsListener.OnSuccess(Key, touched); + } } protected internal override void OnFailure(AerospikeException e) @@ -79,6 +105,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 ccbbb017..0cfd5b5d 100644 --- a/AerospikeClient/Async/IAsyncClient.cs +++ b/AerospikeClient/Async/IAsyncClient.cs @@ -311,7 +311,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 @@ -323,7 +323,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 @@ -331,6 +331,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 10b6b84c..1d1913bf 100644 --- a/AerospikeClient/Command/TouchCommand.cs +++ b/AerospikeClient/Command/TouchCommand.cs @@ -19,9 +19,18 @@ namespace Aerospike.Client { public sealed class TouchCommand : SyncWriteCommand { + private readonly bool failOnNotFound; + internal bool Touched { get; private set; } public TouchCommand(Cluster cluster, WritePolicy writePolicy, Key key) : base(cluster, writePolicy, key) { + this.failOnNotFound = true; + } + + public TouchCommand(Cluster cluster, WritePolicy writePolicy, Key key, bool failOnNotFound) + : base(cluster, writePolicy, key) + { + this.failOnNotFound = failOnNotFound; } protected internal override void WriteBuffer() @@ -36,6 +45,17 @@ protected internal override void ParseResult(Connection conn) if (resultCode == ResultCode.OK) { + 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 cbf897da..a91de549 100644 --- a/AerospikeClient/Main/AerospikeClient.cs +++ b/AerospikeClient/Main/AerospikeClient.cs @@ -821,7 +821,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 @@ -842,6 +843,32 @@ 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; + } + if (policy.Txn != null) + { + TxnMonitor.AddKey(cluster, policy, key); + } + + 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 210dab9d..2dff73b1 100644 --- a/AerospikeClient/Main/IAerospikeClient.cs +++ b/AerospikeClient/Main/IAerospikeClient.cs @@ -291,15 +291,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); + } } }