using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Numerics; using System.Text; using FASTER.core; using LightningDB; namespace PerfectNumbers.Core.Persistence; /// /// Stores last-checked k values (BigInteger) per prime using Microsoft.FASTER.Core. /// Supports one-time migration from an existing LightningDB (LMDB) store. /// public sealed class KStateRepository : IDisposable { private readonly FasterKV _store; private readonly IDevice _logDevice; private readonly IDevice _objectLogDevice; private static readonly Encoding Utf8 = Encoding.UTF8; public string DatabasePath { get; } public static KStateRepository Open( string directory, string repositoryFileName = "kstate.faster", string? lmdbPath = null, string? lmdbDirectory = null, string lmdbDatabaseName = "kstate", bool deleteLmdbAfterMigration = true) { Directory.CreateDirectory(directory); string dbPath = Path.Combine(directory, repositoryFileName); var repo = new KStateRepository(dbPath); string lmdbSource = lmdbPath ?? Path.ChangeExtension(dbPath, ".lmdb"); if (!File.Exists(lmdbSource) && !Directory.Exists(lmdbSource) && !string.IsNullOrEmpty(lmdbDirectory)) { lmdbSource = Path.Combine(lmdbDirectory, Path.GetFileName(Path.ChangeExtension(repositoryFileName, ".lmdb"))); } repo.MigrateFromLmdb(lmdbSource, lmdbDatabaseName, deleteLmdbAfterMigration); return repo; } private KStateRepository(string path) { DatabasePath = path; _logDevice = Devices.CreateLogDevice(path + ".log"); _objectLogDevice = Devices.CreateLogDevice(path + ".obj.log"); _store = new FasterKV( 1L << 20, new LogSettings { LogDevice = _logDevice, ObjectLogDevice = _objectLogDevice, }); } public void Upsert(ulong prime, BigInteger k) { using var session = _store.For(new SimpleFunctions()).NewSession>(); string keyString = prime.ToString(CultureInfo.InvariantCulture); string valueString = k.ToString(CultureInfo.InvariantCulture); session.Upsert(keyString, valueString); session.CompletePending(true); } public bool TryGet(ulong prime, out BigInteger value) { using var session = _store.For(new SimpleFunctions()).NewSession>(); string keyString = prime.ToString(CultureInfo.InvariantCulture); if (session.Read(keyString, out string? stored).Found && stored is not null && BigInteger.TryParse(stored, NumberStyles.Integer, CultureInfo.InvariantCulture, out value)) { return true; } value = BigInteger.Zero; return false; } public void SeedFromStates(IEnumerable<(ulong Prime, BigInteger K)> states) { foreach (var (prime, k) in states) { Upsert(prime, k); } } public void Commit() { _store.Log.FlushAndEvict(wait: true); } public void Dispose() { _store.Dispose(); _logDevice.Dispose(); _objectLogDevice.Dispose(); } private void MigrateFromLmdb(string lmdbPath, string databaseName, bool deleteAfter) { try { if (!LmdbExists(lmdbPath)) { Console.WriteLine($"No LMDB k-state found at '{lmdbPath}', skipping migration."); return; } Console.WriteLine($"Migrating k-state from LMDB at '{lmdbPath}' to FASTER..."); using var env = new LightningEnvironment(lmdbPath) { MaxDatabases = 2 }; env.Open(); using var tx = env.BeginTransaction(TransactionBeginFlags.ReadOnly); var db = tx.OpenDatabase(databaseName, new DatabaseConfiguration { Flags = DatabaseOpenFlags.None }); using var cursor = tx.CreateCursor(db); foreach (var entry in cursor.AsEnumerable()) { string keyString = Utf8.GetString(entry.Item1.CopyToNewArray()); string valueString = Utf8.GetString(entry.Item2.CopyToNewArray()); if (ulong.TryParse(keyString, NumberStyles.None, CultureInfo.InvariantCulture, out ulong prime) && BigInteger.TryParse(valueString, NumberStyles.Integer, CultureInfo.InvariantCulture, out BigInteger k) && k > BigInteger.Zero) { Upsert(prime, k); } } _store.Log.FlushAndEvict(wait: true); Console.WriteLine("Migration of k-state to FASTER completed."); } catch { // Ignore migration failures; repo will simply start empty. } if (deleteAfter) { Console.WriteLine($"Starting cleanup of LMDB k-state at '{lmdbPath}'..."); try { if (Directory.Exists(lmdbPath)) { Directory.Delete(lmdbPath, recursive: true); } else if (File.Exists(lmdbPath)) { File.Delete(lmdbPath); } } catch { // ignore cleanup failures } } } private static bool LmdbExists(string path) { if (Directory.Exists(path) || File.Exists(path)) { return true; } string candidate = Path.Combine(path, "data.mdb"); if (File.Exists(candidate)) { return true; } string? directory = Path.GetDirectoryName(path); if (!string.IsNullOrEmpty(directory) && Directory.Exists(directory)) { string sibling = Path.Combine(directory, "data.mdb"); if (File.Exists(sibling)) { return true; } } return false; } }