diff --git a/README.md b/README.md index 9da8701..27fd827 100644 --- a/README.md +++ b/README.md @@ -376,12 +376,18 @@ To enable this option set the following configuration key. Do not forget to set this option back to `false` or to delte it after the transition period is over. -## Restrictions +## Restrictions && Limitations -Key must be less than 250 characters and value must be less than 1MB of data. +Key must be less than 250 characters and value must be less than the value `-I` configured during memcached instance start. The key length restriction can be lifted, see [Long keys support](#long-keys-support) section for the details. +If the value is greater than the configured `-I` value the memcached client won't throw an exception - it simply won't store a key without any notification. +Therefore, when trying to read such key - nothing will be returned. + +Also, please note that this library utilizes a memcached item `flags` to store item type information to simplify and make deserialization faster. +This implies that one is not advised to use this library to read the data from memcached that you didn't put there using this library - it might be deserialized incorrectly or not deserialized at all. + ## Additional configuration ### SASL diff --git a/docker-compose.yaml b/docker-compose.yaml index 09386cf..d75d136 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -4,20 +4,23 @@ services: image: memcached:latest restart: always container_name: memcached-1 - command: ["-m", "128"] - ports: + command: ["-m", "128", "-I", "1M"] + # command: ["-m", "128", "-I", "512k", "-o", "slab_chunk_max=524288"] + ports: - 11211:11211 memcached-2: image: memcached:latest restart: always container_name: memcached-2 - command: ["-m", "128"] - ports: + command: ["-m", "128", "-I", "1M"] + # command: ["-m", "128", "-I", "512k", "-o", "slab_chunk_max=524288"] + ports: - 11212:11211 memcached-3: image: memcached:latest restart: always container_name: memcached-3 - command: ["-m", "128"] - ports: + command: ["-m", "128", "-I", "1M"] + # command: ["-m", "128", "-I", "512k", "-o", "slab_chunk_max=524288"] + ports: - 11213:11211 \ No newline at end of file diff --git a/src/Aer.Memcached.Client/Serializers/Binary/BinarySerializer.cs b/src/Aer.Memcached.Client/Serializers/Binary/BinarySerializer.cs index 4b262d9..30844e1 100644 --- a/src/Aer.Memcached.Client/Serializers/Binary/BinarySerializer.cs +++ b/src/Aer.Memcached.Client/Serializers/Binary/BinarySerializer.cs @@ -13,7 +13,7 @@ public class BinarySerializer private const uint RawDataFlag = 0xfa52; private const uint TypeCodeSerializationMask = 0x0100; private const uint TypeCodeDeserializationMask = 0xff; - + private readonly IObjectBinarySerializer _objectBinarySerializer; public BinarySerializer(IObjectBinarySerializerFactory objectBinarySerializerFactory) @@ -119,6 +119,17 @@ internal DeserializationResult Deserialize( var typeCode = (TypeCode) (item.Flags & TypeCodeDeserializationMask); if (typeCode == TypeCode.Empty) { + // the flags value is set during item serialization + // it stores the cache item System.Runtime.TypeCode + // if it is not set -> Flags == 0 or Flags contain some unknown value + // then the item is considered empty + // The Data property for such empty items though + // will contain ASCII bytes for "Not found" string + // 0x4E, 0x6F, 0x74, 0x20, 0x66, 0x6F, 0x75, 0x6E, 0x64 + // we don't check them since typeCode absence already tells us that the item is empty + // it does, though impede using this library for reading existing + // memcached keys that were written not using this library + // since they may set Flags to some arbitrary value which we interpret return DeserializationResult.Empty; } diff --git a/tests/Aer.Memcached.Tests/Aer.Memcached.Tests.csproj b/tests/Aer.Memcached.Tests/Aer.Memcached.Tests.csproj index 3ab0d37..eb88ba3 100644 --- a/tests/Aer.Memcached.Tests/Aer.Memcached.Tests.csproj +++ b/tests/Aer.Memcached.Tests/Aer.Memcached.Tests.csproj @@ -5,6 +5,7 @@ enable false + latest diff --git a/tests/Aer.Memcached.Tests/TestClasses/MemcachedClientMethodsTestsBase.cs b/tests/Aer.Memcached.Tests/TestClasses/MemcachedClientMethodsTestsBase.cs index 02d685c..bf79e4d 100644 --- a/tests/Aer.Memcached.Tests/TestClasses/MemcachedClientMethodsTestsBase.cs +++ b/tests/Aer.Memcached.Tests/TestClasses/MemcachedClientMethodsTestsBase.cs @@ -114,7 +114,7 @@ public async Task Get_CheckAllTypes_DefaultValue() await StoreAndGet_CheckType>(); } } - + [DataTestMethod] [DataRow(true)] [DataRow(false)] @@ -205,14 +205,14 @@ await Client.MultiStoreAsync( var getValue = await Client.MultiGetAsync(new[] {key}, CancellationToken.None); getValue.Count.Should().Be(1); - + await Task.Delay(TimeSpan.FromSeconds(CacheItemExpirationSeconds * 2)); - getValue = await Client.MultiGetAsync(new[]{key}, CancellationToken.None); + getValue = await Client.MultiGetAsync(new[] {key}, CancellationToken.None); getValue.Count.Should().Be(0); } - + [TestMethod] public async Task StoreAndGet_EmptyString() { @@ -227,41 +227,47 @@ public async Task StoreAndGet_EmptyString() getValue.Success.Should().BeTrue(); getValue.IsEmptyResult.Should().BeFalse(); } - + [TestMethod] public async Task MultiStoreAndGet_NullAsString() { var keyValues = new Dictionary(); - + foreach (var _ in Enumerable.Range(0, 5)) { keyValues[Guid.NewGuid().ToString()] = null; } - - await Client.MultiStoreAsync(keyValues, TimeSpan.FromSeconds(CacheItemExpirationSeconds), CancellationToken.None); - + + await Client.MultiStoreAsync( + keyValues, + TimeSpan.FromSeconds(CacheItemExpirationSeconds), + CancellationToken.None); + var getValues = await Client.MultiGetAsync(keyValues.Keys, CancellationToken.None); - + foreach (var keyValue in keyValues) { getValues[keyValue.Key].Should().Be(keyValues[keyValue.Key]); } } - + [TestMethod] public async Task MultiStoreAndGet_EmptyString() { var keyValues = new Dictionary(); - + foreach (var _ in Enumerable.Range(0, 5)) { keyValues[Guid.NewGuid().ToString()] = string.Empty; } - - await Client.MultiStoreAsync(keyValues, TimeSpan.FromSeconds(CacheItemExpirationSeconds), CancellationToken.None); - + + await Client.MultiStoreAsync( + keyValues, + TimeSpan.FromSeconds(CacheItemExpirationSeconds), + CancellationToken.None); + var getValues = await Client.MultiGetAsync(keyValues.Keys, CancellationToken.None); - + foreach (var keyValue in keyValues) { getValues[keyValue.Key].Should().BeNull(); @@ -275,15 +281,15 @@ public async Task StoreAndGet_NullExpiration() var value = Fixture.Create(); await Client.StoreAsync( - key, - value, - expirationTime: null, + key, + value, + expirationTime: null, CancellationToken.None); var getValue = await Client.GetAsync(key, CancellationToken.None); getValue.Success.Should().BeTrue(); - + getValue.IsEmptyResult.Should().BeFalse(); getValue.Result.Should().BeEquivalentTo(value); } @@ -302,12 +308,12 @@ public async Task StoreAndGet_SimpleObject() getValue.Success.Should().BeTrue(); getValue.IsEmptyResult.Should().BeFalse(); } - + [TestMethod] public async Task MultiStoreAndGet_SimpleObject() { var keyValues = new Dictionary(); - + foreach (var _ in Enumerable.Range(0, 5)) { keyValues[Guid.NewGuid().ToString()] = Fixture.Create(); @@ -317,11 +323,14 @@ public async Task MultiStoreAndGet_SimpleObject() var tooLongKey = GetTooLongKey(); keyValues[tooLongKey] = Fixture.Create(); - - await Client.MultiStoreAsync(keyValues, TimeSpan.FromSeconds(CacheItemExpirationSeconds), CancellationToken.None); - + + await Client.MultiStoreAsync( + keyValues, + TimeSpan.FromSeconds(CacheItemExpirationSeconds), + CancellationToken.None); + var getValues = await Client.MultiGetAsync(keyValues.Keys, CancellationToken.None); - + foreach (var keyValue in keyValues) { getValues[keyValue.Key].Should().BeEquivalentTo(keyValues[keyValue.Key]); @@ -395,7 +404,7 @@ public async Task MultiStoreAndGet_ExpirationNowAndInThePast_NoKeysStored() CancellationToken.None); storeResult.Success.Should().BeFalse(); - + storeResult = await Client.MultiStoreAsync( keyValues, expirationTime: DateTimeOffset.Now, @@ -403,11 +412,11 @@ public async Task MultiStoreAndGet_ExpirationNowAndInThePast_NoKeysStored() storeResult.Success.Should().BeFalse(); - var getValues = + var getValues = await Client.MultiGetAsync(keyValues.Keys, CancellationToken.None); // no keys should be stored - + getValues.Count.Should().Be(0); } @@ -468,11 +477,64 @@ public async Task StoreAndGet_ObjectWithCollections() var getValue = await Client.GetAsync(key, CancellationToken.None); - getValue.Result.Should().BeEquivalentTo(value); getValue.Success.Should().BeTrue(); + getValue.Result.Should().BeEquivalentTo(value); getValue.IsEmptyResult.Should().BeFalse(); } + [TestMethod] + public async Task StoreAndGet_VeryLargeObject() + { + var key = Guid.NewGuid().ToString(); + + // very large array 1_024_000 * 4 = 4MB + var value = new int[1_024_000]; + + // memcached, while restricting item to configured "-I" parameter, + // returns success while trying to add a too large item to cache, + // it just does nothing - it does not throw anything but does not store the key either + var storeResult = await Client.StoreAsync( + key, + value, + TimeSpan.FromSeconds(CacheItemExpirationSeconds), + CancellationToken.None); + + storeResult.Success.Should().BeTrue(); + + // when trying to read too large object + // memcached simply returns nothing since it was not set in the first place + var getValue = await Client.GetAsync(key, CancellationToken.None); + + getValue.Success.Should().BeTrue(); + getValue.IsEmptyResult.Should().BeTrue(); + } + + [TestMethod] + public async Task StoreAndMultiGet_VeryLargeObject() + { + var key = Guid.NewGuid().ToString(); + + // very large array 1_024_000 * 4 = 4MB + var value = new int[1_024_000]; + + // memcached, while restricting item to configured "-I" parameter, + // returns success while trying to add a too large item to cache, + // it just does nothing - it does not throw anything but does not store the key either + var storeResult = await Client.StoreAsync( + key, + value, + TimeSpan.FromSeconds(CacheItemExpirationSeconds), + CancellationToken.None); + + storeResult.Success.Should().BeTrue(); + + // when trying to read too large object + // memcached simply returns nothing since it was not set in the first place + var getValue = await Client.MultiGetAsync([key], CancellationToken.None); + + getValue.ContainsKey(key).Should().BeFalse(); + } + [TestMethod] public async Task MultiStoreAndGet_ObjectWithCollections() {