diff --git a/lsm-tree/CHANGELOG.md b/lsm-tree/CHANGELOG.md index 9176c7920..6ea0b09e9 100644 --- a/lsm-tree/CHANGELOG.md +++ b/lsm-tree/CHANGELOG.md @@ -1,5 +1,13 @@ # Revision history for `lsm-tree` +## next version + +### Non-breaking changes + +* Copy the salt when exporting a snapshot as `/salt`. +* Expose new function `newSessionImportingSnapshot` that reads the salt from the + exported snapshot when creating a new session. + ## 1.1.0.0 -- 2026-05-13 ### Breaking changes diff --git a/lsm-tree/src-core/Database/LSMTree/Internal/Paths.hs b/lsm-tree/src-core/Database/LSMTree/Internal/Paths.hs index 87c8439c9..0e76d3089 100644 --- a/lsm-tree/src-core/Database/LSMTree/Internal/Paths.hs +++ b/lsm-tree/src-core/Database/LSMTree/Internal/Paths.hs @@ -20,6 +20,7 @@ module Database.LSMTree.Internal.Paths ( , snapshotMetaDataFile , SnapshotMetaDataChecksumFile (..) , snapshotMetaDataChecksumFile + , exportedSnapshotSaltFile -- * Table paths , tableBlobPath -- * Run paths @@ -208,6 +209,13 @@ snapshotMetaDataChecksumFile :: NamedSnapshotDir -> SnapshotMetaDataChecksumFile snapshotMetaDataChecksumFile (NamedSnapshotDir dir) = SnapshotMetaDataChecksumFile (dir mkFsPath ["metadata.checksum"]) +-- | The file holding the session salt that is copied alongside an /exported/ +-- snapshot, given the path to the exported snapshot directory. +-- +-- See 'Database.LSMTree.Internal.Unsafe.exportSnapshot'. +exportedSnapshotSaltFile :: FsPath -> FsPath +exportedSnapshotSaltFile dir = dir mkFsPath ["salt"] + {------------------------------------------------------------------------------- Table paths -------------------------------------------------------------------------------} diff --git a/lsm-tree/src-core/Database/LSMTree/Internal/Unsafe.hs b/lsm-tree/src-core/Database/LSMTree/Internal/Unsafe.hs index d0b3d1f8d..d856ce364 100644 --- a/lsm-tree/src-core/Database/LSMTree/Internal/Unsafe.hs +++ b/lsm-tree/src-core/Database/LSMTree/Internal/Unsafe.hs @@ -84,6 +84,7 @@ module Database.LSMTree.Internal.Unsafe ( , listSnapshots , importSnapshot , exportSnapshot + , newSessionImportingSnapshot -- * Multiple writable tables , duplicate -- * Table union @@ -1966,6 +1967,48 @@ importSnapshot sesh snap sourcePath = do delayedCommit reg $ traceWith sesh.sessionTracer $ TraceImportedSnapshot snap +{-# SPECIALISE newSessionImportingSnapshot :: + Tracer IO LSMTreeTrace + -> HasFS IO h + -> HasBlockIO IO h + -> FsPath + -> SnapshotName + -> FsPath + -> IO (Session IO h) #-} +-- | Create a new session and import a snapshot into it. Unlike 'newSession', this +-- does not take a 'Bloom.Salt': instead, the salt is read from the @salt@ file +-- that 'exportSnapshot' wrote alongside the exported snapshot, so that the +-- imported snapshot's Bloom filters are validated against the salt they were +-- built with. +newSessionImportingSnapshot :: + forall m h. + (MonadSTM m, MonadMVar m, PrimMonad m, MonadMask m, MonadEvaluate m) + => Tracer m LSMTreeTrace + -> HasFS m h + -> HasBlockIO m h + -> FsPath -- ^ Path to the (new) session directory + -> SnapshotName -- ^ The name to import the snapshot as + -> FsPath -- ^ Path to the exported snapshot directory + -> m (Session m h) +newSessionImportingSnapshot tr hfs hbio dir snap sourcePath = do + -- Read the salt that 'exportSnapshot' copied alongside the snapshot. + sourceExists <- FS.doesDirectoryExist hfs sourcePath + unless sourceExists $ throwIO (SnapshotImportDirDoesNotExistError sourcePath) + let saltPath = Paths.exportedSnapshotSaltFile sourcePath + saltExists <- FS.doesFileExist hfs saltPath + unless saltExists $ throwIO (SnapshotImportDirDoesNotExistError sourcePath) + salt <- + FS.withFile hfs saltPath FS.ReadMode $ \h -> do + bs <- FS.hGetAll hfs h + evaluate $ S.deserialise bs + + -- Create the new session with that salt, then import the snapshot into it. + -- If importing fails, close the freshly-created session before re-throwing. + sesh <- newSession tr hfs hbio salt dir + flip onException (closeSession sesh) $ + importSnapshot sesh snap sourcePath + pure sesh + -- | A snapshot was intended to be exported, but the destination directory -- already exists. data SnapshotExportDirExistsError @@ -2009,6 +2052,12 @@ exportSnapshot sesh snap destinationPath = do -- Create hard links for all files in the destination directory FS.hardLinkDirectoryRecursive hfs hbio reg sourcePath destinationPath + -- Copy the session's salt (stored in the session metadata file) into + -- the exported snapshot, so that the snapshot is self-contained. + let saltSource = Paths.metadataFile (sessionRoot seshEnv) + saltDest = Paths.exportedSnapshotSaltFile destinationPath + FS.copyFile hfs reg saltSource saltDest + -- Make the directory and its contents durable. FS.synchroniseDirectoryRecursive hfs hbio destinationPath diff --git a/lsm-tree/src/Database/LSMTree.hs b/lsm-tree/src/Database/LSMTree.hs index fba7f1dc4..d21a9c81a 100644 --- a/lsm-tree/src/Database/LSMTree.hs +++ b/lsm-tree/src/Database/LSMTree.hs @@ -25,6 +25,7 @@ module Database.LSMTree ( withRestoreSession, openSession, newSession, + newSessionImportingSnapshot, restoreSession, closeSession, @@ -760,6 +761,70 @@ restoreSession :: restoreSession tracer hasFS hasBlockIO sessionDir = Session <$> Internal.restoreSession tracer hasFS hasBlockIO sessionDir +{- | +Create a new session and import a snapshot into it. + +The session directory must be empty. + +Unlike 'newSession', this does not take a 'Salt'. Instead, the salt is read from +the @salt@ file that 'exportSnapshot' wrote alongside the exported snapshot, so +that the imported snapshot's Bloom filters are validated against the salt they +were originally built with. This is equivalent to calling 'newSession' followed +by 'importSnapshot', but it sources the salt from the export rather than +requiring the caller to supply it. + +The 'FsPath' to the exported snapshot directory is a relative path that is +interpreted relative to a /root/. What the root is depends on which function was +used to create the session. See 'withOpenSession', 'withOpenSessionIO', and +'withOpenMountedSessionIO' for more information about the root. + +The worst-case disk I\/O complexity of this operation depends on the merge policy +of the imported table: + +['LazyLevelling']: + \(O(T \log_T \frac{n}{B})\). + +__Warning:__ Sessions hold open resources and must be closed using 'closeSession'. + +Throws the following exceptions: + +['SessionDirDoesNotExistError']: + If the session directory does not exist. +['SessionDirLockedError']: + If the session directory is locked by another process. +['SessionDirCorruptedError']: + If the session directory is malformed. +['SnapshotImportDirDoesNotExistError']: + If the source directory for the to-be-imported snapshot does not exist, or + does not contain the exported salt. +-} +{-# SPECIALISE + newSessionImportingSnapshot :: + Tracer IO LSMTreeTrace -> + HasFS IO HandleIO -> + HasBlockIO IO HandleIO -> + FsPath -> + SnapshotName -> + FsPath -> + IO (Session IO) + #-} +newSessionImportingSnapshot :: + forall m h. + (IOLike m, Typeable h) => + Tracer m LSMTreeTrace -> + HasFS m h -> + HasBlockIO m h -> + -- | The session directory. + FsPath -> + -- | The name to import the snapshot as. + SnapshotName -> + -- | The exported snapshot directory. + FsPath -> + m (Session m) +newSessionImportingSnapshot tracer hasFS hasBlockIO sessionDir snap sourceDir = + Session <$> + Internal.newSessionImportingSnapshot tracer hasFS hasBlockIO sessionDir snap sourceDir + {- | Close a session.