Skip to content

Commit 81c8ebb

Browse files
committed
[lmdb facebookincubator#3] Add LMDB storage backend
1 parent a35cc98 commit 81c8ebb

24 files changed

Lines changed: 15383 additions & 13 deletions

File tree

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ jobs:
169169
run: echo CABAL_CONFIG_FLAGS="$CABAL_CONFIG_FLAGS --index-state=${{ matrix.index-state }}" >>"$GITHUB_ENV"
170170

171171
- name: Install hsthrift and Glean dependencies
172-
run: apt-get install -y pkg-config rsync libgmock-dev libpcre3-dev libtinfo-dev libxxhash-dev
172+
run: apt-get install -y pkg-config rsync libgmock-dev libpcre3-dev libtinfo-dev libxxhash-dev squashfs-tools
173173

174174
- name: Build hsthrift and Glean
175175
run: make

glean.cabal.in

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -415,6 +415,22 @@ library rocksdb
415415
glean:storage,
416416
glean:rts,
417417

418+
library lmdb
419+
import: fb-haskell, fb-cpp, deps, folly
420+
visibility: private
421+
CXX_LIB_glean_cpp_lmdb
422+
include-dirs: .
423+
install-includes:
424+
glean/lmdb/container-impl.h
425+
glean/lmdb/database-impl.h
426+
glean/lmdb/ffi.h
427+
glean/lmdb/glean_lmdb.h
428+
glean/lmdb/util.h
429+
pkgconfig-depends: fmt
430+
build-depends:
431+
glean:storage,
432+
glean:rts,
433+
418434
library util
419435
import: fb-haskell, fb-cpp, deps, thrift-client
420436
visibility: public
@@ -599,6 +615,7 @@ library db
599615
Glean.Database.Storage.Memory
600616
Glean.Database.Storage.DB
601617
Glean.Database.Storage.RocksDB
618+
Glean.Database.Storage.LMDB
602619
Glean.Database.Trace
603620
Glean.Database.Types
604621
Glean.Database.Validate
@@ -653,6 +670,7 @@ library db
653670
glean:rts,
654671
glean:stubs,
655672
glean:rocksdb,
673+
glean:lmdb,
656674

657675
-- Backend API, and a few things built on top
658676
library backend-api

glean/db/Glean/Database/Config.hs

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ module Glean.Database.Config (
1515
tmpDataStore,
1616
memoryDataStore,
1717
rocksdbName,
18+
lmdbName,
1819

1920
-- * Config, and options parser
2021
options,
@@ -84,6 +85,7 @@ import Glean.Database.Schema.ComputeIds
8485
import Glean.Database.Storage
8586
import qualified Glean.Database.Storage.Memory as Memory
8687
import qualified Glean.Database.Storage.RocksDB as RocksDB
88+
import qualified Glean.Database.Storage.LMDB as LMDB
8789
import Glean.Database.Trace
8890
import qualified Glean.Internal.Types as Internal
8991
import Glean.Internal.Types (StorageName(..))
@@ -118,17 +120,20 @@ data DataStore = DataStore
118120
, dataStoreTag :: String
119121
}
120122

121-
rocksdbName, memoryName :: StorageName
123+
rocksdbName, lmdbName, memoryName :: StorageName
122124
rocksdbName = StorageName "rocksdb"
125+
lmdbName = StorageName "lmdb"
123126
memoryName = StorageName "memory"
124127

125128
fileDataStore :: FilePath -> DataStore
126129
fileDataStore path = DataStore
127130
{ withStorage = \scfg f -> do
128131
rocksdb <- RocksDB.newStorage path scfg
132+
lmdb <- LMDB.newStorage path scfg
129133
f (
130134
HashMap.fromList
131-
[ (rocksdbName, Some rocksdb)
135+
[ (rocksdbName, Some rocksdb),
136+
(lmdbName, Some lmdb)
132137
],
133138
Some (Catalog.fileCatalog path)
134139
)
@@ -511,7 +516,10 @@ options = do
511516
long "db-root" <>
512517
metavar "DIR" <>
513518
help "Directory containing databases")
514-
pure $ fileDataStore path
519+
lmdb <- switch (long "lmdb")
520+
pure $
521+
(if lmdb then \s -> s { defaultStorage = lmdbName } else id) $
522+
fileDataStore path
515523
dbTmp = tmpDataStore <$ flag' () (
516524
long "db-tmp" <>
517525
help "Store databases in a temporary directory (default)")
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
{-
2+
Copyright (c) Meta Platforms, Inc. and affiliates.
3+
All rights reserved.
4+
5+
This source code is licensed under the BSD-style license found in the
6+
LICENSE file in the root directory of this source tree.
7+
-}
8+
9+
module Glean.Database.Storage.LMDB
10+
( LMDB(..)
11+
, newStorage
12+
) where
13+
14+
import Control.Exception
15+
import Data.Int
16+
import Foreign.C.String
17+
import Foreign.C.Types
18+
import Foreign.ForeignPtr
19+
import Foreign.Ptr
20+
import System.Directory
21+
import System.IO.Temp (withTempDirectory)
22+
import System.FilePath
23+
import System.Process
24+
25+
import Util.FFI
26+
import Util.IO (safeRemovePathForcibly)
27+
28+
import Glean.Database.Backup.Backend (Data(Data))
29+
import Glean.Database.Repo (databasePath)
30+
import Glean.Database.Storage
31+
import Glean.Database.Storage.DB
32+
import Glean.FFI
33+
import Glean.RTS.Foreign.Lookup (CanLookup(..))
34+
import Glean.RTS.Foreign.Ownership as Ownership
35+
hiding (computeDerivedOwnership)
36+
import Glean.RTS.Types (Fid(..), invalidFid)
37+
import qualified Glean.ServerConfig.Types as ServerConfig
38+
import Glean.Types (Repo)
39+
import Glean.Util.Disk
40+
import Glean.Impl.MemoryReader
41+
42+
data LMDB = LMDB
43+
{ lmdbRoot :: FilePath
44+
, lmdbMaxDiskSize :: Maybe Int
45+
-- ^ virtual limit to report capped disk capacities. The limit is
46+
-- not enforced. It's up to each io usage to check diskspace before writing.
47+
-- We're using this to avoid serving too many dbs on query servers,
48+
-- and smarter sharding.
49+
}
50+
51+
newStorage :: FilePath -> ServerConfig.Config -> IO LMDB
52+
newStorage root ServerConfig.Config{..} = do
53+
mem_capacity <- totalMemCapacityKB
54+
return LMDB
55+
{ lmdbRoot = root
56+
, lmdbMaxDiskSize = case mem_capacity of
57+
Just mem -> (* (mem * 1024)) . fromIntegral <$>
58+
config_db_rocksdb_disk_mem_capacity_ratio_limit
59+
Nothing -> Nothing
60+
}
61+
62+
newtype instance Database LMDB = Database DB
63+
deriving (CanLookup)
64+
65+
instance Storage LMDB where
66+
describe db = "lmdb:" <> lmdbRoot db
67+
68+
open lmdb repo mode (DBVersion version) = do
69+
(cmode, start, ownership) <- case mode of
70+
ReadOnly -> return (0, invalidFid, Nothing)
71+
ReadWrite -> return (1, invalidFid, Nothing)
72+
Create start ownership _ -> do
73+
createDirectoryIfMissing True path
74+
return (2, start, ownership)
75+
withCString path $ \cpath ->
76+
using (invoke $ glean_lmdb_container_open cpath cmode)
77+
$ \container -> do
78+
fp <- mask_ $ do
79+
first_unit_id <- maybe (return firstUsetId) nextUsetId ownership
80+
p <- invoke $
81+
glean_lmdb_container_open_database container start
82+
first_unit_id version
83+
newForeignPtr glean_rocksdb_database_free p
84+
return (Database (DB (castForeignPtr fp) repo))
85+
where
86+
path = containerPath lmdb repo
87+
88+
delete lmdb = safeRemovePathForcibly . containerPath lmdb
89+
90+
safeRemoveForcibly lmdb =
91+
safeRemovePathForcibly . databasePath (lmdbRoot lmdb)
92+
93+
getTotalCapacity lmdb = do
94+
exists <- doesDirectoryExist (lmdbRoot lmdb)
95+
if exists
96+
then do
97+
fullDiskCapacity <- getDiskSize (lmdbRoot lmdb)
98+
return $ Just $ case lmdbMaxDiskSize lmdb of
99+
Just maxDiskSize -> min maxDiskSize fullDiskCapacity
100+
Nothing -> fullDiskCapacity
101+
else return Nothing
102+
103+
getUsedCapacity lmdb = do
104+
exists <- doesDirectoryExist (lmdbRoot lmdb)
105+
if exists
106+
then Just <$> getUsedDiskSpace (lmdbRoot lmdb)
107+
else return Nothing
108+
109+
getFreeCapacity lmdb = do
110+
used <- getUsedCapacity lmdb
111+
total <- getTotalCapacity lmdb
112+
case (used,total) of
113+
(Just used, Just total) -> return $ total - used
114+
_ -> getFreeDiskSpace (lmdbRoot lmdb) -- not aware of disk limit
115+
116+
withScratchRoot rocks f = f $ lmdbRoot rocks </> ".scratch"
117+
118+
restore lmdb repo scratch scratch_file = do
119+
withTempDirectory scratch "restore" $ \scratch_restore -> do
120+
let db = scratch_restore </> "db"
121+
createDirectoryIfMissing True db
122+
callProcess "unsquashfs" ["-d", db, scratch_file ]
123+
-- to avoid retaining an extra copy of the DB during restore,
124+
-- delete the input file now.
125+
let target = containerPath lmdb repo
126+
createDirectoryIfMissing True $ takeDirectory target
127+
renameDirectory db target
128+
129+
130+
containerPath :: LMDB -> Repo -> FilePath
131+
containerPath LMDB{..} repo = databasePath lmdbRoot repo </> "db"
132+
133+
instance DatabaseOps (Database LMDB) where
134+
close (Database db) = close db
135+
predicateStats (Database db) = predicateStats db
136+
store (Database db) = store db
137+
retrieve (Database db) = retrieve db
138+
commit (Database db) = commit db
139+
addOwnership (Database db) = addOwnership db
140+
optimize (Database db) = optimize db
141+
computeOwnership (Database db) = computeOwnership db
142+
storeOwnership (Database db) = storeOwnership db
143+
getOwnership (Database db) = getOwnership db
144+
getUnitId (Database db) = getUnitId db
145+
getUnit (Database db) = getUnit db
146+
addDefineOwnership (Database db) = addDefineOwnership db
147+
computeDerivedOwnership (Database db) = computeDerivedOwnership db
148+
cacheOwnership (Database db) = cacheOwnership db
149+
prepareFactOwnerCache (Database db) = prepareFactOwnerCache db
150+
151+
backup (Database db) scratch process = do
152+
createDirectoryIfMissing True (scratch </> "backup")
153+
backup db scratch $ \_ _ -> do
154+
withTempDirectory scratch "out" $ \tmpdir -> do
155+
let out = tmpdir </> "db.squashfs"
156+
callProcess "mksquashfs" [
157+
scratch </> "backup", out,
158+
"-comp", "zstd", "-Xcompression-level", "8" ]
159+
size <- getFileSize out
160+
process out (Data $ fromIntegral size)
161+
162+
foreign import ccall safe glean_lmdb_container_open
163+
:: CString
164+
-> CInt
165+
-> Ptr Container
166+
-> IO CString
167+
168+
foreign import ccall safe glean_lmdb_container_open_database
169+
:: Container
170+
-> Fid
171+
-> UsetId
172+
-> Int64
173+
-> Ptr (Ptr (Database LMDB))
174+
-> IO CString
175+
foreign import ccall safe "&glean_rocksdb_database_free"
176+
glean_rocksdb_database_free :: Destroy (Database LMDB)

0 commit comments

Comments
 (0)