diff options
Diffstat (limited to 'lib/VNLib.Data.Caching')
-rw-r--r-- | lib/VNLib.Data.Caching/src/ClientExtensions.cs | 300 |
1 files changed, 149 insertions, 151 deletions
diff --git a/lib/VNLib.Data.Caching/src/ClientExtensions.cs b/lib/VNLib.Data.Caching/src/ClientExtensions.cs index 8273486..f0eee06 100644 --- a/lib/VNLib.Data.Caching/src/ClientExtensions.cs +++ b/lib/VNLib.Data.Caching/src/ClientExtensions.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2023 Vaughn Nugent +* Copyright (c) 2024 Vaughn Nugent * * Library: VNLib * Package: VNLib.Data.Caching @@ -24,7 +24,7 @@ using System; using System.Linq; -using System.Text.Json; +using System.Buffers; using System.Threading; using System.Threading.Tasks; using System.Runtime.CompilerServices; @@ -44,14 +44,14 @@ namespace VNLib.Data.Caching /// </summary> public static class ClientExtensions { + private readonly record struct AddOrUpdateState<T>(T State, ICacheObjectSerializer Serializer); - private static readonly JsonCacheObjectSerializer DefaultSerializer = new(256); [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void LogDebug(this FBMClient client, string message, params object?[] args) { client.Config.DebugLog?.Debug($"[CACHE] : {message}", args); - } + } /// <summary> /// Updates the state of the object, and optionally updates the ID of the object. The data @@ -62,31 +62,45 @@ namespace VNLib.Data.Caching /// <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="serializer">The custom serializer to used to serialze the object to binary</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="ArgumentException"></exception> + /// <exception cref="ArgumentNullException"></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 Task AddOrUpdateObjectAsync<T>(this FBMClient client, string objectId, string? newId, T data, CancellationToken cancellationToken = default) + public static Task AddOrUpdateObjectAsync<T>( + this FBMClient client, + string objectId, + string? newId, + T data, + ICacheObjectSerializer serializer, + CancellationToken cancellationToken = default) { - //Use the default/json serialzer if not specified - return AddOrUpdateObjectAsync(client, objectId, newId, data, DefaultSerializer, cancellationToken); - } + ArgumentNullException.ThrowIfNull(serializer); + + //Safe to use struct, should not get promoted to heap during update + AddOrUpdateState<T> state = new(data, serializer); + + return AddOrUpdateObjectAsync(client, objectId, newId, Serialize, state, cancellationToken); + + static void Serialize(AddOrUpdateState<T> state, IBufferWriter<byte> finiteWriter) + => state.Serializer.Serialize(state.State, finiteWriter); + } + /// <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="serializer">The custom serializer to used to serialze the object to binary</param> + /// <param name="data">An <see cref="IObjectData"/> that represents the data to set</param> /// <param name="cancellationToken">A token to cancel the operation</param> /// <returns>A task that resolves when the server responds</returns> /// <exception cref="OutOfMemoryException"></exception> @@ -95,64 +109,11 @@ namespace VNLib.Data.Caching /// <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, - ICacheObjectSerializer serializer, - CancellationToken cancellationToken = default) + public static Task AddOrUpdateObjectAsync(this FBMClient client, string objectId, string? newId, IObjectData data, CancellationToken cancellationToken = default) { - _ = client ?? throw new ArgumentNullException(nameof(client)); - _ = serializer ?? throw new ArgumentNullException(nameof(serializer)); - - client.LogDebug("Updating object {id}, newid {nid}", objectId, newId); - - //Rent a new request - FBMRequest request = client.RentRequest(); - try - { - //Set action as get/create - request.WriteHeader(HeaderCommand.Action, Actions.AddOrUpdate); - - //Set object-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); - } - - //Serialize the message using the request buffer - serializer.Serialize(data, request.GetBodyWriter()); - - //Make request - using FBMResponse response = await client.SendAsync(request, cancellationToken); - response.ThrowIfNotSet(); - - //Get the status code - FBMMessageHeader status = response.Headers.FirstOrDefault(static a => a.Header == HeaderCommand.Status); - - //Check status code - if (status.Value.Equals(ResponseCodes.Okay, StringComparison.OrdinalIgnoreCase)) - { - return; - } - else if (status.Value.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 - { - //Return the request(clears data and reset) - client.ReturnRequest(request); - } + return AddOrUpdateObjectAsync(client, objectId, newId, static d => d.GetData(), data, cancellationToken); } + /// <summary> /// Updates the state of the object, and optionally updates the ID of the object. The data @@ -161,8 +122,9 @@ namespace VNLib.Data.Caching /// <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">An <see cref="IObjectData"/> that represents the data to set</param> + /// <param name="callback">A callback method that will return the desired object data</param> /// <param name="cancellationToken">A token to cancel the operation</param> + /// <param name="state">The state to be passed to the callback</param> /// <returns>A task that resolves when the server responds</returns> /// <exception cref="OutOfMemoryException"></exception> /// <exception cref="InvalidStatusException"></exception> @@ -170,11 +132,31 @@ namespace VNLib.Data.Caching /// <exception cref="InvalidResponseException"></exception> /// <exception cref="MessageTooLargeException"></exception> /// <exception cref="ObjectNotFoundException"></exception> - public static Task AddOrUpdateObjectAsync(this FBMClient client, string objectId, string? newId, IObjectData data, CancellationToken cancellationToken = default) + public static Task AddOrUpdateObjectAsync<T>( + this FBMClient client, + string objectId, + string? newId, + ObjectDataGet<T> callback, + T state, + CancellationToken cancellationToken = default + ) { - return AddOrUpdateObjectAsync(client, objectId, newId, static d => d.GetData(), data, cancellationToken); + //Safe to use struct, should not get promoted to heap during update + ObjectDataGetState<T> getState = new(state, callback); + + return AddOrUpdateObjectAsync(client, objectId, newId, PutData, getState, cancellationToken); + + //Function to put the data from the callback into the writer + static void PutData(ObjectDataGetState<T> state, IBufferWriter<byte> writer) + { + //Get the data from the callback + ReadOnlySpan<byte> data = state.Getter(state.UserState); + //Write the data to the writer + writer.Write(data); + } } + /// <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 @@ -186,16 +168,26 @@ namespace VNLib.Data.Caching /// <param name="cancellationToken">A token to cancel the operation</param> /// <param name="state">The state to be passed to the callback</param> /// <returns>A task that resolves when the server responds</returns> + /// <exception cref="ArgumentException"></exception> + /// <exception cref="ArgumentNullException"></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, ObjectDataReader<T> callback, T state, CancellationToken cancellationToken = default) + public static Task AddOrUpdateObjectAsync<T>( + this FBMClient client, + string objectId, + string? newId, + ObjectDataReader<T> callback, + T state, + CancellationToken cancellationToken = default + ) { - _ = client ?? throw new ArgumentNullException(nameof(client)); - _ = callback ?? throw new ArgumentNullException(nameof(callback)); + ArgumentNullException.ThrowIfNull(client); + ArgumentNullException.ThrowIfNull(callback); + ArgumentException.ThrowIfNullOrWhiteSpace(objectId); client.LogDebug("Updating object {id}, newid {nid}", objectId, newId); @@ -215,53 +207,84 @@ namespace VNLib.Data.Caching request.WriteHeader(Constants.NewObjectId, newId); } - //Write the message body as the objet data - request.WriteBody(callback(state)); + //Write the message body as the object data + callback(state, request.GetBodyWriter()); - //Make request - using FBMResponse response = await client.SendAsync(request, cancellationToken); - response.ThrowIfNotSet(); - - //Get the status code - FBMMessageHeader status = response.Headers.FirstOrDefault(static a => a.Header == HeaderCommand.Status); + return ExecAsync(client, request, objectId, cancellationToken); + } + catch + { + //Return the request(clears data and reset) + client.ReturnRequest(request); + throw; + } - //Check status code - if (status.Value.Equals(ResponseCodes.Okay, StringComparison.OrdinalIgnoreCase)) + static async Task ExecAsync(FBMClient client, FBMRequest request, string objectId, CancellationToken cancellationToken) + { + try { - return; + //Make request + using FBMResponse response = await client.SendAsync(request, cancellationToken); + response.ThrowIfNotSet(); + + //Get the status code + FBMMessageHeader status = response.Headers.FirstOrDefault(static a => a.Header == HeaderCommand.Status); + + //Check status code + if (status.ValueEquals(ResponseCodes.Okay, StringComparison.OrdinalIgnoreCase)) + { + return; + } + else if (status.ValueEquals(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()); } - else if (status.Value.Equals(ResponseCodes.NotFound, StringComparison.OrdinalIgnoreCase)) + finally { - throw new ObjectNotFoundException($"object {objectId} not found on remote server"); + //Return the request(clears data and reset) + client.ReturnRequest(request); } - - //Invalid status - throw new InvalidStatusException("Invalid status code recived for object upsert request", status.ToString()); - } - finally - { - //Return the request(clears data and reset) - client.ReturnRequest(request); } } + /// <summary> - /// Gets an object from the server if it exists, and uses the default serialzer to - /// recover the object + /// 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> + /// <param name="getter">A callback function that computes an object result from binary data</param> + /// <param name="state">A user-state parameter to be passed back to the callback function</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="ArgumentException"></exception> + /// <exception cref="ArgumentNullException"></exception> /// <exception cref="InvalidStatusException"></exception> /// <exception cref="ObjectDisposedException"></exception> /// <exception cref="InvalidResponseException"></exception> - public static Task<T?> GetObjectAsync<T>(this FBMClient client, string objectId, CancellationToken cancellationToken = default) + public static async Task<T?> GetObjectAsync<T, TState>( + this FBMClient client, + string objectId, + GetObjectFromData<T,TState> getter, + TState state, + CancellationToken cancellationToken = default + ) { - return GetObjectAsync<T>(client, objectId, DefaultSerializer, cancellationToken); + ArgumentNullException.ThrowIfNull(getter); + + //Get state will store the object result if successfull get operation + GetObjectState<T, TState> st = new(state, getter); + + //Get the object, if successfull, compute the result + bool success = await GetObjectAsync(client, objectId, static (s, d) => s.ComputeResult(d), st, cancellationToken); + + //If the get operation failed, return a default value + return success ? st.Result : default; } /// <summary> @@ -273,51 +296,17 @@ namespace VNLib.Data.Caching /// <param name="cancellationToken">A token to cancel the operation</param> /// <param name="deserialzer">The custom data deserialzer used to deserialze the binary cache result</param> /// <returns>A task that completes to return the results of the response payload</returns> + /// <exception cref="ArgumentException"></exception> + /// <exception cref="ArgumentNullException"></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, ICacheObjectDeserializer deserialzer, CancellationToken cancellationToken = default) + public static Task<T?> GetObjectAsync<T>(this FBMClient client, string objectId, ICacheObjectDeserializer deserialzer, CancellationToken cancellationToken = default) { - _ = client ?? throw new ArgumentNullException(nameof(client)); - _ = deserialzer ?? throw new ArgumentNullException(nameof(deserialzer)); - - client.LogDebug("Getting object {id}", objectId); - - //Rent a new request - FBMRequest request = client.RentRequest(); - try - { - //Set action as get/create - request.WriteHeader(HeaderCommand.Action, Actions.Get); - - //Set object id header - request.WriteHeader(Constants.ObjectId, objectId); - - //Make request - using FBMResponse response = await client.SendAsync(request, cancellationToken); - response.ThrowIfNotSet(); - - //Get the status code - FBMMessageHeader status = response.Headers.FirstOrDefault(static a => a.Header == HeaderCommand.Status); - - //Check ok status code, then its safe to deserialize - if (status.Value.Equals(ResponseCodes.Okay, StringComparison.Ordinal)) - { - return deserialzer.Deserialize<T>(response.ResponseBody); - } - - //Object may not exist on the server yet - if (status.Value.Equals(ResponseCodes.NotFound, StringComparison.Ordinal)) - { - return default; - } - - throw new InvalidStatusException("Invalid status code recived for object get request", status.ToString()); - } - finally - { - client.ReturnRequest(request); - } + ArgumentNullException.ThrowIfNull(deserialzer); + + //Use the deserialzer to deserialize the data as a state parameter + return GetObjectAsync(client, objectId, static (s, d) => s.Deserialize<T>(d), deserialzer, cancellationToken); } /// <summary> @@ -330,6 +319,8 @@ namespace VNLib.Data.Caching /// <param name="data">An object data instance used to store the found object data</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="ArgumentException"></exception> + /// <exception cref="ArgumentNullException"></exception> /// <exception cref="InvalidStatusException"></exception> /// <exception cref="ObjectDisposedException"></exception> /// <exception cref="InvalidResponseException"></exception> @@ -349,13 +340,16 @@ namespace VNLib.Data.Caching /// <param name="state">The state parameter to pass to the callback method</param> /// <param name="cancellationToken">A token to cancel the operation</param> /// <returns>When complete, true if the object was found, false if not found, and an exception otherwise</returns> + /// <exception cref="ArgumentException"></exception> + /// <exception cref="ArgumentNullException"></exception> /// <exception cref="InvalidStatusException"></exception> /// <exception cref="ObjectDisposedException"></exception> /// <exception cref="InvalidResponseException"></exception> public static async Task<bool> GetObjectAsync<T>(this FBMClient client, string objectId, ObjectDataSet<T> setter, T state, CancellationToken cancellationToken = default) { - _ = client ?? throw new ArgumentNullException(nameof(client)); - _ = setter ?? throw new ArgumentNullException(nameof(setter)); + ArgumentNullException.ThrowIfNull(client); + ArgumentNullException.ThrowIfNull(setter); + ArgumentException.ThrowIfNullOrWhiteSpace(objectId); client.LogDebug("Getting object {id}", objectId); @@ -405,13 +399,16 @@ namespace VNLib.Data.Caching /// <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="ArgumentException"></exception> + /// <exception cref="ArgumentNullException"></exception> /// <exception cref="InvalidStatusException"></exception> /// <exception cref="ObjectDisposedException"></exception> /// <exception cref="InvalidResponseException"></exception> /// <exception cref="ObjectNotFoundException"></exception> public static async Task<bool> DeleteObjectAsync(this FBMClient client, string objectId, CancellationToken cancellationToken = default) { - _ = client ?? throw new ArgumentNullException(nameof(client)); + ArgumentNullException.ThrowIfNull(client); + ArgumentException.ThrowIfNullOrWhiteSpace(objectId); client.LogDebug("Deleting object {id}", objectId); @@ -431,11 +428,11 @@ namespace VNLib.Data.Caching //Get the status code FBMMessageHeader status = response.Headers.FirstOrDefault(static a => a.Header == HeaderCommand.Status); - if (status.Value.Equals(ResponseCodes.Okay, StringComparison.Ordinal)) + if (status.ValueEquals(ResponseCodes.Okay, StringComparison.Ordinal)) { return true; } - else if (status.Value.Equals(ResponseCodes.NotFound, StringComparison.OrdinalIgnoreCase)) + else if (status.ValueEquals(ResponseCodes.NotFound, StringComparison.OrdinalIgnoreCase)) { return false; } @@ -459,7 +456,8 @@ namespace VNLib.Data.Caching /// <exception cref="InvalidOperationException"></exception> public static async Task WaitForChangeAsync(this FBMClient client, WaitForChangeResult change, CancellationToken cancellationToken = default) { - _ = change ?? throw new ArgumentNullException(nameof(change)); + ArgumentNullException.ThrowIfNull(client); + ArgumentNullException.ThrowIfNull(change); //Rent a new request FBMRequest request = client.RentRequest(); @@ -473,9 +471,9 @@ namespace VNLib.Data.Caching response.ThrowIfNotSet(); - change.Status = response.Headers.FirstOrDefault(static a => a.Header == HeaderCommand.Status).Value.ToString(); - change.CurrentId = response.Headers.SingleOrDefault(static v => v.Header == Constants.ObjectId).Value.ToString(); - change.NewId = response.Headers.SingleOrDefault(static v => v.Header == Constants.NewObjectId).Value.ToString(); + change.Status = response.Headers.FirstOrDefault(static a => a.Header == HeaderCommand.Status).GetValueString(); + change.CurrentId = response.Headers.SingleOrDefault(static v => v.Header == Constants.ObjectId).GetValueString(); + change.NewId = response.Headers.SingleOrDefault(static v => v.Header == Constants.NewObjectId).GetValueString(); } finally { |