module Database.VCache.Open
( openVCache
) where
import Control.Monad
import Control.Exception
import System.FileLock (FileLock)
import qualified System.FileLock as FileLock
import qualified System.EasyFile as EasyFile
import qualified System.IO.Error as IOE
import Foreign.Storable
import Foreign.Marshal.Alloc
import Foreign.Ptr
import Data.Bits
import Data.IORef
import Data.Maybe
import qualified Data.Map.Strict as Map
import qualified Data.ByteString as BS
import qualified Data.List as L
import Control.Concurrent.MVar
import Control.Concurrent.STM.TVar
import Control.Concurrent
import qualified System.IO as Sys
import qualified System.Exit as Sys
import Database.LMDB.Raw
import Database.VCache.Types
import Database.VCache.RWLock
import Database.VCache.Aligned
import Database.VCache.Write
import Database.VCache.Clean
openVCache :: Int -> FilePath -> IO VCache
openVCache nMB fp = do
let (fdir,fn) = EasyFile.splitFileName fp
let eBadFile = fp ++ " not recognized as a file name"
when (L.null fn) (fail $ "openVCache: " ++ eBadFile)
EasyFile.createDirectoryIfMissing True fdir
let fpLock = fp ++ "-lock"
let nBytes = (max 1 nMB) * 1024 * 1024
mbLock <- FileLock.tryLockFile fpLock FileLock.Exclusive
case mbLock of
Nothing -> ioError $ IOE.mkIOError
IOE.alreadyInUseErrorType
"openVCache lockfile"
Nothing (Just fpLock)
Just fl -> openVC' nBytes fl fp
`onException` FileLock.unlockFile fl
vcFlags :: [MDB_EnvFlag]
vcFlags = [MDB_NOSUBDIR
,MDB_NOLOCK
]
vcRootPath :: BS.ByteString
vcRootPath = BS.singleton 47
vcAllocStart :: Address
vcAllocStart = 999999999
vcDefaultCacheLimit :: Int
vcDefaultCacheLimit = 10 * 1000 * 1000
vcInitCacheSizeEst :: CacheSizeEst
vcInitCacheSizeEst = CacheSizeEst
{ csze_addr_size = sz
, csze_addr_sqsz = (sz * sz)
}
where sz = 2048
threaded :: Bool
threaded = rtsSupportsBoundThreads
openVC' :: Int -> FileLock -> FilePath -> IO VCache
openVC' nBytes fl fp = do
unless threaded (fail "VCache needs -threaded runtime")
dbEnv <- mdb_env_create
mdb_env_set_mapsize dbEnv nBytes
mdb_env_set_maxdbs dbEnv 5
mdb_env_open dbEnv fp vcFlags
flip onException (mdb_env_close dbEnv) $ do
txnInit <- mdb_txn_begin dbEnv Nothing False
dbiMemory <- mdb_dbi_open' txnInit (Just "@") [MDB_CREATE, MDB_INTEGERKEY]
dbiRoots <- mdb_dbi_open' txnInit (Just "/") [MDB_CREATE]
dbiHashes <- mdb_dbi_open' txnInit (Just "#") [MDB_CREATE, MDB_INTEGERKEY, MDB_DUPSORT, MDB_DUPFIXED, MDB_INTEGERDUP]
dbiRefct <- mdb_dbi_open' txnInit (Just "^") [MDB_CREATE, MDB_INTEGERKEY]
dbiRefct0 <- mdb_dbi_open' txnInit (Just "%") [MDB_CREATE, MDB_INTEGERKEY]
allocEnd <- findLastAddrAllocated txnInit dbiMemory
mdb_txn_commit txnInit
let allocStart = nextAllocAddress allocEnd
memory <- newMVar (initMemory allocStart)
tvWrites <- newTVarIO (Writes Map.empty [])
mvSignal <- newMVar ()
cLimit <- newIORef vcDefaultCacheLimit
cSize <- newIORef vcInitCacheSizeEst
cVRefs <- newMVar Map.empty
ctWrites <- newIORef $ WriteCt 0 0 0
gcStart <- newIORef Nothing
gcCount <- newIORef 0
rwLock <- newRWLock
_ <- mkWeakMVar mvSignal $ do
mdb_env_close dbEnv
FileLock.unlockFile fl
let vc = VCache
{ vcache_path = vcRootPath
, vcache_space = VSpace
{ vcache_lockfile = fl
, vcache_db_env = dbEnv
, vcache_db_memory = dbiMemory
, vcache_db_vroots = dbiRoots
, vcache_db_caddrs = dbiHashes
, vcache_db_refcts = dbiRefct
, vcache_db_refct0 = dbiRefct0
, vcache_memory = memory
, vcache_signal = mvSignal
, vcache_writes = tvWrites
, vcache_rwlock = rwLock
, vcache_climit = cLimit
, vcache_csize = cSize
, vcache_cvrefs = cVRefs
, vcache_signal_writes = updWriteCt ctWrites
, vcache_ct_writes = ctWrites
, vcache_alloc_init = allocStart
, vcache_gc_start = gcStart
, vcache_gc_count = gcCount
}
}
initVCacheThreads (vcache_space vc)
return $! vc
nextAllocAddress :: Address -> Address
nextAllocAddress addr | (0 == (addr .&. 1)) = 2 + addr
| otherwise = 1 + addr
findLastAddrAllocated :: MDB_txn -> MDB_dbi' -> IO Address
findLastAddrAllocated txn dbiMemory = alloca $ \ pKey ->
mdb_cursor_open' txn dbiMemory >>= \ crs ->
mdb_cursor_get' MDB_LAST crs pKey nullPtr >>= \ bFound ->
mdb_cursor_close' crs >>
if (not bFound) then return vcAllocStart else
peek pKey >>= \ key ->
let bBadSize = fromIntegral (sizeOf vcAllocStart) /= mv_size key in
if bBadSize then fail "VCache memory table corrupted" else
peekAligned (castPtr (mv_data key))
initMemory :: Address -> Memory
initMemory addr = m0 where
af = AllocFrame Map.empty Map.empty addr
ac = Allocator addr af af af
gcf = GCFrame Map.empty
gc = GC gcf gcf
m0 = Memory Map.empty Map.empty gc ac
updWriteCt :: IORef WriteCt -> Writes -> IO ()
updWriteCt var w = modifyIORef' var $ \ wct ->
let frmCt = 1 + wct_frames wct in
let pvCt = wct_pvars wct + Map.size (write_data w) in
let synCt = wct_sync wct + L.length (write_sync w) in
WriteCt { wct_frames = frmCt, wct_pvars = pvCt, wct_sync = synCt }
initVCacheThreads :: VSpace -> IO ()
initVCacheThreads vc = begin where
begin = do
task (writeStep vc)
task (cleanStep vc)
return ()
task step = void (forkIO (forever step `catch` onE))
onE :: SomeException -> IO ()
onE e | isBlockedOnMVar e = return ()
onE e = do
putErrLn "VCache background thread has failed."
putErrLn (indent " " (show e))
putErrLn "Halting program."
Sys.exitFailure
isBlockedOnMVar :: (Exception e) => e -> Bool
isBlockedOnMVar = isJust . test . toException where
test :: SomeException -> Maybe BlockedIndefinitelyOnMVar
test = fromException
putErrLn :: String -> IO ()
putErrLn = Sys.hPutStrLn Sys.stderr
indent :: String -> String -> String
indent ws = (ws ++) . indent' where
indent' ('\n':s) = '\n' : ws ++ indent' s
indent' (c:s) = c : indent' s
indent' [] = []