220 lines
7.9 KiB
C#
Executable File
220 lines
7.9 KiB
C#
Executable File
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());
|
|
}
|
|
}
|
|
}
|