Initialize CBDD solution and add a .NET-focused gitignore for generated artifacts.
This commit is contained in:
219
src/CBDD.Core/Storage/StorageEngine.Dictionary.cs
Executable file
219
src/CBDD.Core/Storage/StorageEngine.Dictionary.cs
Executable file
@@ -0,0 +1,219 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Text;
|
||||
|
||||
namespace ZB.MOM.WW.CBDD.Core.Storage;
|
||||
|
||||
public sealed partial class StorageEngine
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, ushort> _dictionaryCache = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly ConcurrentDictionary<ushort, string> _dictionaryReverseCache = new();
|
||||
private uint _dictionaryRootPageId;
|
||||
private ushort _nextDictionaryId;
|
||||
|
||||
// Lock for dictionary modifications (simple lock for now, could be RW lock)
|
||||
private readonly object _dictionaryLock = new();
|
||||
|
||||
private void InitializeDictionary()
|
||||
{
|
||||
// 1. Read File Header (Page 0) to get Dictionary Root
|
||||
var headerBuffer = new byte[PageSize];
|
||||
ReadPage(0, null, headerBuffer);
|
||||
var header = PageHeader.ReadFrom(headerBuffer);
|
||||
|
||||
if (header.DictionaryRootPageId == 0)
|
||||
{
|
||||
// Initialize new Dictionary
|
||||
lock (_dictionaryLock)
|
||||
{
|
||||
// Double check
|
||||
ReadPage(0, null, headerBuffer);
|
||||
header = PageHeader.ReadFrom(headerBuffer);
|
||||
if (header.DictionaryRootPageId == 0)
|
||||
{
|
||||
_dictionaryRootPageId = AllocatePage();
|
||||
|
||||
// Init Dictionary Page
|
||||
var pageBuffer = new byte[PageSize];
|
||||
DictionaryPage.Initialize(pageBuffer, _dictionaryRootPageId);
|
||||
WritePageImmediate(_dictionaryRootPageId, pageBuffer);
|
||||
|
||||
// Update Header
|
||||
header.DictionaryRootPageId = _dictionaryRootPageId;
|
||||
header.WriteTo(headerBuffer);
|
||||
WritePageImmediate(0, headerBuffer);
|
||||
|
||||
// Init Next ID
|
||||
_nextDictionaryId = DictionaryPage.ReservedValuesEnd + 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
_dictionaryRootPageId = header.DictionaryRootPageId;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_dictionaryRootPageId = header.DictionaryRootPageId;
|
||||
|
||||
// Warm cache
|
||||
ushort maxId = DictionaryPage.ReservedValuesEnd;
|
||||
foreach (var (key, val) in DictionaryPage.FindAllGlobal(this, _dictionaryRootPageId))
|
||||
{
|
||||
var lowerKey = key.ToLowerInvariant();
|
||||
_dictionaryCache[lowerKey] = val;
|
||||
_dictionaryReverseCache[val] = lowerKey;
|
||||
if (val > maxId) maxId = val;
|
||||
}
|
||||
_nextDictionaryId = (ushort)(maxId + 1);
|
||||
}
|
||||
|
||||
// Pre-register internal keys used for Schema persistence
|
||||
RegisterKeys(new[] { "_id", "t", "_v", "f", "n", "b", "s", "a" });
|
||||
|
||||
// Pre-register common array indices to avoid mapping during high-frequency writes
|
||||
var indices = new List<string>(101);
|
||||
for (int i = 0; i <= 100; i++) indices.Add(i.ToString());
|
||||
RegisterKeys(indices);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the key-to-id dictionary cache.
|
||||
/// </summary>
|
||||
/// <returns>The key-to-id map.</returns>
|
||||
public ConcurrentDictionary<string, ushort> GetKeyMap() => _dictionaryCache;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the id-to-key dictionary cache.
|
||||
/// </summary>
|
||||
/// <returns>The id-to-key map.</returns>
|
||||
public ConcurrentDictionary<ushort, string> GetKeyReverseMap() => _dictionaryReverseCache;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the ID for a dictionary key, creating it if it doesn't exist.
|
||||
/// Thread-safe.
|
||||
/// </summary>
|
||||
/// <param name="key">The dictionary key.</param>
|
||||
/// <returns>The dictionary identifier for the key.</returns>
|
||||
public ushort GetOrAddDictionaryEntry(string key)
|
||||
{
|
||||
key = key.ToLowerInvariant();
|
||||
if (_dictionaryCache.TryGetValue(key, out var id))
|
||||
{
|
||||
return id;
|
||||
}
|
||||
|
||||
lock (_dictionaryLock)
|
||||
{
|
||||
// Double checked locking
|
||||
if (_dictionaryCache.TryGetValue(key, out id))
|
||||
{
|
||||
return id;
|
||||
}
|
||||
|
||||
// Try to find in storage (in case cache is incomplete or another process?)
|
||||
// Note: FindAllGlobal loaded everything, so strict cache miss means it's not in DB.
|
||||
// BUT if we support concurrent writers (multiple processed), we should re-check DB.
|
||||
// Current CBDD seems to be single-process exclusive lock (FileShare.None).
|
||||
// So in-memory cache is authoritative after load.
|
||||
|
||||
// Generate New ID
|
||||
ushort nextId = _nextDictionaryId;
|
||||
if (nextId == 0) nextId = DictionaryPage.ReservedValuesEnd + 1; // Should be init, but safety
|
||||
|
||||
// Insert into Page
|
||||
// usage of default(ulong) or null transaction?
|
||||
// Dictionary updates should ideally be transactional or immediate?
|
||||
// "Immediate" for now to simplify, as dictionary is cross-collection.
|
||||
// If we use transaction, we need to pass it in. For now, immediate write.
|
||||
|
||||
// We need to support "Insert Global" which handles overflow.
|
||||
// DictionaryPage.Insert only handles single page.
|
||||
|
||||
// We need logic here to traverse chain and find space.
|
||||
if (InsertDictionaryEntryGlobal(key, nextId))
|
||||
{
|
||||
_dictionaryCache[key] = nextId;
|
||||
_dictionaryReverseCache[nextId] = key;
|
||||
_nextDictionaryId++;
|
||||
return nextId;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException("Failed to insert dictionary entry (Storage Full?)");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the dictionary key for an identifier.
|
||||
/// </summary>
|
||||
/// <param name="id">The dictionary identifier.</param>
|
||||
/// <returns>The dictionary key if found; otherwise, <see langword="null"/>.</returns>
|
||||
public string? GetDictionaryKey(ushort id)
|
||||
{
|
||||
if (_dictionaryReverseCache.TryGetValue(id, out var key))
|
||||
return key;
|
||||
return null;
|
||||
}
|
||||
|
||||
private bool InsertDictionaryEntryGlobal(string key, ushort value)
|
||||
{
|
||||
var pageId = _dictionaryRootPageId;
|
||||
var pageBuffer = new byte[PageSize];
|
||||
|
||||
while (true)
|
||||
{
|
||||
ReadPage(pageId, null, pageBuffer);
|
||||
|
||||
// Try Insert
|
||||
if (DictionaryPage.Insert(pageBuffer, key, value))
|
||||
{
|
||||
// Success - Write Back
|
||||
WritePageImmediate(pageId, pageBuffer);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Page Full - Check Next Page
|
||||
var header = PageHeader.ReadFrom(pageBuffer);
|
||||
if (header.NextPageId != 0)
|
||||
{
|
||||
pageId = header.NextPageId;
|
||||
continue;
|
||||
}
|
||||
|
||||
// No Next Page - Allocate New
|
||||
var newPageId = AllocatePage();
|
||||
var newPageBuffer = new byte[PageSize];
|
||||
DictionaryPage.Initialize(newPageBuffer, newPageId);
|
||||
|
||||
// Should likely insert into NEW page immediately to save I/O?
|
||||
// Or just link and loop?
|
||||
// Let's Insert into new page logic here to avoid re-reading.
|
||||
if (!DictionaryPage.Insert(newPageBuffer, key, value))
|
||||
return false; // Should not happen on empty page unless key is huge > page
|
||||
|
||||
// Write New Page
|
||||
WritePageImmediate(newPageId, newPageBuffer);
|
||||
|
||||
// Update Previous Page Link
|
||||
header.NextPageId = newPageId;
|
||||
header.WriteTo(pageBuffer);
|
||||
WritePageImmediate(pageId, pageBuffer);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a set of keys in the global dictionary.
|
||||
/// Ensures all keys are assigned an ID and persisted.
|
||||
/// </summary>
|
||||
/// <param name="keys">The keys to register.</param>
|
||||
public void RegisterKeys(IEnumerable<string> keys)
|
||||
{
|
||||
foreach (var key in keys)
|
||||
{
|
||||
GetOrAddDictionaryEntry(key.ToLowerInvariant());
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user