/*
* Copyright (c) 2023 Vaughn Nugent
*
* Library: VNLib
* Package: VNLib.Data.Caching.ObjectCache
* File: CacheEntry.cs
*
* CacheEntry.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.Buffers;
using System.Diagnostics;
using System.Buffers.Binary;
using System.Runtime.CompilerServices;
using VNLib.Utils.Memory;
namespace VNLib.Data.Caching
{
///
/// A structure that represents an item in cache. It contains the binary content
/// of a cache entry by its internal memory handle
///
public readonly record struct CacheEntry : IDisposable
{
private const int TIME_SEGMENT_SIZE = sizeof(long);
private const int LENGTH_SEGMENT_SIZE = sizeof(int);
private const int DATA_SEGMENT_START = TIME_SEGMENT_SIZE + LENGTH_SEGMENT_SIZE;
private readonly ICacheEntryMemoryManager _manager;
private readonly object _handle;
///
/// Creates a new and copies the initial data to the internal buffer
///
/// The initial data to store
/// The heap to allocate the buffer from
/// The newly initialized and ready to use
///
public static CacheEntry Create(ReadOnlySpan data, ICacheEntryMemoryManager dataManager)
{
_ = dataManager ?? throw new ArgumentNullException(nameof(dataManager));
//Calc buffer size
uint bufferSize = GetRequiredHandleSize(data.Length);
object handle = dataManager.AllocHandle(bufferSize);
//Create new entry from handle
CacheEntry entry = new(dataManager, handle);
entry.SetLength((uint)data.Length);
//Get the data segment
Span segment = entry.GetDataSegment();
Debug.Assert(segment.Length == data.Length);
//Copy data segment
data.CopyTo(segment);
return entry;
}
///
/// Creates a new from an existing handle
///
/// The cache data handle to create the entry around
/// The cache entry memory manager the handle blongs to
/// The re-constructed entry
///
///
public static CacheEntry FromExistingHandle(object handle, ICacheEntryMemoryManager manager)
{
_ = handle ?? throw new ArgumentNullException(nameof(handle));
_ = manager ?? throw new ArgumentNullException(nameof(manager));
//validate handle size it at least the minimum size
if (manager.GetHandleSize(handle) < DATA_SEGMENT_START)
{
throw new ArgumentException("Memory segment is too small to be a valid cache entry");
}
return new(manager, handle);
}
private static uint GetRequiredHandleSize(int size)
{
//Caculate the minimum handle size to store all required information, rounded to nearest page
return (uint)MemoryUtil.NearestPage(size + DATA_SEGMENT_START);
}
private CacheEntry(ICacheEntryMemoryManager manager, object handle)
{
_manager = manager;
_handle = handle;
}
///
public readonly void Dispose() => _manager?.FreeHandle(_handle);
private readonly Span GetTimeSegment() => _manager.GetSpan(_handle, 0, TIME_SEGMENT_SIZE);
private readonly Span GetLengthSegment() => _manager.GetSpan(_handle, TIME_SEGMENT_SIZE, LENGTH_SEGMENT_SIZE);
///
/// Gets the size of the block of memory held by the underlying handle
///
/// The size of the block held by the current entry
///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly nuint GetMemoryUsage() => _manager.GetHandleSize(_handle);
///
/// Gets the last set time
///
/// The last date stored
///
public readonly DateTime GetTime()
{
//Get the time segment and write the value in big endian
ReadOnlySpan segment = GetTimeSegment();
long ticks = BinaryPrimitives.ReadInt64BigEndian(segment);
//ticks back to
return new(ticks);
}
///
/// Sets the last modified time
///
/// The new time to set the handle to
///
public readonly void SetTime(DateTime time)
{
//Get native ticks value
long timeData = time.Ticks;
//Get the time segment and write the value in big endian
Span segment = GetTimeSegment();
BinaryPrimitives.WriteInt64BigEndian(segment, timeData);
}
///
/// Gets the length of the data segment
///
/// The length of the data segment
///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly uint GetLength()
{
//Get the length segment
ReadOnlySpan segment = GetLengthSegment();
//Recover the integer
return BinaryPrimitives.ReadUInt32BigEndian(segment);
}
private readonly void SetLength(uint length)
{
//Get the length segment
Span segment = GetLengthSegment();
//Update the length value
BinaryPrimitives.WriteUInt32BigEndian(segment, length);
}
///
/// Gets the stored data segment
///
/// The data segment
///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly Span GetDataSegment()
{
//Get the actual length of the segment
uint length = GetLength();
//Get the segment from its begining offset and
return _manager.GetSpan(_handle, DATA_SEGMENT_START, length);
}
///
/// Writes the specified segment to the internal buffer and resizes the buffer if necessary.
/// This operation overwrites any previously stored data
///
/// The data segment to store
///
public readonly void UpdateData(ReadOnlySpan data)
{
//Calc required buffer size
uint bufferSize = GetRequiredHandleSize(data.Length);
//Resize buffer if necessary
if(_manager.GetHandleSize(_handle) < bufferSize)
{
//resize handle
_manager.ResizeHandle(_handle, bufferSize);
}
//Reset data length
SetLength((uint)data.Length);
//Get the data segment
Span segment = GetDataSegment();
//Test segment length is equivalent to the requested data length
Debug.Assert(segment.Length == data.Length);
//Copy data segment
data.CopyTo(segment);
}
///
/// Gets a offset to the start of the
/// internal data segment, and avoids calling the fixed keyword.
/// The handle must be disposed/released to avoid memeory leaks.
///
///
/// WARNING: You must respect the return value so
/// as not to overrun the valid data segment.
///
/// A handle that points to the begining of the data segment
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly MemoryHandle UnsafeGetDataSegmentHandle()
{
//Get the handle offset to the data segment start, the caller must know when the data segment ends
return _manager.PinHandle(_handle, DATA_SEGMENT_START);
}
///
/// Gets the internal memory handle and manager its associated with
///
/// The opaque memory handle
/// The associated memory manager
public readonly void GetInternalHandle(out object handle, out ICacheEntryMemoryManager manager)
{
handle = _handle;
manager = _manager;
}
}
}