diff --git a/Enyim.Caching.Tests/MemcachedClientMutateTests.cs b/Enyim.Caching.Tests/MemcachedClientMutateTests.cs index be9b275a..07564789 100644 --- a/Enyim.Caching.Tests/MemcachedClientMutateTests.cs +++ b/Enyim.Caching.Tests/MemcachedClientMutateTests.cs @@ -2,34 +2,48 @@ using System.Collections.Generic; using System.Linq; using System.Text; +using System.Threading.Tasks; using Xunit; namespace Enyim.Caching.Tests { - public class MemcachedClientMutateTests : MemcachedClientTestsBase - { - [Fact] - public void When_Incrementing_Value_Result_Is_Successful() - { - var key = GetUniqueKey("mutate"); - var mutateResult = _client.ExecuteIncrement(key, 100, 10); - MutateAssertPass(mutateResult, 100); + public class MemcachedClientMutateTests : MemcachedClientTestsBase + { + [Fact] + public void When_Incrementing_Value_Result_Is_Successful() + { + var key = GetUniqueKey("mutate"); + var mutateResult = _client.ExecuteIncrement(key, 100, 10); + MutateAssertPass(mutateResult, 100); - mutateResult = _client.ExecuteIncrement(key, 100, 10); - MutateAssertPass(mutateResult, 110); - } + mutateResult = _client.ExecuteIncrement(key, 100, 10); + MutateAssertPass(mutateResult, 110); + } - [Fact] - public void When_Decrementing_Value_Result_Is_Successful() - { - var key = GetUniqueKey("mutate"); - var mutateResult = _client.ExecuteDecrement(key, 100, 10); - MutateAssertPass(mutateResult, 100); + [Fact] + public void When_Decrementing_Value_Result_Is_Successful() + { + var key = GetUniqueKey("mutate"); + var mutateResult = _client.ExecuteDecrement(key, 100, 10); + MutateAssertPass(mutateResult, 100); - mutateResult = _client.ExecuteDecrement(key, 100, 10); - MutateAssertPass(mutateResult, 90); - } - } + mutateResult = _client.ExecuteDecrement(key, 100, 10); + MutateAssertPass(mutateResult, 90); + } + + [Fact] + public async Task When_Touch_Item_Result_Is_Successful() + { + var key = GetUniqueKey("touch"); + await _client.AddAsync(key, "value", 1); + Assert.True((await _client.GetAsync(key)).Success); + var result = await _client.TouchAsync(key, TimeSpan.FromSeconds(60)); + await Task.Delay(1010); + Assert.True(result.Success, "Success was false"); + Assert.True((result.StatusCode ?? 0) == 0, "StatusCode was not null or 0"); + Assert.True((await _client.GetAsync(key)).Success); + } + } } #region [ License information ] diff --git a/Enyim.Caching/IMemcachedClient.cs b/Enyim.Caching/IMemcachedClient.cs index 9bd2146f..1a873916 100644 --- a/Enyim.Caching/IMemcachedClient.cs +++ b/Enyim.Caching/IMemcachedClient.cs @@ -13,7 +13,7 @@ public interface IMemcachedClient : IDisposable bool Set(string key, object value, int cacheSeconds); Task SetAsync(string key, object value, int cacheSeconds); - + bool Replace(string key, object value, int cacheSeconds); Task ReplaceAsync(string key, object value, int cacheSeconds); @@ -66,6 +66,9 @@ public interface IMemcachedClient : IDisposable CasResult Increment(string key, ulong defaultValue, ulong delta, DateTime expiresAt, ulong cas); CasResult Increment(string key, ulong defaultValue, ulong delta, TimeSpan validFor, ulong cas); + Task TouchAsync(string key, DateTime expiresAt); + Task TouchAsync(string key, TimeSpan validFor); + bool Remove(string key); Task RemoveAsync(string key); diff --git a/Enyim.Caching/Memcached/Enums.cs b/Enyim.Caching/Memcached/Enums.cs index b20ef2b6..413c9094 100644 --- a/Enyim.Caching/Memcached/Enums.cs +++ b/Enyim.Caching/Memcached/Enums.cs @@ -1,16 +1,17 @@ +using Enyim.Caching.Memcached.Protocol.Binary; using System; namespace Enyim.Caching.Memcached { - public enum MutationMode : byte { Increment = 0x05, Decrement = 0x06 }; - public enum ConcatenationMode : byte { Append = 0x0E, Prepend = 0x0F }; - public enum MemcachedProtocol { Binary, Text } + public enum MutationMode : byte { Increment = 0x05, Decrement = 0x06, Touch = OpCode.Touch }; + public enum ConcatenationMode : byte { Append = 0x0E, Prepend = 0x0F }; + public enum MemcachedProtocol { Binary, Text } } #region [ License information ] /* ************************************************************ * - * Copyright (c) 2010 Attila Kiskó, enyim.com + * Copyright (c) 2010 Attila Kisk? enyim.com * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/Enyim.Caching/Memcached/IOperationFactory.cs b/Enyim.Caching/Memcached/IOperationFactory.cs index fb468dcb..929695d6 100644 --- a/Enyim.Caching/Memcached/IOperationFactory.cs +++ b/Enyim.Caching/Memcached/IOperationFactory.cs @@ -5,19 +5,19 @@ namespace Enyim.Caching.Memcached { - public interface IOperationFactory - { - IGetOperation Get(string key); - IMultiGetOperation MultiGet(IList keys); + public interface IOperationFactory + { + IGetOperation Get(string key); + IMultiGetOperation MultiGet(IList keys); - IStoreOperation Store(StoreMode mode, string key, CacheItem value, uint expires, ulong cas); - IDeleteOperation Delete(string key, ulong cas); - IMutatorOperation Mutate(MutationMode mode, string key, ulong defaultValue, ulong delta, uint expires, ulong cas); - IConcatOperation Concat(ConcatenationMode mode, string key, ulong cas, ArraySegment data); + IStoreOperation Store(StoreMode mode, string key, CacheItem value, uint expires, ulong cas); + IDeleteOperation Delete(string key, ulong cas); + IMutatorOperation Mutate(MutationMode mode, string key, ulong defaultValue, ulong delta, uint expires, ulong cas); + IConcatOperation Concat(ConcatenationMode mode, string key, ulong cas, ArraySegment data); - IStatsOperation Stats(string type); - IFlushOperation Flush(); - } + IStatsOperation Stats(string type); + IFlushOperation Flush(); + } } #region [ License information ] diff --git a/Enyim.Caching/Memcached/MemcachedNode.cs b/Enyim.Caching/Memcached/MemcachedNode.cs index 6d6bd2be..4c6c69f0 100755 --- a/Enyim.Caching/Memcached/MemcachedNode.cs +++ b/Enyim.Caching/Memcached/MemcachedNode.cs @@ -863,6 +863,7 @@ protected virtual async Task ExecuteOperationAsync(IOperati } else { + _logger.LogInformation($"{op}.{nameof(op.ReadResponseAsync)} result: {readResult.Message}"); readResult.Combine(result); } return result; diff --git a/Enyim.Caching/Memcached/Protocol/Binary/ConcatOperation.cs b/Enyim.Caching/Memcached/Protocol/Binary/ConcatOperation.cs index ac4beb43..7082dfa4 100644 --- a/Enyim.Caching/Memcached/Protocol/Binary/ConcatOperation.cs +++ b/Enyim.Caching/Memcached/Protocol/Binary/ConcatOperation.cs @@ -5,49 +5,49 @@ namespace Enyim.Caching.Memcached.Protocol.Binary { - /// - /// Implements append/prepend. - /// - public class ConcatOperation : BinarySingleItemOperation, IConcatOperation - { - private ArraySegment data; - private ConcatenationMode mode; + /// + /// Implements append/prepend. + /// + public class ConcatOperation : BinarySingleItemOperation, IConcatOperation + { + private readonly ArraySegment data; + private readonly ConcatenationMode mode; - public ConcatOperation(ConcatenationMode mode, string key, ArraySegment data) - : base(key) - { - this.data = data; - this.mode = mode; - } + public ConcatOperation(ConcatenationMode mode, string key, ArraySegment data) + : base(key) + { + this.data = data; + this.mode = mode; + } - protected override BinaryRequest Build() - { - var request = new BinaryRequest((OpCode)this.mode) - { - Key = this.Key, - Cas = this.Cas, - Data = this.data - }; + protected override BinaryRequest Build() + { + var request = new BinaryRequest((OpCode)this.mode) + { + Key = this.Key, + Cas = this.Cas, + Data = this.data + }; - return request; - } + return request; + } - protected override IOperationResult ProcessResponse(BinaryResponse response) - { - return new BinaryOperationResult() { Success = true }; - } + protected override IOperationResult ProcessResponse(BinaryResponse response) + { + return new BinaryOperationResult() { Success = true }; + } - ConcatenationMode IConcatOperation.Mode - { - get { return this.mode; } - } - } + ConcatenationMode IConcatOperation.Mode + { + get { return this.mode; } + } + } } #region [ License information ] /* ************************************************************ * - * Copyright (c) 2010 Attila Kiskó, enyim.com + * Copyright (c) 2010 Attila Kisk? enyim.com * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/Enyim.Caching/Memcached/Protocol/Binary/MutatorOperation.cs b/Enyim.Caching/Memcached/Protocol/Binary/MutatorOperation.cs index 0af7d6b3..0eb67a75 100644 --- a/Enyim.Caching/Memcached/Protocol/Binary/MutatorOperation.cs +++ b/Enyim.Caching/Memcached/Protocol/Binary/MutatorOperation.cs @@ -2,93 +2,106 @@ using Enyim.Caching.Memcached.Results.Extensions; using Enyim.Caching.Memcached.Results.Helpers; using Enyim.Caching.Memcached.Results; +using System.Buffers.Binary; namespace Enyim.Caching.Memcached.Protocol.Binary { - public class MutatorOperation : BinarySingleItemOperation, IMutatorOperation - { - private ulong defaultValue; - private ulong delta; - private uint expires; - private MutationMode mode; - private ulong result; - - public MutatorOperation(MutationMode mode, string key, ulong defaultValue, ulong delta, uint expires) - : base(key) - { - if (delta < 0) throw new ArgumentOutOfRangeException("delta", "delta must be >= 0"); - - this.defaultValue = defaultValue; - this.delta = delta; - this.expires = expires; - this.mode = mode; - } - - protected unsafe void UpdateExtra(BinaryRequest request) - { - byte[] extra = new byte[20]; - - fixed (byte* buffer = extra) - { - BinaryConverter.EncodeUInt64(this.delta, buffer, 0); - - BinaryConverter.EncodeUInt64(this.defaultValue, buffer, 8); - BinaryConverter.EncodeUInt32(this.expires, buffer, 16); - } - - request.Extra = new ArraySegment(extra); - } - - protected override BinaryRequest Build() - { - var request = new BinaryRequest((OpCode)this.mode) - { - Key = this.Key, - Cas = this.Cas - }; - - this.UpdateExtra(request); - - return request; - } - - protected override IOperationResult ProcessResponse(BinaryResponse response) - { - var result = new BinaryOperationResult(); - var status = response.StatusCode; - this.StatusCode = status; - - if (status == 0) - { - var data = response.Data; - if (data.Count != 8) - return result.Fail("Result must be 8 bytes long, received: " + data.Count, new InvalidOperationException()); - - this.result = BinaryConverter.DecodeUInt64(data.Array, data.Offset); - - return result.Pass(); - } - - var message = ResultHelper.ProcessResponseData(response.Data); - return result.Fail(message); - } - - MutationMode IMutatorOperation.Mode - { - get { return this.mode; } - } - - ulong IMutatorOperation.Result - { - get { return this.result; } - } - } + public class MutatorOperation : BinarySingleItemOperation, IMutatorOperation + { + private readonly ulong defaultValue; + private readonly ulong delta; + private readonly uint expires; + private readonly MutationMode mode; + private ulong result; + + public MutatorOperation(MutationMode mode, string key, ulong defaultValue, ulong delta, uint expires) + : base(key) + { + if (delta < 0) throw new ArgumentOutOfRangeException("delta", "delta must be >= 0"); + + this.defaultValue = defaultValue; + this.delta = delta; + this.expires = expires; + this.mode = mode; + } + + protected unsafe void UpdateExtra(BinaryRequest request) + { + if (mode == MutationMode.Touch) + { + Span extra = stackalloc byte[4]; + BinaryPrimitives.WriteUInt32BigEndian(extra, this.expires); + request.Extra = new ArraySegment(extra.ToArray()); + } + else + { + byte[] extra = new byte[20]; + + fixed (byte* buffer = extra) + { + BinaryConverter.EncodeUInt64(this.delta, buffer, 0); + + BinaryConverter.EncodeUInt64(this.defaultValue, buffer, 8); + BinaryConverter.EncodeUInt32(this.expires, buffer, 16); + } + + request.Extra = new ArraySegment(extra); + } + } + + protected override BinaryRequest Build() + { + var request = new BinaryRequest((OpCode)this.mode) + { + Key = this.Key, + Cas = this.Cas + }; + + this.UpdateExtra(request); + + return request; + } + + protected override IOperationResult ProcessResponse(BinaryResponse response) + { + var result = new BinaryOperationResult(); + var status = response.StatusCode; + this.StatusCode = status; + + if (status == 0) + { + if (mode != MutationMode.Touch) + { + var data = response.Data; + if (data.Count != 8) + return result.Fail("Result must be 8 bytes long, received: " + data.Count, new InvalidOperationException()); + + this.result = BinaryConverter.DecodeUInt64(data.Array, data.Offset); + } + + return result.Pass(); + } + + var message = ResultHelper.ProcessResponseData(response.Data); + return result.Fail(message); + } + + MutationMode IMutatorOperation.Mode + { + get { return this.mode; } + } + + ulong IMutatorOperation.Result + { + get { return this.result; } + } + } } #region [ License information ] /* ************************************************************ * - * Copyright (c) 2010 Attila Kiskó, enyim.com + * Copyright (c) 2010 Attila Kisk? enyim.com * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/Enyim.Caching/Memcached/Protocol/Binary/OpCode.cs b/Enyim.Caching/Memcached/Protocol/Binary/OpCode.cs index 3f22b069..d34dd981 100644 --- a/Enyim.Caching/Memcached/Protocol/Binary/OpCode.cs +++ b/Enyim.Caching/Memcached/Protocol/Binary/OpCode.cs @@ -1,47 +1,48 @@ namespace Enyim.Caching.Memcached.Protocol.Binary { - public enum OpCode : byte - { - Get = 0x00, - Set = 0x01, - Add = 0x02, - Replace = 0x03, - Delete = 0x04, - Increment = 0x05, - Decrement = 0x06, - Quit = 0x07, - Flush = 0x08, - GetQ = 0x09, - NoOp = 0x0A, - Version = 0x0B, - GetK = 0x0C, - GetKQ = 0x0D, - Append = 0x0E, - Prepend = 0x0F, - Stat = 0x10, - SetQ = 0x11, - AddQ = 0x12, - ReplaceQ = 0x13, - DeleteQ = 0x14, - IncrementQ = 0x15, - DecrementQ = 0x16, - QuitQ = 0x17, - FlushQ = 0x18, - AppendQ = 0x19, - PrependQ = 0x1A, + public enum OpCode : byte + { + Get = 0x00, + Set = 0x01, + Add = 0x02, + Replace = 0x03, + Delete = 0x04, + Increment = 0x05, + Decrement = 0x06, + Quit = 0x07, + Flush = 0x08, + GetQ = 0x09, + NoOp = 0x0A, + Version = 0x0B, + GetK = 0x0C, + GetKQ = 0x0D, + Append = 0x0E, + Prepend = 0x0F, + Stat = 0x10, + SetQ = 0x11, + AddQ = 0x12, + ReplaceQ = 0x13, + DeleteQ = 0x14, + IncrementQ = 0x15, + DecrementQ = 0x16, + QuitQ = 0x17, + FlushQ = 0x18, + AppendQ = 0x19, + PrependQ = 0x1A, + Touch = 0x1C, - // SASL authentication op-codes - SaslList = 0x20, - SaslStart = 0x21, - SaslStep = 0x22 - }; + // SASL authentication op-codes + SaslList = 0x20, + SaslStart = 0x21, + SaslStep = 0x22 + }; } #region [ License information ] /* ************************************************************ * - * Copyright (c) 2010 Attila Kiskó, enyim.com + * Copyright (c) 2010 Attila Kisk? enyim.com * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/Enyim.Caching/Memcached/Protocol/Text/MutatorOperation.cs b/Enyim.Caching/Memcached/Protocol/Text/MutatorOperation.cs index 6e1d065e..4a30c153 100644 --- a/Enyim.Caching/Memcached/Protocol/Text/MutatorOperation.cs +++ b/Enyim.Caching/Memcached/Protocol/Text/MutatorOperation.cs @@ -44,7 +44,7 @@ protected internal override IOperationResult ReadResponse(PooledSocket socket) //maybe we should throw an exception when the item is not found? if (String.Compare(response, "NOT_FOUND", StringComparison.Ordinal) == 0) - return result.Fail("Failed to read response. Item not found"); + return result.Fail("Failed to read response. Item not found"); result.Success = UInt64.TryParse(response, NumberStyles.AllowLeadingWhite | NumberStyles.AllowTrailingWhite, CultureInfo.InvariantCulture, out this.result); diff --git a/Enyim.Caching/Memcached/Results/Helpers/ResultHelper.cs b/Enyim.Caching/Memcached/Results/Helpers/ResultHelper.cs index 0aad132b..70f9889d 100644 --- a/Enyim.Caching/Memcached/Results/Helpers/ResultHelper.cs +++ b/Enyim.Caching/Memcached/Results/Helpers/ResultHelper.cs @@ -7,29 +7,28 @@ namespace Enyim.Caching.Memcached.Results.Helpers { - public static class ResultHelper - { + public static class ResultHelper + { - public static string ProcessResponseData(ArraySegment data, string message = "") - { + public static string ProcessResponseData(ArraySegment data, string message = "") + { + if (data != null && data.Count > 0) + { + try + { + return message + + (!string.IsNullOrEmpty(message) ? ": " : "") + + Encoding.UTF8.GetString(data.Array, data.Offset, data.Count); + } + catch (Exception ex) + { + return ex.GetBaseException().Message; + } + } - if (data != null && data.Count > 0) - { - try - { - return message + - (! string.IsNullOrEmpty(message) ? ": " : "") + - Encoding.UTF8.GetString(data.Array, data.Offset, data.Count); - } - catch (Exception ex) - { - return ex.GetBaseException().Message; - } - } - - return string.Empty; - } - } + return string.Empty; + } + } } #region [ License information ] diff --git a/Enyim.Caching/Memcached/StoreMode.cs b/Enyim.Caching/Memcached/StoreMode.cs index 063ccd9c..27580a96 100755 --- a/Enyim.Caching/Memcached/StoreMode.cs +++ b/Enyim.Caching/Memcached/StoreMode.cs @@ -1,58 +1,58 @@ namespace Enyim.Caching.Memcached { - /// - /// Inidicates the mode how the items are stored in Memcached. - /// - public enum StoreMode - { - /// - /// Store the data, but only if the server does not already hold data for a given key - /// - Add = 1, - /// - /// Store the data, but only if the server does already hold data for a given key - /// - Replace, - /// - /// Store the data, overwrite if already exist - /// - Set - }; + /// + /// Inidicates the mode how the items are stored in Memcached. + /// + public enum StoreMode + { + /// + /// Store the data, but only if the server does not already hold data for a given key + /// + Add = 1, + /// + /// Store the data, but only if the server does already hold data for a given key + /// + Replace, + /// + /// Store the data, overwrite if already exist + /// + Set + }; - internal enum StoreCommand - { - /// - /// Store the data, but only if the server does not already hold data for a given key - /// - Add = 1, - /// - /// Store the data, but only if the server does already hold data for a given key - /// - Replace, - /// - /// Store the data, overwrite if already exist - /// - Set, - /// - /// Appends the data to an existing key's data - /// - Append, - /// - /// Inserts the data before an existing key's data - /// - Prepend, - /// - /// Stores the data only if it has not been updated by someone else. Uses a "transaction id" to check for modification. - /// - CheckAndSet - }; + internal enum StoreCommand + { + /// + /// Store the data, but only if the server does not already hold data for a given key + /// + Add = 1, + /// + /// Store the data, but only if the server does already hold data for a given key + /// + Replace, + /// + /// Store the data, overwrite if already exist + /// + Set, + /// + /// Appends the data to an existing key's data + /// + Append, + /// + /// Inserts the data before an existing key's data + /// + Prepend, + /// + /// Stores the data only if it has not been updated by someone else. Uses a "transaction id" to check for modification. + /// + CheckAndSet + }; } #region [ License information ] /* ************************************************************ * - * Copyright (c) 2010 Attila Kiskó, enyim.com + * Copyright (c) 2010 Attila Kisk? enyim.com * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/Enyim.Caching/MemcachedClient.cs b/Enyim.Caching/MemcachedClient.cs index bbfa3c81..a6a1aa10 100755 --- a/Enyim.Caching/MemcachedClient.cs +++ b/Enyim.Caching/MemcachedClient.cs @@ -744,6 +744,18 @@ public CasResult Decrement(string key, ulong defaultValue, ulong delta, D #endregion + #region Touch + public async Task TouchAsync(string key, DateTime expiresAt) + { + return await PerformMutateAsync(MutationMode.Touch, key, 0, 0, GetExpiration(null, expiresAt)); + } + + public async Task TouchAsync(string key, TimeSpan validFor) + { + return await PerformMutateAsync(MutationMode.Touch, key, 0, 0, GetExpiration(validFor, null)); + } + #endregion + private IMutateOperationResult PerformMutate(MutationMode mode, string key, ulong defaultValue, ulong delta, uint expires) { ulong tmp = 0; @@ -792,6 +804,40 @@ protected virtual IMutateOperationResult PerformMutate(MutationMode mode, string return result; } + protected virtual async Task PerformMutateAsync(MutationMode mode, string key, ulong defaultValue, ulong delta, uint expires) + { + ulong cas = 0; + var hashedKey = this.keyTransformer.Transform(key); + var node = this.pool.Locate(hashedKey); + var result = MutateOperationResultFactory.Create(); + + if (node != null) + { + var command = this.pool.OperationFactory.Mutate(mode, hashedKey, defaultValue, delta, expires, cas); + var commandResult = await node.ExecuteAsync(command); + + result.Cas = cas = command.CasValue; + result.StatusCode = command.StatusCode; + + if (commandResult.Success) + { + result.Value = command.Result; + result.Pass(); + return result; + } + else + { + result.InnerResult = commandResult; + result.Fail("Mutate operation failed, see InnerResult or StatusCode for more details"); + } + + } + + // TODO not sure about the return value when the command fails + result.Fail("Unable to locate node"); + return result; + } + #endregion #region [ Concatenate ] diff --git a/Enyim.Caching/NullMemcachedClient.cs b/Enyim.Caching/NullMemcachedClient.cs index 7484a47c..f690e656 100644 --- a/Enyim.Caching/NullMemcachedClient.cs +++ b/Enyim.Caching/NullMemcachedClient.cs @@ -253,7 +253,7 @@ public Task SetAsync(string key, object value, int cacheSeconds) { return Task.FromResult(true); } - + public bool Replace(string key, object value, int cacheSeconds) { return true; @@ -273,5 +273,15 @@ public Task FlushAllAsync() { return Task.CompletedTask; } + + public Task TouchAsync(string key, DateTime expiresAt) + { + return Task.FromResult(new MutateOperationResult()); + } + + public Task TouchAsync(string key, TimeSpan validFor) + { + return Task.FromResult(new MutateOperationResult()); + } } }