From 5c2dc879179e4d8ece012a5078152a601a4914ba Mon Sep 17 00:00:00 2001 From: vnugent Date: Sat, 21 Jan 2023 21:45:05 -0500 Subject: Serialzer pooling support added --- lib/VNLib.Data.Caching/src/ClientExtensions.cs | 60 ++++++++++++++------- lib/VNLib.Data.Caching/src/ReusableJsonWriter.cs | 69 ++++++++++++++++++++++++ 2 files changed, 110 insertions(+), 19 deletions(-) create mode 100644 lib/VNLib.Data.Caching/src/ReusableJsonWriter.cs diff --git a/lib/VNLib.Data.Caching/src/ClientExtensions.cs b/lib/VNLib.Data.Caching/src/ClientExtensions.cs index a207886..0a24a83 100644 --- a/lib/VNLib.Data.Caching/src/ClientExtensions.cs +++ b/lib/VNLib.Data.Caching/src/ClientExtensions.cs @@ -23,7 +23,6 @@ */ using System; -using System.IO; using System.Linq; using System.Buffers; using System.Text.Json; @@ -31,23 +30,26 @@ 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.Utils.Memory.Caching; 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 { - /// /// Provides caching extension methods for /// public static class ClientExtensions { + //Create threadlocal writer for attempted reuse without locks + private static readonly ObjectRental JsonWriterPool = ObjectRental.Create(); + private static readonly JsonSerializerOptions LocalOptions = new() { DictionaryKeyPolicy = JsonNamingPolicy.CamelCase, @@ -63,12 +65,10 @@ namespace VNLib.Data.Caching DefaultBufferSize = 128 }; + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void LogDebug(this FBMClient client, string message, params object?[] args) { - if (client.Config.DebugLog != null) - { - client.Config.DebugLog.Debug($"[CACHE] : {message}", args); - } + client.Config.DebugLog?.Debug($"[CACHE] : {message}", args); } /// @@ -96,24 +96,29 @@ namespace VNLib.Data.Caching { //Set action as get/create request.WriteHeader(HeaderCommand.Action, Actions.Get); - //Set session-id header + + //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 ReadOnlyMemory status = response.Headers.FirstOrDefault(static a => a.Key == HeaderCommand.Status).Value; + + //Check ok status code, then its safe to deserialize if (status.Span.Equals(ResponseCodes.Okay, StringComparison.Ordinal)) { return JsonSerializer.Deserialize(response.ResponseBody, LocalOptions); } - //Session may not exist on the server yet + + //Object 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 @@ -145,34 +150,39 @@ namespace VNLib.Data.Caching _ = client ?? throw new ArgumentNullException(nameof(client)); client.LogDebug("Updating object {id}, newid {nid}", objectId, newId); - + + //Rent new json writer + ReusableJsonWriter writer = JsonWriterPool.Rent(); + //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 bodyWriter = request.GetBodyWriter(); - //Write json data to the message - using (Utf8JsonWriter jsonWriter = new(bodyWriter)) - { - JsonSerializer.Serialize(jsonWriter, data, LocalOptions); - } + + //Serialize the message + writer.Serialize(bodyWriter, data, LocalOptions); //Make request using FBMResponse response = await client.SendAsync(request, cancellationToken); - response.ThrowIfNotSet(); + //Get the status code ReadOnlyMemory status = response.Headers.FirstOrDefault(static a => a.Key == HeaderCommand.Status).Value; + //Check status code if (status.Span.Equals(ResponseCodes.Okay, StringComparison.OrdinalIgnoreCase)) { @@ -182,6 +192,7 @@ namespace VNLib.Data.Caching { 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()); } @@ -189,6 +200,8 @@ namespace VNLib.Data.Caching { //Return the request(clears data and reset) client.ReturnRequest(request); + //Return writer to pool later + JsonWriterPool.Return(writer); } } @@ -208,6 +221,7 @@ namespace VNLib.Data.Caching _ = client ?? throw new ArgumentNullException(nameof(client)); client.LogDebug("Deleting object {id}", objectId); + //Rent a new request FBMRequest request = client.RentRequest(); try @@ -219,10 +233,11 @@ namespace VNLib.Data.Caching //Make request using FBMResponse response = await client.SendAsync(request, cancellationToken); - response.ThrowIfNotSet(); + //Get the status code ReadOnlyMemory status = response.Headers.FirstOrDefault(static a => a.Key == HeaderCommand.Status).Value; + if (status.Span.Equals(ResponseCodes.Okay, StringComparison.Ordinal)) { return; @@ -231,6 +246,7 @@ namespace VNLib.Data.Caching { throw new ObjectNotFoundException($"object {objectId} not found on remote server"); } + throw new InvalidStatusException("Invalid status code recived for object get request", status.ToString()); } finally @@ -278,39 +294,45 @@ namespace VNLib.Data.Caching /// /// The id of the object requested /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static string ObjectId(this FBMContext context) { return context.Request.Headers.First(static kvp => kvp.Key == Constants.ObjectId).Value.ToString(); } + /// /// Gets the new ID of the object if specified from the request. Null if the request did not specify an id update /// /// /// The new ID of the object if speicifed, null otherwise + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static string? NewObjectId(this FBMContext context) { return context.Request.Headers.FirstOrDefault(static kvp => kvp.Key == Constants.NewObjectId).Value.ToString(); } + /// /// Gets the request method for the request /// /// /// The request method string + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static string Method(this FBMContext context) { return context.Request.Headers.First(static kvp => kvp.Key == HeaderCommand.Action).Value.ToString(); } + /// /// Closes a response with a status code /// /// /// The status code to send to the client + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void CloseResponse(this FBMContext context, string responseCode) { context.Response.WriteHeader(HeaderCommand.Status, responseCode); } - /// /// Initializes the worker for a reconnect policy and returns an object that can listen for changes /// and configure the connection as necessary diff --git a/lib/VNLib.Data.Caching/src/ReusableJsonWriter.cs b/lib/VNLib.Data.Caching/src/ReusableJsonWriter.cs new file mode 100644 index 0000000..c763f91 --- /dev/null +++ b/lib/VNLib.Data.Caching/src/ReusableJsonWriter.cs @@ -0,0 +1,69 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Data.Caching +* File: ReusableJsonWriter.cs +* +* ReusableJsonWriter.cs is part of VNLib.Data.Caching which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Data.Caching is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Data.Caching is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +using System.IO; +using System.Buffers; +using System.Text.Json; + +using VNLib.Utils; + +namespace VNLib.Data.Caching +{ + internal sealed class ReusableJsonWriter : VnDisposeable + { + private readonly Utf8JsonWriter _writer; + + public ReusableJsonWriter() + { + _writer = new(Stream.Null); + } + + /// + /// Serializes the message and writes the serialzied data to the buffer writer + /// + /// + /// The buffer writer to store data at + /// The object to serialize + /// Optional - serializer options + public void Serialize(IBufferWriter writer, T value, JsonSerializerOptions? options = null) + { + //Init the writer with the new buffer writer + _writer.Reset(writer); + try + { + //Serialize message + JsonSerializer.Serialize(_writer, value, options); + //Flush writer to underlying buffer + _writer.Flush(); + } + finally + { + //Unlink the writer + _writer.Reset(Stream.Null); + } + } + + protected override void Free() => _writer.Dispose(); + } +} -- cgit