glean-hs-0.1.0: haskell/src/Glean/RocksDB.hs
-- | RocksDB storage backend for glean-hs.
--
-- Implements the 'Storage' typeclass using our Rust substrate
-- via the FFI bindings in "Glean.FFI".
--
-- This module replaces Meta Glean's RocksDB.hs, which depends on
-- Meta-internal packages (Util.FFI, Util.Log, ServiceData).
-- Our implementation has zero Meta internal dependencies.
--
-- Usage:
-- @
-- import Glean.RocksDB (RocksDB)
-- import Glean.Storage
--
-- withStorage (defaultDbConfig "\/tmp\/mydb") $ \(db :: RocksDB) -> do
-- store db myBatch
-- facts <- retrieve db
-- @
module Glean.RocksDB
( RocksDB
, rocksDbOpen
, rocksDbClose
) where
import Control.Exception (throwIO, catch, SomeException)
import qualified Data.ByteString as BS
import Data.IORef
import qualified Data.Map.Strict as Map
import qualified Data.Text as Text
import Glean.FFI
import Glean.Storage
-- ── RocksDB handle ────────────────────────────────────────────────────────────
-- | A RocksDB-backed Glean database.
-- Wraps the Rust substrate via FFI.
data RocksDB = RocksDB
{ rocksContainer :: !GleanContainer
-- ^ The RocksDB container (database files on disk).
, rocksDatabase :: !GleanDatabase
-- ^ The logical Glean database within the container.
, rocksConfig :: !DbConfig
-- ^ Configuration used to open this database.
, rocksProps :: !(IORef DbProperties)
-- ^ Mutable database properties (fact count etc.).
, rocksClosed :: !(IORef Bool)
-- ^ True if this database has been closed.
}
-- ── Smart constructors ────────────────────────────────────────────────────────
-- | Open a RocksDB database directly.
-- Prefer 'withStorage' or 'open' (the Storage instance method).
rocksDbOpen :: DbConfig -> IO RocksDB
rocksDbOpen config = do
-- Allocate block cache
cache <- newCache (dbCacheSize config)
-- Determine open mode
let mode
| dbReadOnly config = ReadOnly
| dbCreate config = Create
| otherwise = ReadWrite
-- Open the container (RocksDB instance)
container <- openContainer (dbPath config) mode (Just cache)
`catch` \(e :: SomeException) ->
throwIO $ StorageOpenFailed
(Text.pack $ "Failed to open container: " ++ show e)
-- Open the logical database within the container
db <- openDatabase
container
(dbStartId config)
1 -- first_unit_id (default)
(dbVersion config)
`catch` \(e :: SomeException) ->
throwIO $ StorageOpenFailed
(Text.pack $ "Failed to open database: " ++ show e)
-- Read persisted metadata from RocksDB
factCount <- Glean.FFI.getMeta db "meta:fact_count"
_batchCount <- Glean.FFI.getMeta db "meta:batch_count"
-- Initialize mutable state from persisted values
propsRef <- newIORef $ DbProperties
{ propVersion = dbVersion config
, propFirstId = dbStartId config
, propFirstFreeId = dbStartId config + factCount
, propFactCount = fromIntegral factCount
}
closedRef <- newIORef False
return RocksDB
{ rocksContainer = container
, rocksDatabase = db
, rocksConfig = config
, rocksProps = propsRef
, rocksClosed = closedRef
}
-- | Close a RocksDB database.
rocksDbClose :: RocksDB -> IO ()
rocksDbClose rdb = do
closed <- readIORef (rocksClosed rdb)
if closed
then return () -- idempotent close
else do
writeIORef (rocksClosed rdb) True
freeDatabase (rocksDatabase rdb)
-- Container is freed by GC via ForeignPtr finalizer
-- ── Storage instance ──────────────────────────────────────────────────────────
instance Storage RocksDB where
open = rocksDbOpen
close = rocksDbClose
store rdb batch = do
checkNotClosed rdb
let bytes = batchData batch
if BS.null bytes
then return () -- nothing to store
else do
Glean.FFI.store (rocksDatabase rdb) bytes
(fromIntegral (batchCount batch))
`catch` \(e :: SomeException) ->
throwIO $ StorageWriteFailed
(Text.pack $ "store failed: " ++ show e)
-- Update properties
modifyIORef' (rocksProps rdb) $ \props -> props
{ propFirstFreeId = propFirstFreeId props
+ fromIntegral (batchCount batch)
, propFactCount = propFactCount props + batchCount batch
}
retrieve rdb = do
checkNotClosed rdb
result <- Glean.FFI.retrieve (rocksDatabase rdb)
`catch` \(e :: SomeException) ->
throwIO $ StorageReadFailed
(Text.pack $ "retrieve failed: " ++ show e)
case result of
Nothing -> return Nothing
Just bytes -> do
props <- readIORef (rocksProps rdb)
return $ Just $ FactBatch
{ batchData = bytes
, batchFirstId = propFirstId props
, batchCount = propFactCount props
, batchPredicates = Map.empty -- populated by indexer
}
commit rdb = do
checkNotClosed rdb
-- RocksDB auto-commits writes
-- Explicit flush for durability guarantee
flush rdb
flush rdb = do
checkNotClosed rdb
-- Flush is handled at the RocksDB level
-- No direct FFI call needed for basic operation
return ()
optimize rdb = do
checkNotClosed rdb
-- RocksDB compaction — deferred to Phase 10 (cache/optimize)
return ()
predicateStats rdb = do
checkNotClosed rdb
props <- readIORef (rocksProps rdb)
-- Detailed per-predicate stats require index scan
-- Basic implementation: return aggregate stats
return $ Map.singleton 0 $ PredicateStats
{ statsCount = propFactCount props
, statsFirstId = propFirstId props
, statsLastId = propFirstFreeId props
}
properties rdb = do
checkNotClosed rdb
readIORef (rocksProps rdb)
backup rdb targetPath = do
checkNotClosed rdb
Glean.FFI.restore targetPath (dbPath (rocksConfig rdb))
`catch` \(e :: SomeException) ->
throwIO $ StorageWriteFailed
(Text.pack $ "backup failed: " ++ show e)
-- ── Internal helpers ──────────────────────────────────────────────────────────
-- | Throw if the database has been closed.
checkNotClosed :: RocksDB -> IO ()
checkNotClosed rdb = do
closed <- readIORef (rocksClosed rdb)
if closed
then throwIO $ StorageCloseFailed
(Text.pack "Operation on closed database")
else return ()