aboutsummaryrefslogtreecommitdiff
path: root/lib/VNLib.Data.Caching.ObjectCache/src
diff options
context:
space:
mode:
Diffstat (limited to 'lib/VNLib.Data.Caching.ObjectCache/src')
-rw-r--r--lib/VNLib.Data.Caching.ObjectCache/src/BlobCache.cs139
-rw-r--r--lib/VNLib.Data.Caching.ObjectCache/src/BlobItem.cs207
-rw-r--r--lib/VNLib.Data.Caching.ObjectCache/src/CacheListener.cs64
-rw-r--r--lib/VNLib.Data.Caching.ObjectCache/src/ChangeEvent.cs42
-rw-r--r--lib/VNLib.Data.Caching.ObjectCache/src/ObjectCacheStore.cs263
-rw-r--r--lib/VNLib.Data.Caching.ObjectCache/src/VNLib.Data.Caching.ObjectCache.csproj34
6 files changed, 749 insertions, 0 deletions
diff --git a/lib/VNLib.Data.Caching.ObjectCache/src/BlobCache.cs b/lib/VNLib.Data.Caching.ObjectCache/src/BlobCache.cs
new file mode 100644
index 0000000..26df351
--- /dev/null
+++ b/lib/VNLib.Data.Caching.ObjectCache/src/BlobCache.cs
@@ -0,0 +1,139 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Data.Caching.ObjectCache
+* File: BlobCache.cs
+*
+* BlobCache.cs is part of VNLib.Data.Caching.ObjectCache which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Data.Caching.ObjectCache 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.ObjectCache 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;
+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;
+
+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/lib/VNLib.Data.Caching.ObjectCache/src/BlobItem.cs b/lib/VNLib.Data.Caching.ObjectCache/src/BlobItem.cs
new file mode 100644
index 0000000..728875f
--- /dev/null
+++ b/lib/VNLib.Data.Caching.ObjectCache/src/BlobItem.cs
@@ -0,0 +1,207 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Data.Caching.ObjectCache
+* File: BlobItem.cs
+*
+* BlobItem.cs is part of VNLib.Data.Caching.ObjectCache which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Data.Caching.ObjectCache 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.ObjectCache 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;
+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;
+
+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/lib/VNLib.Data.Caching.ObjectCache/src/CacheListener.cs b/lib/VNLib.Data.Caching.ObjectCache/src/CacheListener.cs
new file mode 100644
index 0000000..7f544e1
--- /dev/null
+++ b/lib/VNLib.Data.Caching.ObjectCache/src/CacheListener.cs
@@ -0,0 +1,64 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Data.Caching.ObjectCache
+* File: CacheListener.cs
+*
+* CacheListener.cs is part of VNLib.Data.Caching.ObjectCache which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Data.Caching.ObjectCache 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.ObjectCache 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;
+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/lib/VNLib.Data.Caching.ObjectCache/src/ChangeEvent.cs b/lib/VNLib.Data.Caching.ObjectCache/src/ChangeEvent.cs
new file mode 100644
index 0000000..7ee4427
--- /dev/null
+++ b/lib/VNLib.Data.Caching.ObjectCache/src/ChangeEvent.cs
@@ -0,0 +1,42 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Data.Caching.ObjectCache
+* File: ChangeEvent.cs
+*
+* ChangeEvent.cs is part of VNLib.Data.Caching.ObjectCache which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Data.Caching.ObjectCache 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.ObjectCache 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/.
+*/
+
+namespace VNLib.Data.Caching.ObjectCache
+{
+ /// <summary>
+ /// An event object that is passed when change events occur
+ /// </summary>
+ public class ChangeEvent
+ {
+ public readonly string CurrentId;
+ public readonly string? AlternateId;
+ public readonly bool Deleted;
+ internal ChangeEvent(string id, string? alternate, bool deleted)
+ {
+ CurrentId = id;
+ AlternateId = alternate;
+ Deleted = deleted;
+ }
+ }
+}
diff --git a/lib/VNLib.Data.Caching.ObjectCache/src/ObjectCacheStore.cs b/lib/VNLib.Data.Caching.ObjectCache/src/ObjectCacheStore.cs
new file mode 100644
index 0000000..514d00b
--- /dev/null
+++ b/lib/VNLib.Data.Caching.ObjectCache/src/ObjectCacheStore.cs
@@ -0,0 +1,263 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Data.Caching.ObjectCache
+* File: ObjectCacheStore.cs
+*
+* ObjectCacheStore.cs is part of VNLib.Data.Caching.ObjectCache which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Data.Caching.ObjectCache 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.ObjectCache 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;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+
+using VNLib.Utils.Async;
+using VNLib.Utils.Memory;
+using VNLib.Utils.Logging;
+using VNLib.Utils.Extensions;
+using VNLib.Net.Messaging.FBM.Server;
+using static VNLib.Data.Caching.Constants;
+
+namespace VNLib.Data.Caching.ObjectCache
+{
+ public delegate ReadOnlySpan<byte> GetBodyDataCallback<T>(T state);
+
+ /// <summary>
+ /// A <see cref="FBMListener"/> implementation of a <see cref="CacheListener"/>
+ /// </summary>
+ public class ObjectCacheStore : CacheListener, IDisposable
+ {
+ private readonly SemaphoreSlim StoreLock;
+ private bool disposedValue;
+
+ ///<inheritdoc/>
+ protected override ILogProvider Log { get; }
+
+ /// <summary>
+ /// A queue that stores update and delete events
+ /// </summary>
+ public AsyncQueue<ChangeEvent> EventQueue { get; }
+
+ /// <summary>
+ /// Initialzies a new <see cref="ObjectCacheStore"/>
+ /// </summary>
+ /// <param name="dir">The <see cref="DirectoryInfo"/> to store blob files to</param>
+ /// <param name="cacheMax"></param>
+ /// <param name="log"></param>
+ /// <param name="heap"></param>
+ /// <param name="singleReader">A value that indicates if a single thread is processing events</param>
+ public ObjectCacheStore(DirectoryInfo dir, int cacheMax, ILogProvider log, IUnmangedHeap heap, bool singleReader)
+ {
+ Log = log;
+ //We can use a single writer and single reader in this context
+ EventQueue = new(true, singleReader);
+ InitCache(dir, cacheMax, heap);
+ InitListener(heap);
+ StoreLock = new(1,1);
+ }
+
+ ///<inheritdoc/>
+ protected override async Task ProcessAsync(FBMContext context, object? userState, CancellationToken cancellationToken)
+ {
+ try
+ {
+ //Get the action header
+ string action = context.Method();
+ //Optional newid header
+ string? alternateId = context.NewObjectId();
+
+ switch (action)
+ {
+ case Actions.Get:
+ {
+ //Get the object-id header
+ string objectId = context.ObjectId();
+ //Take lock on store
+ using SemSlimReleaser rel = await StoreLock.GetReleaserAsync(cancellationToken: cancellationToken);
+ if (Cache!.TryGetValue(objectId, out MemoryHandle<byte>? data))
+ {
+ //Set the status code and write the buffered data to the response buffer
+ context.CloseResponse(ResponseCodes.Okay);
+ //Copy data to response buffer
+ context.Response.WriteBody(data.Span);
+ }
+ else
+ {
+ context.CloseResponse(ResponseCodes.NotFound);
+ }
+ }
+ break;
+ case Actions.AddOrUpdate:
+ {
+ //Get the object-id header
+ string objectId = context.ObjectId();
+ //Add/update a blob async
+ await AddOrUpdateBlobAsync(objectId, alternateId, static context => context.Request.BodyData, context);
+ //Notify update the event bus
+ await EventQueue.EnqueueAsync(new(objectId, alternateId, false), cancellationToken);
+ //Set status code
+ context.CloseResponse(ResponseCodes.Okay);
+ }
+ break;
+ case Actions.Delete:
+ {
+ //Get the object-id header
+ string objectId = context.ObjectId();
+
+ if (await DeleteItemAsync(objectId))
+ {
+ //Notify deleted
+ await EventQueue.EnqueueAsync(new(objectId, null, true), cancellationToken);
+ //Set status header
+ context.CloseResponse(ResponseCodes.Okay);
+ }
+ else
+ {
+ //Set status header
+ context.CloseResponse(ResponseCodes.NotFound);
+ }
+ }
+ break;
+ // event queue dequeue request
+ case Actions.Dequeue:
+ {
+ //If no event bus is registered, then this is not a legal command
+ if (userState is not AsyncQueue<ChangeEvent> eventBus)
+ {
+ context.CloseResponse(ResponseCodes.NotFound);
+ break;
+ }
+ //Wait for a new message to process
+ ChangeEvent ev = await eventBus.DequeueAsync(cancellationToken);
+ if (ev.Deleted)
+ {
+ context.CloseResponse("deleted");
+ context.Response.WriteHeader(ObjectId, ev.CurrentId);
+ }
+ else
+ {
+ //Changed
+ context.CloseResponse("modified");
+ context.Response.WriteHeader(ObjectId, ev.CurrentId);
+ //Set old id if an old id is set
+ if (ev.CurrentId != null)
+ {
+ context.Response.WriteHeader(NewObjectId, ev.AlternateId);
+ }
+ }
+ }
+ break;
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ throw;
+ }
+ catch(Exception ex)
+ {
+ //Log error and set error status code
+ Log.Error(ex);
+ context.CloseResponse(ResponseCodes.Error);
+ }
+ }
+
+ /// <summary>
+ /// Asynchronously deletes a previously stored item
+ /// </summary>
+ /// <param name="id">The id of the object to delete</param>
+ /// <returns>A task that completes when the item has been deleted</returns>
+ public async Task<bool> DeleteItemAsync(string id)
+ {
+ using SemSlimReleaser rel = await StoreLock.GetReleaserAsync();
+ return Cache!.Remove(id);
+ }
+
+ /// <summary>
+ /// Asynchronously adds or updates an object in the store and optionally update's its id
+ /// </summary>
+ /// <param name="objectId">The current (or old) id of the object</param>
+ /// <param name="alternateId">An optional id to update the blob to</param>
+ /// <param name="bodyData">A callback that returns the data for the blob</param>
+ /// <param name="state">The state parameter to pass to the data callback</param>
+ /// <returns></returns>
+ public async Task AddOrUpdateBlobAsync<T>(string objectId, string? alternateId, GetBodyDataCallback<T> bodyData, T state)
+ {
+ MemoryHandle<byte>? blob;
+ //See if new/alt session id was specified
+ if (string.IsNullOrWhiteSpace(alternateId))
+ {
+ //Take lock on store
+ using SemSlimReleaser rel = await StoreLock.GetReleaserAsync();
+ //See if blob exists
+ if (!Cache!.TryGetValue(objectId, out blob))
+ {
+ //If not, create new blob and add to store
+ blob = Heap.AllocAndCopy(bodyData(state));
+ Cache.Add(objectId, blob);
+ }
+ else
+ {
+ //Reset the buffer state
+ blob.WriteAndResize(bodyData(state));
+ }
+ }
+ //Need to change the id of the record
+ else
+ {
+ //Take lock on store
+ using SemSlimReleaser rel = await StoreLock.GetReleaserAsync();
+ //Try to change the blob key
+ if (!Cache!.TryChangeKey(objectId, alternateId, out blob))
+ {
+ //Blob not found, create new blob
+ blob = Heap.AllocAndCopy(bodyData(state));
+ Cache.Add(alternateId, blob);
+ }
+ else
+ {
+ //Reset the buffer state
+ blob.WriteAndResize(bodyData(state));
+ }
+ }
+ }
+
+ ///<inheritdoc/>
+ protected virtual void Dispose(bool disposing)
+ {
+ if (!disposedValue)
+ {
+ if (disposing)
+ {
+ Cache?.Clear();
+ }
+
+ StoreLock.Dispose();
+
+ disposedValue = true;
+ }
+ }
+ ///<inheritdoc/>
+ public void Dispose()
+ {
+ // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
+ Dispose(disposing: true);
+ GC.SuppressFinalize(this);
+ }
+ }
+}
diff --git a/lib/VNLib.Data.Caching.ObjectCache/src/VNLib.Data.Caching.ObjectCache.csproj b/lib/VNLib.Data.Caching.ObjectCache/src/VNLib.Data.Caching.ObjectCache.csproj
new file mode 100644
index 0000000..119622f
--- /dev/null
+++ b/lib/VNLib.Data.Caching.ObjectCache/src/VNLib.Data.Caching.ObjectCache.csproj
@@ -0,0 +1,34 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFramework>net6.0</TargetFramework>
+
+ <Authors>Vaughn Nugent</Authors>
+ <Copyright>Copyright © 2023 Vaughn Nugent</Copyright>
+ <Nullable>enable</Nullable>
+ <GenerateDocumentationFile>True</GenerateDocumentationFile>
+ <PackageProjectUrl>https://www.vaughnnugent.com/resources</PackageProjectUrl>
+ <AssemblyVersion>1.0.0.1</AssemblyVersion>
+ <Version>1.0.1.1</Version>
+ <AnalysisLevel>latest-all</AnalysisLevel>
+ <SignAssembly>True</SignAssembly>
+ <AssemblyOriginatorKeyFile>\\vaughnnugent.com\Internal\Folder Redirection\vman\Documents\Programming\Software\StrongNameingKey.snk</AssemblyOriginatorKeyFile>
+ <Description>Provides server-side object-cache related infrastructure</Description>
+ </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.Data.Caching\src\VNLib.Data.Caching.csproj" />
+ </ItemGroup>
+
+</Project>