diff --git a/cs/src/core/Device/LocalStorageDevice.cs b/cs/src/core/Device/LocalStorageDevice.cs index 3974f74fc..7d3ef2d92 100644 --- a/cs/src/core/Device/LocalStorageDevice.cs +++ b/cs/src/core/Device/LocalStorageDevice.cs @@ -36,14 +36,36 @@ public LocalStorageDevice(string filename, bool disableFileBuffering = true, long capacity = Devices.CAPACITY_UNSPECIFIED, bool recoverDevice = false) + : this(filename, preallocateFile, deleteOnClose, disableFileBuffering, capacity, recoverDevice, initialLogFileHandles: null) + { + } + + /// + /// Constructor with more options for derived classes + /// + /// File name (or prefix) with path + /// + /// + /// + /// The maximum number of bytes this storage device can accommondate, or CAPACITY_UNSPECIFIED if there is no such limit + /// Whether to recover device metadata from existing files + /// Optional set of preloaded safe file handles, which can speed up hydration of preexisting log file handles + protected internal LocalStorageDevice(string filename, + bool preallocateFile = false, + bool deleteOnClose = false, + bool disableFileBuffering = true, + long capacity = Devices.CAPACITY_UNSPECIFIED, + bool recoverDevice = false, + IEnumerable> initialLogFileHandles = null) : base(filename, GetSectorSize(filename), capacity) - { Native32.EnableProcessPrivileges(); this.preallocateFile = preallocateFile; this.deleteOnClose = deleteOnClose; this.disableFileBuffering = disableFileBuffering; - logHandles = new SafeConcurrentDictionary(); + logHandles = initialLogFileHandles != null + ? new SafeConcurrentDictionary(initialLogFileHandles) + : new SafeConcurrentDictionary(); if (recoverDevice) RecoverFiles(); } @@ -217,48 +239,16 @@ public override void Close() } /// - /// + /// Creates a SafeFileHandle for the specified segment. This can be used by derived classes to prepopulate logHandles in the constructor. /// - /// - /// - protected string GetSegmentName(int segmentId) - { - return FileName + "." + segmentId; - } - - /// - /// - /// - /// - /// - // Can be used to pre-load handles, e.g., after a checkpoint - protected SafeFileHandle GetOrAddHandle(int _segmentId) - { - return logHandles.GetOrAdd(_segmentId, segmentId => CreateHandle(segmentId)); - } - - private static uint GetSectorSize(string filename) - { - if (!Native32.GetDiskFreeSpace(filename.Substring(0, 3), - out uint lpSectorsPerCluster, - out uint _sectorSize, - out uint lpNumberOfFreeClusters, - out uint lpTotalNumberOfClusters)) - { - Debug.WriteLine("Unable to retrieve information for disk " + filename.Substring(0, 3) + " - check if the disk is available and you have specified the full path with drive name. Assuming sector size of 512 bytes."); - _sectorSize = 512; - } - return _sectorSize; - } - - private SafeFileHandle CreateHandle(int segmentId) + protected internal static SafeFileHandle CreateHandle(int segmentId, bool disableFileBuffering, bool deleteOnClose, bool preallocateFile, long segmentSize, string fileName) { uint fileAccess = Native32.GENERIC_READ | Native32.GENERIC_WRITE; uint fileShare = unchecked(((uint)FileShare.ReadWrite & ~(uint)FileShare.Inheritable)); uint fileCreation = unchecked((uint)FileMode.OpenOrCreate); uint fileFlags = Native32.FILE_FLAG_OVERLAPPED; - if (this.disableFileBuffering) + if (disableFileBuffering) { fileFlags = fileFlags | Native32.FILE_FLAG_NO_BUFFERING; } @@ -273,7 +263,7 @@ private SafeFileHandle CreateHandle(int segmentId) } var logHandle = Native32.CreateFileW( - GetSegmentName(segmentId), + GetSegmentName(fileName, segmentId), fileAccess, fileShare, IntPtr.Zero, fileCreation, fileFlags, IntPtr.Zero); @@ -281,11 +271,11 @@ private SafeFileHandle CreateHandle(int segmentId) if (logHandle.IsInvalid) { var error = Marshal.GetLastWin32Error(); - throw new IOException($"Error creating log file for {GetSegmentName(segmentId)}, error: {error}", Native32.MakeHRFromErrorCode(error)); + throw new IOException($"Error creating log file for {GetSegmentName(fileName, segmentId)}, error: {error}", Native32.MakeHRFromErrorCode(error)); } if (preallocateFile && segmentSize != -1) - SetFileSize(FileName, logHandle, segmentSize); + SetFileSize(fileName, logHandle, segmentSize); try { @@ -293,16 +283,59 @@ private SafeFileHandle CreateHandle(int segmentId) } catch (Exception e) { - throw new FasterException("Error binding log handle for " + GetSegmentName(segmentId) + ": " + e.ToString()); + throw new FasterException("Error binding log handle for " + GetSegmentName(fileName, segmentId) + ": " + e.ToString()); } return logHandle; } + /// + /// Static method to construct segment name + /// + protected static string GetSegmentName(string fileName, int segmentId) + { + return fileName + "." + segmentId; + } + + /// + /// + /// + /// + /// + protected string GetSegmentName(int segmentId) => GetSegmentName(FileName, segmentId); + + /// + /// + /// + /// + /// + // Can be used to pre-load handles, e.g., after a checkpoint + protected SafeFileHandle GetOrAddHandle(int _segmentId) + { + return logHandles.GetOrAdd(_segmentId, segmentId => CreateHandle(segmentId)); + } + + private SafeFileHandle CreateHandle(int segmentId) + => CreateHandle(segmentId, this.disableFileBuffering, this.deleteOnClose, this.preallocateFile, this.segmentSize, this.FileName); + + private static uint GetSectorSize(string filename) + { + if (!Native32.GetDiskFreeSpace(filename.Substring(0, 3), + out uint lpSectorsPerCluster, + out uint _sectorSize, + out uint lpNumberOfFreeClusters, + out uint lpTotalNumberOfClusters)) + { + Debug.WriteLine("Unable to retrieve information for disk " + filename.Substring(0, 3) + " - check if the disk is available and you have specified the full path with drive name. Assuming sector size of 512 bytes."); + _sectorSize = 512; + } + return _sectorSize; + } + /// Sets file size to the specified value. /// Does not reset file seek pointer to original location. - private bool SetFileSize(string filename, SafeFileHandle logHandle, long size) + private static bool SetFileSize(string filename, SafeFileHandle logHandle, long size) { - if (segmentSize <= 0) + if (size <= 0) return false; if (Native32.EnableVolumePrivileges(filename, logHandle)) diff --git a/cs/src/core/Utilities/SafeConcurrentDictionary.cs b/cs/src/core/Utilities/SafeConcurrentDictionary.cs index e9513af5d..38aefddfb 100644 --- a/cs/src/core/Utilities/SafeConcurrentDictionary.cs +++ b/cs/src/core/Utilities/SafeConcurrentDictionary.cs @@ -18,10 +18,20 @@ namespace FASTER.core /// Type of values in the dictionary internal sealed class SafeConcurrentDictionary : IEnumerable> { - private readonly ConcurrentDictionary dictionary = new ConcurrentDictionary(); + private readonly ConcurrentDictionary dictionary; private readonly ConcurrentDictionary keyLocks = new ConcurrentDictionary(); + public SafeConcurrentDictionary() + { + this.dictionary = new ConcurrentDictionary(); + } + + public SafeConcurrentDictionary(IEnumerable> initialCollection) + { + this.dictionary = new ConcurrentDictionary(initialCollection); + } + /// /// Returns the count of the dictionary. /// diff --git a/cs/test/SharedDirectoryTests.cs b/cs/test/SharedDirectoryTests.cs index 3fef94120..ade30b815 100644 --- a/cs/test/SharedDirectoryTests.cs +++ b/cs/test/SharedDirectoryTests.cs @@ -2,9 +2,11 @@ // Licensed under the MIT license. using FASTER.core; +using Microsoft.Win32.SafeHandles; using NUnit.Framework; using NUnit.Framework.Internal; using System; +using System.Collections.Generic; using System.IO; using System.Linq; @@ -68,7 +70,7 @@ public void SharedLogDirectory() CopyDirectory(new DirectoryInfo(this.original.CheckpointDirectory), new DirectoryInfo(cloneCheckpointDirectory)); // Recover from original checkpoint - this.clone.Initialize(cloneCheckpointDirectory, this.sharedLogDirectory); + this.clone.Initialize(cloneCheckpointDirectory, this.sharedLogDirectory, populateLogHandles: true); this.clone.Faster.Recover(checkpointGuid); // Both sessions should work concurrently @@ -95,12 +97,32 @@ private struct FasterTestInstance public FasterKV Faster { get; private set; } public IDevice LogDevice { get; private set; } - public void Initialize(string checkpointDirectory, string logDirectory) + public void Initialize(string checkpointDirectory, string logDirectory, bool populateLogHandles = false) { this.CheckpointDirectory = checkpointDirectory; this.LogDirectory = logDirectory; - this.LogDevice = Devices.CreateLogDevice($"{this.LogDirectory}\\log", deleteOnClose: true); + string logFileName = "log"; + string deviceFileName = $"{this.LogDirectory}\\{logFileName}"; + KeyValuePair[] initialHandles = null; + if (populateLogHandles) + { + var segmentIds = new List(); + foreach (FileInfo item in new DirectoryInfo(logDirectory).GetFiles(logFileName + "*")) + { + segmentIds.Add(int.Parse(item.Name.Replace(logFileName, "").Replace(".", ""))); + } + segmentIds.Sort(); + initialHandles = new KeyValuePair[segmentIds.Count]; + for (int i = 0; i < segmentIds.Count; i++) + { + var segmentId = segmentIds[i]; + var handle = LocalStorageDevice.CreateHandle(segmentId, disableFileBuffering: false, deleteOnClose: true, preallocateFile: false, segmentSize: -1, fileName: deviceFileName); + initialHandles[i] = new KeyValuePair(segmentId, handle); + } + } + + this.LogDevice = new LocalStorageDevice(deviceFileName, deleteOnClose: true, disableFileBuffering: false, initialLogFileHandles: initialHandles); this.Faster = new FasterKV( keySpace, new Functions(),