aboutsummaryrefslogtreecommitdiff
path: root/lib/VNLib.Data.Caching.ObjectCache/src/CacheEntry.cs
blob: 93709019f8e293c410ba3da22651c626eeac6697 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
/*
* 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
{

    /// <summary>
    /// A structure that represents an item in cache. It contains the binary content
    /// of a cache entry by its internal memory handle
    /// </summary>
    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;

        /// <summary>
        /// Creates a new <see cref="CacheEntry"/> and copies the initial data to the internal buffer
        /// </summary>
        /// <param name="data">The initial data to store</param>
        /// <param name="dataManager">The heap to allocate the buffer from</param>
        /// <returns>The newly initialized and ready to use <see cref="CacheEntry"/></returns>
        /// <exception cref="ArgumentNullException"></exception>
        public static CacheEntry Create(ReadOnlySpan<byte> 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<byte> segment = entry.GetDataSegment();

            Debug.Assert(segment.Length == data.Length);

            //Copy data segment
            data.CopyTo(segment);

            return entry;
        }

        /// <summary>
        /// Creates a new <see cref="CacheEntry"/> from an existing handle
        /// </summary>
        /// <param name="handle">The cache data handle to create the entry around</param>
        /// <param name="manager">The cache entry memory manager the handle blongs to</param>
        /// <returns>The re-constructed entry</returns>
        /// <exception cref="ArgumentNullException"></exception>
        /// <exception cref="ArgumentException"></exception>
        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;
        }

        ///<inheritdoc/>
        public readonly void Dispose() => _manager?.FreeHandle(_handle);
       
        private readonly Span<byte> GetTimeSegment() => _manager.GetSpan(_handle, 0, TIME_SEGMENT_SIZE);
      
        private readonly Span<byte> GetLengthSegment() => _manager.GetSpan(_handle, TIME_SEGMENT_SIZE, LENGTH_SEGMENT_SIZE);

        /// <summary>
        /// Gets the size of the block of memory held by the underlying handle
        /// </summary>
        /// <returns>The size of the block held by the current entry</returns>
        /// <exception cref="ObjectDisposedException"></exception>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public readonly nuint GetMemoryUsage() => _manager.GetHandleSize(_handle);

        /// <summary>
        /// Gets the last set time
        /// </summary>
        /// <returns>The last date stored</returns>
        /// <exception cref="ObjectDisposedException"></exception>
        public readonly DateTime GetTime()
        {
            //Get the time segment and write the value in big endian
            ReadOnlySpan<byte> segment = GetTimeSegment();

            long ticks = BinaryPrimitives.ReadInt64BigEndian(segment);

            //ticks back to 
            return new(ticks);
        }

        /// <summary>
        /// Sets the last modified time
        /// </summary>
        /// <param name="time">The new time to set the handle to</param>
        /// <exception cref="ObjectDisposedException"></exception>
        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<byte> segment = GetTimeSegment();

            BinaryPrimitives.WriteInt64BigEndian(segment, timeData);
        }

        /// <summary>
        /// Gets the length of the data segment
        /// </summary>
        /// <returns>The length of the data segment</returns>
        /// <exception cref="ObjectDisposedException"></exception>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public readonly uint GetLength()
        {
            //Get the length segment
            ReadOnlySpan<byte> segment = GetLengthSegment();
            //Recover the integer
            return BinaryPrimitives.ReadUInt32BigEndian(segment);
        }

        private readonly void SetLength(uint length)
        {
            //Get the length segment
            Span<byte> segment = GetLengthSegment();
        
            //Update the length value
            BinaryPrimitives.WriteUInt32BigEndian(segment, length);
        }

        /// <summary>
        /// Gets the stored data segment
        /// </summary>
        /// <returns>The data segment</returns>
        /// <exception cref="ObjectDisposedException"></exception>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public readonly Span<byte> GetDataSegment()
        {
            //Get the segment from its begining offset and 
            return _manager.GetSpan(_handle, DATA_SEGMENT_START, GetLength());
        }

        /// <summary>
        /// Writes the specified segment to the internal buffer and resizes the buffer if necessary.
        /// This operation overwrites any previously stored data
        /// </summary>
        /// <param name="data">The data segment to store</param>
        /// <exception cref="ObjectDisposedException"></exception>
        public readonly void UpdateData(ReadOnlySpan<byte> 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<byte> segment = GetDataSegment();

            //Test segment length is equivalent to the requested data length
            Debug.Assert(segment.Length == data.Length);

            //Copy data segment
            data.CopyTo(segment);
        }

        /// <summary>
        /// Gets a <see cref="MemoryHandle"/> 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.
        /// </summary>
        /// <remarks>
        /// WARNING: You must respect the <see cref="GetLength"/> return value so 
        /// as not to overrun the valid data segment.
        /// </remarks>
        /// <returns>A handle that points to the begining of the data segment</returns>
        [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);
        }

        /// <summary>
        /// Gets the internal memory handle and manager its associated with
        /// </summary>
        /// <param name="handle">The opaque memory handle</param>
        /// <param name="manager">The associated memory manager</param>
        public readonly void GetInternalHandle(out object handle, out ICacheEntryMemoryManager manager)
        {
            handle = _handle;
            manager = _manager;
        }
    }
}