Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions lsm-tree/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Revision history for `lsm-tree`

## next version

### Non-breaking changes

* Copy the salt when exporting a snapshot as `<exportedSnap>/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
Expand Down
8 changes: 8 additions & 0 deletions lsm-tree/src-core/Database/LSMTree/Internal/Paths.hs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ module Database.LSMTree.Internal.Paths (
, snapshotMetaDataFile
, SnapshotMetaDataChecksumFile (..)
, snapshotMetaDataChecksumFile
, exportedSnapshotSaltFile
-- * Table paths
, tableBlobPath
-- * Run paths
Expand Down Expand Up @@ -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
-------------------------------------------------------------------------------}
Expand Down
49 changes: 49 additions & 0 deletions lsm-tree/src-core/Database/LSMTree/Internal/Unsafe.hs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ module Database.LSMTree.Internal.Unsafe (
, listSnapshots
, importSnapshot
, exportSnapshot
, newSessionImportingSnapshot
-- * Multiple writable tables
, duplicate
-- * Table union
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
65 changes: 65 additions & 0 deletions lsm-tree/src/Database/LSMTree.hs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ module Database.LSMTree (
withRestoreSession,
openSession,
newSession,
newSessionImportingSnapshot,
restoreSession,
closeSession,

Expand Down Expand Up @@ -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.

Expand Down
Loading