diff options
Diffstat (limited to 'VNLib.Data.Caching/src')
-rw-r--r-- | VNLib.Data.Caching/src/BlobCache.cs | 118 | ||||
-rw-r--r-- | VNLib.Data.Caching/src/BlobItem.cs | 185 | ||||
-rw-r--r-- | VNLib.Data.Caching/src/CacheListener.cs | 40 | ||||
-rw-r--r-- | VNLib.Data.Caching/src/ClientExtensions.cs | 310 | ||||
-rw-r--r-- | VNLib.Data.Caching/src/ClientRetryManager.cs | 83 | ||||
-rw-r--r-- | VNLib.Data.Caching/src/Constants.cs | 32 | ||||
-rw-r--r-- | VNLib.Data.Caching/src/Exceptions/InvalidStatusException.cs | 39 | ||||
-rw-r--r-- | VNLib.Data.Caching/src/Exceptions/MessageTooLargeException.cs | 26 | ||||
-rw-r--r-- | VNLib.Data.Caching/src/Exceptions/ObjectNotFoundException.cs | 23 | ||||
-rw-r--r-- | VNLib.Data.Caching/src/VNLib.Data.Caching.csproj | 34 | ||||
-rw-r--r-- | VNLib.Data.Caching/src/VNLib.Data.Caching.xml | 354 | ||||
-rw-r--r-- | VNLib.Data.Caching/src/WaitForChangeResult.cs | 21 |
12 files changed, 1265 insertions, 0 deletions
diff --git a/VNLib.Data.Caching/src/BlobCache.cs b/VNLib.Data.Caching/src/BlobCache.cs new file mode 100644 index 0000000..89818be --- /dev/null +++ b/VNLib.Data.Caching/src/BlobCache.cs @@ -0,0 +1,118 @@ +using System; +using System.IO; +using System.Linq; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +using VNLib.Utils.IO; +using VNLib.Utils.Logging; +using VNLib.Utils.Memory; +using VNLib.Utils.Memory.Caching; + + +#nullable enable + +namespace VNLib.Data.Caching +{ + /// <summary> + /// A general purpose binary data storage + /// </summary> + public class BlobCache : LRUCache<string, MemoryHandle<byte>> + { + readonly IUnmangedHeap Heap; + readonly DirectoryInfo SwapDir; + readonly ILogProvider Log; + ///<inheritdoc/> + public override bool IsReadOnly { get; } + ///<inheritdoc/> + protected override int MaxCapacity { get; } + + + /// <summary> + /// Initializes a new <see cref="BlobCache"/> store + /// </summary> + /// <param name="swapDir">The <see cref="IsolatedStorageDirectory"/> to swap blob data to when cache</param> + /// <param name="maxCapacity">The maximum number of items to keep in memory</param> + /// <param name="log">A <see cref="ILogProvider"/> to write log data to</param> + /// <param name="heap">A <see cref="IUnmangedHeap"/> to allocate buffers and store <see cref="BlobItem"/> data in memory</param> + public BlobCache(DirectoryInfo swapDir, int maxCapacity, ILogProvider log, IUnmangedHeap heap) + :base(StringComparer.Ordinal) + { + IsReadOnly = false; + MaxCapacity = maxCapacity; + SwapDir = swapDir; + //Update the lookup table size + LookupTable.EnsureCapacity(maxCapacity); + //Set default heap if not specified + Heap = heap; + Log = log; + } + ///<inheritdoc/> + protected override bool CacheMiss(string key, [NotNullWhen(true)] out MemoryHandle<byte>? value) + { + value = null; + return false; + } + ///<inheritdoc/> + protected override void Evicted(KeyValuePair<string, MemoryHandle<byte>> evicted) + { + //Dispose the blob + evicted.Value.Dispose(); + } + /// <summary> + /// If the <see cref="BlobItem"/> is found in the store, changes the key + /// that referrences the blob. + /// </summary> + /// <param name="currentKey">The key that currently referrences the blob in the store</param> + /// <param name="newKey">The new key that will referrence the blob</param> + /// <param name="blob">The <see cref="BlobItem"/> if its found in the store</param> + /// <returns>True if the record was found and the key was changes</returns> + public bool TryChangeKey(string currentKey, string newKey, [NotNullWhen(true)] out MemoryHandle<byte>? blob) + { + if (LookupTable.Remove(currentKey, out LinkedListNode<KeyValuePair<string, MemoryHandle<byte>>>? node)) + { + //Remove the node from the ll + List.Remove(node); + //Update the node kvp + blob = node.Value.Value; + node.Value = new KeyValuePair<string, MemoryHandle<byte>>(newKey, blob); + //Add to end of list + List.AddLast(node); + //Re-add to lookup table with new key + LookupTable.Add(newKey, node); + return true; + } + blob = null; + return false; + } + /// <summary> + /// Removes the <see cref="BlobItem"/> from the store without disposing the blobl + /// </summary> + /// <param name="key">The key that referrences the <see cref="BlobItem"/> in the store</param> + /// <returns>A value indicating if the blob was removed</returns> + public override bool Remove(string key) + { + //Remove the item from the lookup table and if it exists, remove the node from the list + if (LookupTable.Remove(key, out LinkedListNode<KeyValuePair<string, MemoryHandle<byte>>>? node)) + { + //Remove the new from the list + List.Remove(node); + //dispose the buffer + node.Value.Value.Dispose(); + return true; + } + return false; + } + /// <summary> + /// Removes and disposes all blobl elements in cache (or in the backing store) + /// </summary> + public override void Clear() + { + foreach (MemoryHandle<byte> blob in List.Select(kp => kp.Value)) + { + blob.Dispose(); + } + base.Clear(); + } + } +} diff --git a/VNLib.Data.Caching/src/BlobItem.cs b/VNLib.Data.Caching/src/BlobItem.cs new file mode 100644 index 0000000..a5630e9 --- /dev/null +++ b/VNLib.Data.Caching/src/BlobItem.cs @@ -0,0 +1,185 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +using VNLib.Utils; +using VNLib.Utils.IO; +using VNLib.Utils.Memory; +using VNLib.Utils.Logging; +using VNLib.Utils.Extensions; + +#nullable enable + +namespace VNLib.Data.Caching +{ + /// <summary> + /// A general purpose binary storage item + /// </summary> + public class BlobItem //: VnDisposeable + { + /* + private static readonly JoinableTaskContext JTX = new(); + private static readonly Semaphore CentralSwapLock = new(Environment.ProcessorCount, Environment.ProcessorCount); + + private readonly VnMemoryStream _loadedData; + private bool _loaded; + + /// <summary> + /// The time the blob was last modified + /// </summary> + public DateTimeOffset LastAccessed { get; private set; } + + + /// <summary> + /// Gets the current size of the file (in bytes) as an atomic operation + /// </summary> + public int FileSize => (int)_loadedData.Length; + /// <summary> + /// The operation synchronization lock + /// </summary> + public AsyncReaderWriterLock OpLock { get; } + /// <summary> + /// Initializes a new <see cref="BlobItem"/> + /// </summary> + /// <param name="heap">The heap to allocate buffers from</param> + internal BlobItem(IUnmangedHeap heap) + { + _loadedData = new(heap); + OpLock = new AsyncReaderWriterLock(JTX); + _loaded = true; + LastAccessed = DateTimeOffset.UtcNow; + } + ///<inheritdoc/> + protected override void Free() + { + _loadedData.Dispose(); + OpLock.Dispose(); + } + + /// <summary> + /// Reads data from the internal buffer and copies it to the specified buffer. + /// Use the <see cref="FileSize"/> property to obtain the size of the internal buffer + /// </summary> + /// <param name="buffer">The buffer to copy data to</param> + /// <returns>When completed, the number of bytes copied to the buffer</returns> + public int Read(Span<byte> buffer) + { + //Make sure the blob has been swapped back into memory + if (!_loaded) + { + throw new InvalidOperationException("The blob was not loaded from the disk"); + } + //Read all data from the buffer and write it to the output buffer + _loadedData.AsSpan().CopyTo(buffer); + //Update last-accessed + LastAccessed = DateTimeOffset.UtcNow; + return (int)_loadedData.Length; + } + /// <summary> + /// Overwrites the internal buffer with the contents of the supplied buffer + /// </summary> + /// <param name="buffer">The buffer containing data to store within the blob</param> + /// <returns>A <see cref="ValueTask"/> that completes when write access has been granted and copied</returns> + /// <exception cref="InvalidOperationException"></exception> + public void Write(ReadOnlySpan<byte> buffer) + { + //Make sure the blob has been swapped back into memory + if (!_loaded) + { + throw new InvalidOperationException("The blob was not loaded from the disk"); + } + //Reset the buffer + _loadedData.SetLength(buffer.Length); + _loadedData.Seek(0, SeekOrigin.Begin); + _loadedData.Write(buffer); + LastAccessed = DateTimeOffset.UtcNow; + } + + /// <summary> + /// Writes the contents of the memory buffer to its designated file on the disk + /// </summary> + /// <param name="heap">The heap to allocate buffers from</param> + /// <param name="swapDir">The <see cref="IsolatedStorageDirectory"/> that stores the file</param> + /// <param name="filename">The name of the file to write data do</param> + /// <param name="log">A log to write errors to</param> + /// <returns>A task that completes when the swap to disk is complete</returns> + internal async Task SwapToDiskAsync(IUnmangedHeap heap, DirectoryInfo swapDir, string filename, ILogProvider log) + { + try + { + //Wait for write lock + await using (AsyncReaderWriterLock.Releaser releaser = await OpLock.WriteLockAsync()) + { + //Enter swap lock + await CentralSwapLock; + try + { + //Open swap file data stream + await using FileStream swapFile = swapDir.OpenFile(filename, FileMode.OpenOrCreate, FileAccess.ReadWrite, bufferSize: 8128); + //reset swap file + swapFile.SetLength(0); + //Seek loaded-data back to 0 before writing + _loadedData.Seek(0, SeekOrigin.Begin); + //Write loaded data to disk + await _loadedData.CopyToAsync(swapFile, 8128, heap); + } + finally + { + CentralSwapLock.Release(); + } + //Release memory held by stream + _loadedData.SetLength(0); + //Clear loaded flag + _loaded = false; + LastAccessed = DateTimeOffset.UtcNow; + } + log.Debug("Blob {name} swapped to disk", filename); + } + catch(Exception ex) + { + log.Error(ex, "Blob swap to disk error"); + } + } + /// <summary> + /// Reads the contents of the blob into a memory buffer from its designated file on disk + /// </summary> + /// <param name="heap">The heap to allocate buffers from</param> + /// <param name="swapDir">The <see cref="IsolatedStorageDirectory"/> that stores the file</param> + /// <param name="filename">The name of the file to write the blob data to</param> + /// <param name="log">A log to write errors to</param> + /// <returns>A task that completes when the swap from disk is complete</returns> + internal async Task SwapFromDiskAsync(IUnmangedHeap heap, DirectoryInfo swapDir, string filename, ILogProvider log) + { + try + { + //Wait for write lock + await using (AsyncReaderWriterLock.Releaser releaser = await OpLock.WriteLockAsync()) + { + //Enter swap lock + await CentralSwapLock; + try + { + //Open swap file data stream + await using FileStream swapFile = swapDir.OpenFile(filename, FileMode.OpenOrCreate, FileAccess.Read, bufferSize:8128); + //Copy from disk to memory + await swapFile.CopyToAsync(_loadedData, 8128, heap); + } + finally + { + CentralSwapLock.Release(); + } + //Set loaded flag + _loaded = true; + LastAccessed = DateTimeOffset.UtcNow; + } + log.Debug("Blob {name} swapped from disk", filename); + } + catch(Exception ex) + { + log.Error(ex, "Blob swap from disk error"); + } + } + */ + } +} diff --git a/VNLib.Data.Caching/src/CacheListener.cs b/VNLib.Data.Caching/src/CacheListener.cs new file mode 100644 index 0000000..18e9684 --- /dev/null +++ b/VNLib.Data.Caching/src/CacheListener.cs @@ -0,0 +1,40 @@ +using System; +using System.IO; + +using VNLib.Utils.Memory; +using VNLib.Net.Messaging.FBM.Server; + +namespace VNLib.Data.Caching +{ + /// <summary> + /// A base implementation of a memory/disk LRU data cache FBM listener + /// </summary> + public abstract class CacheListener : FBMListenerBase + { + /// <summary> + /// The directory swap files will be stored + /// </summary> + public DirectoryInfo? Directory { get; private set; } + /// <summary> + /// The Cache store to access data blobs + /// </summary> + protected BlobCache? Cache { get; private set; } + /// <summary> + /// The <see cref="IUnmangedHeap"/> to allocate buffers from + /// </summary> + protected IUnmangedHeap? Heap { get; private set; } + + /// <summary> + /// Initializes the <see cref="Cache"/> data store + /// </summary> + /// <param name="dir">The directory to swap cache records to</param> + /// <param name="cacheSize">The size of the LRU cache</param> + /// <param name="heap">The heap to allocate buffers from</param> + protected void InitCache(DirectoryInfo dir, int cacheSize, IUnmangedHeap heap) + { + Heap = heap; + Cache = new(dir, cacheSize, Log, Heap); + Directory = dir; + } + } +} diff --git a/VNLib.Data.Caching/src/ClientExtensions.cs b/VNLib.Data.Caching/src/ClientExtensions.cs new file mode 100644 index 0000000..18a1aa9 --- /dev/null +++ b/VNLib.Data.Caching/src/ClientExtensions.cs @@ -0,0 +1,310 @@ +using System; +using System.IO; +using System.Linq; +using System.Buffers; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using System.Collections.Generic; +using System.Text.Json.Serialization; +using System.Runtime.CompilerServices; + +using VNLib.Utils.Logging; +using VNLib.Net.Messaging.FBM; +using VNLib.Net.Messaging.FBM.Client; +using VNLib.Net.Messaging.FBM.Server; +using VNLib.Data.Caching.Exceptions; + +using static VNLib.Data.Caching.Constants; + +namespace VNLib.Data.Caching +{ + + /// <summary> + /// Provides caching extension methods for <see cref="FBMClient"/> + /// </summary> + public static class ClientExtensions + { + private static readonly JsonSerializerOptions LocalOptions = new() + { + DictionaryKeyPolicy = JsonNamingPolicy.CamelCase, + NumberHandling = JsonNumberHandling.Strict, + ReadCommentHandling = JsonCommentHandling.Disallow, + WriteIndented = false, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + IgnoreReadOnlyFields = true, + PropertyNameCaseInsensitive = true, + IncludeFields = false, + + //Use small buffers + DefaultBufferSize = 128 + }; + + + private static readonly ConditionalWeakTable<FBMClient, SemaphoreSlim> GetLock = new(); + private static readonly ConditionalWeakTable<FBMClient, SemaphoreSlim> UpdateLock = new(); + + private static SemaphoreSlim GetLockCtor(FBMClient client) => new (50); + + private static SemaphoreSlim UpdateLockCtor(FBMClient client) => new (25); + + /// <summary> + /// Gets an object from the server if it exists + /// </summary> + /// <typeparam name="T"></typeparam> + /// <param name="client"></param> + /// <param name="objectId">The id of the object to get</param> + /// <param name="cancellationToken">A token to cancel the operation</param> + /// <returns>A task that completes to return the results of the response payload</returns> + /// <exception cref="JsonException"></exception> + /// <exception cref="OutOfMemoryException"></exception> + /// <exception cref="InvalidStatusException"></exception> + /// <exception cref="ObjectDisposedException"></exception> + /// <exception cref="InvalidResponseException"></exception> + public static async Task<T?> GetObjectAsync<T>(this FBMClient client, string objectId, CancellationToken cancellationToken = default) + { + client.Config.DebugLog?.Debug("[DEBUG] Getting object {id}", objectId); + SemaphoreSlim getLock = GetLock.GetValue(client, GetLockCtor); + //Wait for entry + await getLock.WaitAsync(cancellationToken); + //Rent a new request + FBMRequest request = client.RentRequest(); + try + { + //Set action as get/create + request.WriteHeader(HeaderCommand.Action, Actions.Get); + //Set session-id header + request.WriteHeader(Constants.ObjectId, objectId); + + //Make request + using FBMResponse response = await client.SendAsync(request, cancellationToken); + + response.ThrowIfNotSet(); + //Get the status code + ReadOnlyMemory<char> status = response.Headers.FirstOrDefault(static a => a.Key == HeaderCommand.Status).Value; + if (status.Span.Equals(ResponseCodes.Okay, StringComparison.Ordinal)) + { + return JsonSerializer.Deserialize<T>(response.ResponseBody, LocalOptions); + } + //Session may not exist on the server yet + if (status.Span.Equals(ResponseCodes.NotFound, StringComparison.Ordinal)) + { + return default; + } + throw new InvalidStatusException("Invalid status code recived for object get request", status.ToString()); + } + finally + { + getLock.Release(); + client.ReturnRequest(request); + } + } + + /// <summary> + /// Updates the state of the object, and optionally updates the ID of the object. The data + /// parameter is serialized, buffered, and streamed to the remote server + /// </summary> + /// <typeparam name="T"></typeparam> + /// <param name="client"></param> + /// <param name="objectId">The id of the object to update or replace</param> + /// <param name="newId">An optional parameter to specify a new ID for the old object</param> + /// <param name="data">The payload data to serialize and set as the data state of the session</param> + /// <param name="cancellationToken">A token to cancel the operation</param> + /// <returns>A task that resolves when the server responds</returns> + /// <exception cref="JsonException"></exception> + /// <exception cref="OutOfMemoryException"></exception> + /// <exception cref="InvalidStatusException"></exception> + /// <exception cref="ObjectDisposedException"></exception> + /// <exception cref="InvalidResponseException"></exception> + /// <exception cref="MessageTooLargeException"></exception> + /// <exception cref="ObjectNotFoundException"></exception> + public static async Task AddOrUpdateObjectAsync<T>(this FBMClient client, string objectId, string? newId, T data, CancellationToken cancellationToken = default) + { + client.Config.DebugLog?.Debug("[DEBUG] Updating object {id}, newid {nid}", objectId, newId); + SemaphoreSlim updateLock = UpdateLock.GetValue(client, UpdateLockCtor); + //Wait for entry + await updateLock.WaitAsync(cancellationToken); + //Rent a new request + FBMRequest request = client.RentRequest(); + try + { + //Set action as get/create + request.WriteHeader(HeaderCommand.Action, Actions.AddOrUpdate); + //Set session-id header + request.WriteHeader(Constants.ObjectId, objectId); + //if new-id set, set the new-id header + if (!string.IsNullOrWhiteSpace(newId)) + { + request.WriteHeader(Constants.NewObjectId, newId); + } + //Get the body writer for the message + IBufferWriter<byte> bodyWriter = request.GetBodyWriter(); + //Write json data to the message + using (Utf8JsonWriter jsonWriter = new(bodyWriter)) + { + JsonSerializer.Serialize(jsonWriter, data, LocalOptions); + } + + //Make request + using FBMResponse response = await client.SendAsync(request, cancellationToken); + + response.ThrowIfNotSet(); + //Get the status code + ReadOnlyMemory<char> status = response.Headers.FirstOrDefault(static a => a.Key == HeaderCommand.Status).Value; + //Check status code + if (status.Span.Equals(ResponseCodes.Okay, StringComparison.OrdinalIgnoreCase)) + { + return; + } + else if(status.Span.Equals(ResponseCodes.NotFound, StringComparison.OrdinalIgnoreCase)) + { + throw new ObjectNotFoundException($"object {objectId} not found on remote server"); + } + //Invalid status + throw new InvalidStatusException("Invalid status code recived for object upsert request", status.ToString()); + } + finally + { + updateLock.Release(); + //Return the request(clears data and reset) + client.ReturnRequest(request); + } + } + + /// <summary> + /// Asynchronously deletes an object in the remote store + /// </summary> + /// <param name="client"></param> + /// <param name="objectId">The id of the object to update or replace</param> + /// <param name="cancellationToken">A token to cancel the operation</param> + /// <returns>A task that resolves when the operation has completed</returns> + /// <exception cref="InvalidStatusException"></exception> + /// <exception cref="ObjectDisposedException"></exception> + /// <exception cref="InvalidResponseException"></exception> + /// <exception cref="ObjectNotFoundException"></exception> + public static async Task DeleteObjectAsync(this FBMClient client, string objectId, CancellationToken cancellationToken = default) + { + client.Config.DebugLog?.Debug("[DEBUG] Deleting object {id}", objectId); + + SemaphoreSlim updateLock = UpdateLock.GetValue(client, UpdateLockCtor); + //Wait for entry + await updateLock.WaitAsync(cancellationToken); + //Rent a new request + FBMRequest request = client.RentRequest(); + try + { + //Set action as delete + request.WriteHeader(HeaderCommand.Action, Actions.Delete); + //Set session-id header + request.WriteHeader(Constants.ObjectId, objectId); + + //Make request + using FBMResponse response = await client.SendAsync(request, cancellationToken); + + response.ThrowIfNotSet(); + //Get the status code + ReadOnlyMemory<char> status = response.Headers.FirstOrDefault(static a => a.Key == HeaderCommand.Status).Value; + if (status.Span.Equals(ResponseCodes.Okay, StringComparison.Ordinal)) + { + return; + } + else if(status.Span.Equals(ResponseCodes.NotFound, StringComparison.OrdinalIgnoreCase)) + { + throw new ObjectNotFoundException($"object {objectId} not found on remote server"); + } + throw new InvalidStatusException("Invalid status code recived for object get request", status.ToString()); + } + finally + { + updateLock.Release(); + client.ReturnRequest(request); + } + } + + /// <summary> + /// Dequeues a change event from the server event queue for the current connection, or waits until a change happens + /// </summary> + /// <param name="client"></param> + /// <param name="cancellationToken">A token to cancel the deuque operation</param> + /// <returns>A <see cref="KeyValuePair{TKey, TValue}"/> that contains the modified object id and optionally its new id</returns> + public static async Task<WaitForChangeResult> WaitForChangeAsync(this FBMClient client, CancellationToken cancellationToken = default) + { + //Rent a new request + FBMRequest request = client.RentRequest(); + try + { + //Set action as event dequeue to dequeue a change event + request.WriteHeader(HeaderCommand.Action, Actions.Dequeue); + + //Make request + using FBMResponse response = await client.SendAsync(request, cancellationToken); + + response.ThrowIfNotSet(); + + return new() + { + Status = response.Headers.FirstOrDefault(static a => a.Key == HeaderCommand.Status).Value.ToString(), + CurrentId = response.Headers.SingleOrDefault(static v => v.Key == Constants.ObjectId).Value.ToString(), + NewId = response.Headers.SingleOrDefault(static v => v.Key == Constants.NewObjectId).Value.ToString() + }; + } + finally + { + client.ReturnRequest(request); + } + } + + /// <summary> + /// Gets the Object-id for the request message, or throws an <see cref="InvalidOperationException"/> if not specified + /// </summary> + /// <param name="context"></param> + /// <returns>The id of the object requested</returns> + /// <exception cref="InvalidOperationException"></exception> + public static string ObjectId(this FBMContext context) + { + return context.Request.Headers.First(static kvp => kvp.Key == Constants.ObjectId).Value.ToString(); + } + /// <summary> + /// Gets the new ID of the object if specified from the request. Null if the request did not specify an id update + /// </summary> + /// <param name="context"></param> + /// <returns>The new ID of the object if speicifed, null otherwise</returns> + public static string? NewObjectId(this FBMContext context) + { + return context.Request.Headers.FirstOrDefault(static kvp => kvp.Key == Constants.NewObjectId).Value.ToString(); + } + /// <summary> + /// Gets the request method for the request + /// </summary> + /// <param name="context"></param> + /// <returns>The request method string</returns> + public static string Method(this FBMContext context) + { + return context.Request.Headers.First(static kvp => kvp.Key == HeaderCommand.Action).Value.ToString(); + } + /// <summary> + /// Closes a response with a status code + /// </summary> + /// <param name="context"></param> + /// <param name="responseCode">The status code to send to the client</param> + public static void CloseResponse(this FBMContext context, string responseCode) + { + context.Response.WriteHeader(HeaderCommand.Status, responseCode); + } + + + /// <summary> + /// Initializes the worker for a reconnect policy and returns an object that can listen for changes + /// and configure the connection as necessary + /// </summary> + /// <param name="worker"></param> + /// <param name="retryDelay">The amount of time to wait between retries</param> + /// <param name="serverUri">The uri to reconnect the client to</param> + /// <returns>A <see cref="ClientRetryManager{T}"/> for listening for retry events</returns> + public static ClientRetryManager<T> SetReconnectPolicy<T>(this T worker, TimeSpan retryDelay, Uri serverUri) where T: IStatefulConnection + { + //Return new manager + return new (worker, retryDelay, serverUri); + } + } +} diff --git a/VNLib.Data.Caching/src/ClientRetryManager.cs b/VNLib.Data.Caching/src/ClientRetryManager.cs new file mode 100644 index 0000000..97d3d3a --- /dev/null +++ b/VNLib.Data.Caching/src/ClientRetryManager.cs @@ -0,0 +1,83 @@ +using System; +using System.Threading.Tasks; +using System.Security.Cryptography; + +using VNLib.Utils; +using VNLib.Net.Messaging.FBM.Client; + +namespace VNLib.Data.Caching +{ + /// <summary> + /// Manages a <see cref="FBMClientWorkerBase"/> reconnect policy + /// </summary> + public class ClientRetryManager<T> : VnDisposeable where T: IStatefulConnection + { + const int RetryRandMaxMsDelay = 1000; + + private readonly TimeSpan RetryDelay; + private readonly T Client; + private readonly Uri ServerUri; + + internal ClientRetryManager(T worker, TimeSpan delay, Uri serverUri) + { + this.Client = worker; + this.RetryDelay = delay; + this.ServerUri = serverUri; + //Register disconnect listener + worker.ConnectionClosed += Worker_Disconnected; + } + + private void Worker_Disconnected(object? sender, EventArgs args) + { + //Exec retry on exit + _ = RetryAsync().ConfigureAwait(false); + } + + + /// <summary> + /// Raised before client is to be reconnected + /// </summary> + public event Action<T>? OnBeforeReconnect; + /// <summary> + /// Raised when the client fails to reconnect. Should return a value that instructs the + /// manager to reconnect + /// </summary> + public event Func<T, Exception, bool>? OnReconnectFailed; + + async Task RetryAsync() + { + + //Begin random delay with retry ms + int randomDelayMs = (int)RetryDelay.TotalMilliseconds; + //random delay to add to prevent retry-storm + randomDelayMs += RandomNumberGenerator.GetInt32(RetryRandMaxMsDelay); + //Retry loop + bool retry = true; + while (retry) + { + try + { + //Inform Listener for the retry + OnBeforeReconnect?.Invoke(Client); + //wait for delay before reconnecting + await Task.Delay(randomDelayMs); + //Reconnect async + await Client.ConnectAsync(ServerUri).ConfigureAwait(false); + break; + } + catch (Exception Ex) + { + //Invoke error handler, may be null, incase exit + retry = OnReconnectFailed?.Invoke(Client, Ex) ?? false; + } + } + } + + ///<inheritdoc/> + protected override void Free() + { + //Unregister the event listener + Client.ConnectionClosed -= Worker_Disconnected; + } + } +} diff --git a/VNLib.Data.Caching/src/Constants.cs b/VNLib.Data.Caching/src/Constants.cs new file mode 100644 index 0000000..b06ec1f --- /dev/null +++ b/VNLib.Data.Caching/src/Constants.cs @@ -0,0 +1,32 @@ +using System; + +using VNLib.Net.Messaging.FBM; + +namespace VNLib.Data.Caching +{ + public static class Constants + { + /// <summary> + /// Contains constants the define actions + /// </summary> + public static class Actions + { + public const string Get= "g"; + public const string AddOrUpdate = "u"; + public const string Delete = "d"; + public const string Dequeue = "dq"; + } + /// <summary> + /// Containts constants for operation response codes + /// </summary> + public static class ResponseCodes + { + public const string Okay = "ok"; + public const string Error = "err"; + public const string NotFound = "nf"; + } + + public const HeaderCommand ObjectId = (HeaderCommand)0xAA; + public const HeaderCommand NewObjectId = (HeaderCommand)0xAB; + } +} diff --git a/VNLib.Data.Caching/src/Exceptions/InvalidStatusException.cs b/VNLib.Data.Caching/src/Exceptions/InvalidStatusException.cs new file mode 100644 index 0000000..f5e35f4 --- /dev/null +++ b/VNLib.Data.Caching/src/Exceptions/InvalidStatusException.cs @@ -0,0 +1,39 @@ +using System; + +using VNLib.Net.Messaging.FBM; + +namespace VNLib.Data.Caching.Exceptions +{ + /// <summary> + /// Raised when the response status code of an FBM Request message is not valid for + /// the specified request + /// </summary> + public class InvalidStatusException : InvalidResponseException + { + private readonly string? StatusCode; + /// <summary> + /// Initalizes a new <see cref="InvalidStatusException"/> with the specfied status code + /// </summary> + /// <param name="message"></param> + /// <param name="statusCode"></param> + public InvalidStatusException(string message, string statusCode):this(message) + { + this.StatusCode = statusCode; + } + + ///<inheritdoc/> + public InvalidStatusException() + { + } + ///<inheritdoc/> + public InvalidStatusException(string message) : base(message) + { + } + ///<inheritdoc/> + public InvalidStatusException(string message, Exception innerException) : base(message, innerException) + { + } + ///<inheritdoc/> + public override string Message => $"InvalidStatusException: Status Code {StatusCode} \r\n {base.Message}"; + } +} diff --git a/VNLib.Data.Caching/src/Exceptions/MessageTooLargeException.cs b/VNLib.Data.Caching/src/Exceptions/MessageTooLargeException.cs new file mode 100644 index 0000000..e8f19c5 --- /dev/null +++ b/VNLib.Data.Caching/src/Exceptions/MessageTooLargeException.cs @@ -0,0 +1,26 @@ +using System; +using System.Runtime.Serialization; + +using VNLib.Net.Messaging.FBM; + +namespace VNLib.Data.Caching.Exceptions +{ + /// <summary> + /// Raised when a request (or server response) calculates the size of the message to be too large to proccess + /// </summary> + public class MessageTooLargeException : FBMException + { + ///<inheritdoc/> + public MessageTooLargeException() + {} + ///<inheritdoc/> + public MessageTooLargeException(string message) : base(message) + {} + ///<inheritdoc/> + public MessageTooLargeException(string message, Exception innerException) : base(message, innerException) + {} + ///<inheritdoc/> + protected MessageTooLargeException(SerializationInfo info, StreamingContext context) : base(info, context) + {} + } +} diff --git a/VNLib.Data.Caching/src/Exceptions/ObjectNotFoundException.cs b/VNLib.Data.Caching/src/Exceptions/ObjectNotFoundException.cs new file mode 100644 index 0000000..fb284f3 --- /dev/null +++ b/VNLib.Data.Caching/src/Exceptions/ObjectNotFoundException.cs @@ -0,0 +1,23 @@ +using System; + +namespace VNLib.Data.Caching.Exceptions +{ + /// <summary> + /// Raised when a command was executed on a desired object in the remote cache + /// but the object was not found + /// </summary> + public class ObjectNotFoundException : InvalidStatusException + { + internal ObjectNotFoundException() + {} + + internal ObjectNotFoundException(string message) : base(message) + {} + + internal ObjectNotFoundException(string message, string statusCode) : base(message, statusCode) + {} + + internal ObjectNotFoundException(string message, Exception innerException) : base(message, innerException) + {} + } +} diff --git a/VNLib.Data.Caching/src/VNLib.Data.Caching.csproj b/VNLib.Data.Caching/src/VNLib.Data.Caching.csproj new file mode 100644 index 0000000..b12da09 --- /dev/null +++ b/VNLib.Data.Caching/src/VNLib.Data.Caching.csproj @@ -0,0 +1,34 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <TargetFramework>net6.0</TargetFramework> + <Platforms>AnyCPU;x64</Platforms> + <Authors>Vaughn Nugent</Authors> + <Copyright>Copyright © 2022 Vaughn Nugent</Copyright> + <Version>1.0.0.1</Version> + <GenerateDocumentationFile>True</GenerateDocumentationFile> + <PlatformTarget>x64</PlatformTarget> + <Nullable>enable</Nullable> + </PropertyGroup> + + <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'"> + <DocumentationFile></DocumentationFile> + </PropertyGroup> + + <ItemGroup> + <PackageReference Include="ErrorProne.NET.CoreAnalyzers" Version="0.1.2"> + <PrivateAssets>all</PrivateAssets> + <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> + </PackageReference> + <PackageReference Include="ErrorProne.NET.Structs" Version="0.1.2"> + <PrivateAssets>all</PrivateAssets> + <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> + </PackageReference> + </ItemGroup> + + <ItemGroup> + <ProjectReference Include="..\..\..\..\VNLib\Utils\src\VNLib.Utils.csproj" /> + <ProjectReference Include="..\..\VNLib.Net.Messaging.FBM\src\VNLib.Net.Messaging.FBM.csproj" /> + </ItemGroup> + +</Project> diff --git a/VNLib.Data.Caching/src/VNLib.Data.Caching.xml b/VNLib.Data.Caching/src/VNLib.Data.Caching.xml new file mode 100644 index 0000000..f1ec423 --- /dev/null +++ b/VNLib.Data.Caching/src/VNLib.Data.Caching.xml @@ -0,0 +1,354 @@ +<?xml version="1.0"?> +<doc> + <assembly> + <name>VNLib.Data.Caching</name> + </assembly> + <members> + <member name="T:VNLib.Data.Caching.BlobCache"> + <summary> + A general purpose binary data storage + </summary> + </member> + <member name="P:VNLib.Data.Caching.BlobCache.IsReadOnly"> + <inheritdoc/> + </member> + <member name="P:VNLib.Data.Caching.BlobCache.MaxCapacity"> + <inheritdoc/> + </member> + <member name="M:VNLib.Data.Caching.BlobCache.#ctor(System.IO.DirectoryInfo,System.Int32,VNLib.Utils.Logging.ILogProvider,VNLib.Utils.Memory.PrivateHeap)"> + <summary> + Initializes a new <see cref="T:VNLib.Data.Caching.BlobCache"/> store + </summary> + <param name="swapDir">The <see cref="T:VNLib.Utils.IO.IsolatedStorageDirectory"/> to swap blob data to when cache</param> + <param name="maxCapacity">The maximum number of items to keep in memory</param> + <param name="log">A <see cref="T:VNLib.Utils.Logging.ILogProvider"/> to write log data to</param> + <param name="heap">A <see cref="T:VNLib.Utils.Memory.PrivateHeap"/> to allocate buffers and store <see cref="T:VNLib.Data.Caching.BlobItem"/> data in memory</param> + </member> + <member name="M:VNLib.Data.Caching.BlobCache.SwapAllToDiskAsync"> + <summary> + Swaps all <see cref="T:VNLib.Data.Caching.BlobItem"/>s that are cached in memory + to disk. + </summary> + </member> + <member name="M:VNLib.Data.Caching.BlobCache.CacheMiss(System.String,VNLib.Data.Caching.BlobItem@)"> + <inheritdoc/> + </member> + <member name="M:VNLib.Data.Caching.BlobCache.Evicted(System.Collections.Generic.KeyValuePair{System.String,VNLib.Data.Caching.BlobItem})"> + <inheritdoc/> + </member> + <member name="M:VNLib.Data.Caching.BlobCache.TryChangeKey(System.String,System.String,VNLib.Data.Caching.BlobItem@)"> + <summary> + If the <see cref="T:VNLib.Data.Caching.BlobItem"/> is found in the store, changes the key + that referrences the blob. + </summary> + <param name="currentKey">The key that currently referrences the blob in the store</param> + <param name="newKey">The new key that will referrence the blob</param> + <param name="blob">The <see cref="T:VNLib.Data.Caching.BlobItem"/> if its found in the store</param> + <returns>True if the record was found and the key was changes</returns> + </member> + <member name="M:VNLib.Data.Caching.BlobCache.Remove(System.String)"> + <summary> + Removes the <see cref="T:VNLib.Data.Caching.BlobItem"/> from the store without disposing the blobl + </summary> + <param name="key">The key that referrences the <see cref="T:VNLib.Data.Caching.BlobItem"/> in the store</param> + <returns>A value indicating if the blob was removed</returns> + </member> + <member name="M:VNLib.Data.Caching.BlobCache.Clear"> + <summary> + Removes and disposes all blobl elements in cache (or in the backing store) + </summary> + </member> + <member name="T:VNLib.Data.Caching.BlobItem"> + <summary> + A general purpose binary storage item + </summary> + </member> + <member name="P:VNLib.Data.Caching.BlobItem.LastModified"> + <summary> + The time the blob was last modified + </summary> + </member> + <member name="P:VNLib.Data.Caching.BlobItem.FileSize"> + <summary> + Gets the current size of the file (in bytes) as an atomic operation + </summary> + </member> + <member name="P:VNLib.Data.Caching.BlobItem.OpLock"> + <summary> + The operation synchronization lock + </summary> + </member> + <member name="M:VNLib.Data.Caching.BlobItem.#ctor(VNLib.Utils.Memory.PrivateHeap)"> + <summary> + Initializes a new <see cref="T:VNLib.Data.Caching.BlobItem"/> + </summary> + <param name="heap">The heap to allocate buffers from</param> + </member> + <member name="M:VNLib.Data.Caching.BlobItem.Free"> + <inheritdoc/> + </member> + <member name="M:VNLib.Data.Caching.BlobItem.Read(System.Span{System.Byte})"> + <summary> + Reads data from the internal buffer and copies it to the specified buffer. + Use the <see cref="P:VNLib.Data.Caching.BlobItem.FileSize"/> property to obtain the size of the internal buffer + </summary> + <param name="buffer">The buffer to copy data to</param> + <returns>When completed, the number of bytes copied to the buffer</returns> + </member> + <member name="M:VNLib.Data.Caching.BlobItem.Write(System.ReadOnlySpan{System.Byte})"> + <summary> + Overwrites the internal buffer with the contents of the supplied buffer + </summary> + <param name="buffer">The buffer containing data to store within the blob</param> + <returns>A <see cref="T:System.Threading.Tasks.ValueTask"/> that completes when write access has been granted and copied</returns> + <exception cref="T:System.InvalidOperationException"></exception> + </member> + <member name="M:VNLib.Data.Caching.BlobItem.SwapToDiskAsync(VNLib.Utils.Memory.PrivateHeap,System.IO.DirectoryInfo,System.String,VNLib.Utils.Logging.ILogProvider)"> + <summary> + Writes the contents of the memory buffer to its designated file on the disk + </summary> + <param name="heap">The heap to allocate buffers from</param> + <param name="swapDir">The <see cref="T:VNLib.Utils.IO.IsolatedStorageDirectory"/> that stores the file</param> + <param name="filename">The name of the file to write data do</param> + <param name="log">A log to write errors to</param> + <returns>A task that completes when the swap to disk is complete</returns> + </member> + <member name="M:VNLib.Data.Caching.BlobItem.SwapFromDiskAsync(VNLib.Utils.Memory.PrivateHeap,System.IO.DirectoryInfo,System.String,VNLib.Utils.Logging.ILogProvider)"> + <summary> + Reads the contents of the blob into a memory buffer from its designated file on disk + </summary> + <param name="heap">The heap to allocate buffers from</param> + <param name="swapDir">The <see cref="T:VNLib.Utils.IO.IsolatedStorageDirectory"/> that stores the file</param> + <param name="filename">The name of the file to write the blob data to</param> + <param name="log">A log to write errors to</param> + <returns>A task that completes when the swap from disk is complete</returns> + </member> + <member name="T:VNLib.Data.Caching.CacheListener"> + <summary> + A base implementation of a memory/disk LRU data cache FBM listener + </summary> + </member> + <member name="P:VNLib.Data.Caching.CacheListener.Cache"> + <summary> + The Cache store to access data blobs + </summary> + </member> + <member name="P:VNLib.Data.Caching.CacheListener.Heap"> + <summary> + The <see cref="T:VNLib.Utils.Memory.PrivateHeap"/> to allocate buffers from + </summary> + </member> + <member name="M:VNLib.Data.Caching.CacheListener.InitCache(System.IO.DirectoryInfo,System.Int32,VNLib.Utils.Memory.PrivateHeap)"> + <summary> + Initializes the <see cref="P:VNLib.Data.Caching.CacheListener.Cache"/> data store + </summary> + <param name="dir"></param> + <param name="cacheSize"></param> + <param name="heap"></param> + </member> + <member name="M:VNLib.Data.Caching.CacheListener.SwapToDiskAsync"> + <summary> + Asynchronously swaps all blobs to the disk + </summary> + <returns></returns> + </member> + <member name="M:VNLib.Data.Caching.CacheListener.Dispose(System.Boolean)"> + <inheritdoc/> + </member> + <member name="M:VNLib.Data.Caching.CacheListener.Dispose"> + <inheritdoc/> + </member> + <member name="T:VNLib.Data.Caching.ClientExtensions"> + <summary> + Provides caching extension methods for <see cref="T:VNLib.Net.Messaging.FBM.Client.FBMClient"/> + </summary> + </member> + <member name="M:VNLib.Data.Caching.ClientExtensions.GetObjectAsync``1(VNLib.Net.Messaging.FBM.Client.FBMClient,System.String,System.Text.Json.JsonSerializerOptions,System.Threading.CancellationToken)"> + <summary> + Gets an object from the server if it exists + </summary> + <typeparam name="T"></typeparam> + <param name="client"></param> + <param name="objectId">The id of the object to get</param> + <param name="jso">The <see cref="T:System.Text.Json.JsonSerializerOptions"/> to use for serialization</param> + <param name="cancellationToken">A token to cancel the operation</param> + <returns>A task that completes to return the results of the response payload</returns> + <exception cref="T:System.Text.Json.JsonException"></exception> + <exception cref="T:System.OutOfMemoryException"></exception> + <exception cref="T:VNLib.Data.Caching.Exceptions.InvalidStatusException"></exception> + <exception cref="T:System.ObjectDisposedException"></exception> + <exception cref="T:VNLib.Net.Messaging.FBM.Client.InvalidResponseException"></exception> + </member> + <member name="M:VNLib.Data.Caching.ClientExtensions.AddOrUpdateObjectAsync``1(VNLib.Net.Messaging.FBM.Client.FBMClient,System.String,System.String,``0,System.Text.Json.JsonSerializerOptions,System.Threading.CancellationToken)"> + <summary> + Updates the state of the session, and optionally updates the ID of the session. The data + property is buffered and streamed to the remote server + </summary> + <typeparam name="T"></typeparam> + <param name="client"></param> + <param name="objectId">The id of the object to update or replace</param> + <param name="newId">An optional parameter to specify a new ID for the old object</param> + <param name="data">The payload data to serialize and set as the data state of the session</param> + <param name="jso">Optional <see cref="T:System.Text.Json.JsonSerializerOptions"/></param> + <param name="cancellationToken">A token to cancel the operation</param> + <returns>A task that resolves when the server responds</returns> + <exception cref="T:System.Text.Json.JsonException"></exception> + <exception cref="T:System.OutOfMemoryException"></exception> + <exception cref="T:VNLib.Data.Caching.Exceptions.InvalidStatusException"></exception> + <exception cref="T:System.ObjectDisposedException"></exception> + <exception cref="T:VNLib.Net.Messaging.FBM.Client.InvalidResponseException"></exception> + <exception cref="T:VNLib.Data.Caching.Exceptions.MessageTooLargeException"></exception> + </member> + <member name="M:VNLib.Data.Caching.ClientExtensions.DeleteObjectAsync(VNLib.Net.Messaging.FBM.Client.FBMClient,System.String,System.Threading.CancellationToken)"> + <summary> + Asynchronously deletes an object in the remote store + </summary> + <param name="client"></param> + <param name="objectId">The id of the object to update or replace</param> + <param name="cancellationToken">A token to cancel the operation</param> + <returns>A task that resolves when the operation has completed</returns> + <exception cref="T:VNLib.Data.Caching.Exceptions.InvalidStatusException"></exception> + <exception cref="T:System.ObjectDisposedException"></exception> + <exception cref="T:VNLib.Net.Messaging.FBM.Client.InvalidResponseException"></exception> + </member> + <member name="M:VNLib.Data.Caching.ClientExtensions.ObjectId(VNLib.Net.Messaging.FBM.Server.FBMContext)"> + <summary> + Gets the Object-id for the request message, or throws an <see cref="T:System.InvalidOperationException"/> if not specified + </summary> + <param name="context"></param> + <returns>The id of the object requested</returns> + <exception cref="T:System.InvalidOperationException"></exception> + </member> + <member name="M:VNLib.Data.Caching.ClientExtensions.NewObjectId(VNLib.Net.Messaging.FBM.Server.FBMContext)"> + <summary> + Gets the new ID of the object if specified from the request. Null if the request did not specify an id update + </summary> + <param name="context"></param> + <returns>The new ID of the object if speicifed, null otherwise</returns> + </member> + <member name="M:VNLib.Data.Caching.ClientExtensions.Method(VNLib.Net.Messaging.FBM.Server.FBMContext)"> + <summary> + Gets the request method for the request + </summary> + <param name="context"></param> + <returns>The request method string</returns> + </member> + <member name="M:VNLib.Data.Caching.ClientExtensions.CloseResponse(VNLib.Net.Messaging.FBM.Server.FBMContext,System.String,System.IO.Stream)"> + <summary> + Closes a response with a status code + </summary> + <param name="context"></param> + <param name="responseCode">The status code to send to the client</param> + <param name="payload">The payload to send to the client</param> + </member> + <member name="M:VNLib.Data.Caching.ClientExtensions.CloseResponse(VNLib.Net.Messaging.FBM.Server.FBMContext,System.String)"> + <summary> + Closes a response with a status code + </summary> + <param name="context"></param> + <param name="responseCode">The status code to send to the client</param> + </member> + <member name="M:VNLib.Data.Caching.ClientExtensions.SetAuth(VNLib.Net.Messaging.FBM.Client.FBMClientWorkerBase,System.ReadOnlySpan{System.Byte},System.Int32)"> + <summary> + Computes the authorization headers for the initial client connection + </summary> + <param name="worker"></param> + <param name="secret">The pre-shared secret used to compute a secure hash of a random token</param> + <param name="saltSize">The size (in bytes) of the salt to compute the hash of</param> + </member> + <member name="M:VNLib.Data.Caching.ClientExtensions.Authorized(VNLib.Net.Http.ConnectionInfo,System.ReadOnlySpan{System.Byte})"> + <summary> + Determines if the client has the proper authorization by verifying the client data can compute the same hash result with + the specified secret + </summary> + <param name="server"></param> + <param name="secret">The pre-shared secret used to verify the data</param> + <returns>True if the authorization headers compute to the proper hash result (keys match), false otherwise</returns> + <remarks>The verification is fixed-time</remarks> + <exception cref="T:System.OutOfMemoryException"></exception> + </member> + <member name="M:VNLib.Data.Caching.ClientExtensions.SetReconnectPolicy(VNLib.Net.Messaging.FBM.Client.FBMClientWorkerBase,System.TimeSpan,System.Uri)"> + <summary> + Initializes the worker for a reconnect policy and returns an object that can listen for changes + and configure the connection as necessary + </summary> + <param name="worker"></param> + <param name="retryDelay">The amount of time to wait between retries</param> + <param name="serverUri">The uri to reconnect the client to</param> + <returns>A <see cref="T:VNLib.Data.Caching.ClientRetryManager"/> for listening for retry events</returns> + </member> + <member name="T:VNLib.Data.Caching.ClientRetryManager"> + <summary> + Manages a <see cref="T:VNLib.Net.Messaging.FBM.Client.FBMClientWorkerBase"/> reconnect policy + </summary> + </member> + <member name="E:VNLib.Data.Caching.ClientRetryManager.OnBeforeReconnect"> + <summary> + Raised before client is to be reconnected + </summary> + </member> + <member name="E:VNLib.Data.Caching.ClientRetryManager.OnReconnectFailed"> + <summary> + Raised when the client fails to reconnect. Should return a value that instructs the + manager to reconnect + </summary> + </member> + <member name="E:VNLib.Data.Caching.ClientRetryManager.OnSuccessfulReconnect"> + <summary> + Raised when the client websocket is successfully reconnected + </summary> + </member> + <member name="T:VNLib.Data.Caching.Constants.Actions"> + <summary> + Contains constants the define actions + </summary> + </member> + <member name="T:VNLib.Data.Caching.Constants.ResponseCodes"> + <summary> + Containts constants for operation response codes + </summary> + </member> + <member name="T:VNLib.Data.Caching.Exceptions.InvalidStatusException"> + <summary> + Raised when the response status code of an FBM Request message is not valid for + the specified request + </summary> + </member> + <member name="M:VNLib.Data.Caching.Exceptions.InvalidStatusException.#ctor(System.String,System.String)"> + <summary> + Initalizes a new <see cref="T:VNLib.Data.Caching.Exceptions.InvalidStatusException"/> with the specfied status code + </summary> + <param name="message"></param> + <param name="statusCode"></param> + </member> + <member name="M:VNLib.Data.Caching.Exceptions.InvalidStatusException.#ctor"> + <inheritdoc/> + </member> + <member name="M:VNLib.Data.Caching.Exceptions.InvalidStatusException.#ctor(System.String)"> + <inheritdoc/> + </member> + <member name="M:VNLib.Data.Caching.Exceptions.InvalidStatusException.#ctor(System.String,System.Exception)"> + <inheritdoc/> + </member> + <member name="P:VNLib.Data.Caching.Exceptions.InvalidStatusException.Message"> + <inheritdoc/> + </member> + <member name="T:VNLib.Data.Caching.Exceptions.MessageTooLargeException"> + <summary> + Raised when a request (or server response) calculates the size of the message to be too large to proccess + </summary> + </member> + <member name="M:VNLib.Data.Caching.Exceptions.MessageTooLargeException.#ctor"> + <inheritdoc/> + </member> + <member name="M:VNLib.Data.Caching.Exceptions.MessageTooLargeException.#ctor(System.String)"> + <inheritdoc/> + </member> + <member name="M:VNLib.Data.Caching.Exceptions.MessageTooLargeException.#ctor(System.String,System.Exception)"> + <inheritdoc/> + </member> + <member name="M:VNLib.Data.Caching.Exceptions.MessageTooLargeException.#ctor(System.Runtime.Serialization.SerializationInfo,System.Runtime.Serialization.StreamingContext)"> + <inheritdoc/> + </member> + </members> +</doc> diff --git a/VNLib.Data.Caching/src/WaitForChangeResult.cs b/VNLib.Data.Caching/src/WaitForChangeResult.cs new file mode 100644 index 0000000..a309c7c --- /dev/null +++ b/VNLib.Data.Caching/src/WaitForChangeResult.cs @@ -0,0 +1,21 @@ +namespace VNLib.Data.Caching +{ + /// <summary> + /// The result of a cache server change event + /// </summary> + public readonly struct WaitForChangeResult + { + /// <summary> + /// The operation status code + /// </summary> + public readonly string Status { get; init; } + /// <summary> + /// The current (or old) id of the element that changed + /// </summary> + public readonly string CurrentId { get; init; } + /// <summary> + /// The new id of the element that changed + /// </summary> + public readonly string NewId { get; init; } + } +} |