packages feed

halfs-2.0: System/Device/BlockDevice.hs

{-# LANGUAGE MultiParamTypeClasses, FlexibleInstances #-}
module System.Device.BlockDevice(
         BlockDevice(..)
       , BCM
       , newCachedBlockDevice
       , newRescaledBlockDevice
       , runBCM
       )
 where

import Control.Monad.State.Strict
import Data.ByteString(ByteString)
import qualified Data.ByteString as BS
import Data.Map(Map)
import qualified Data.Map as Map
import Data.Word

-- |The data type that describes the interface to a BlockDevice. If you can
-- fill this out reasonably, you can be a file system backend.
data BlockDevice m = BlockDevice {
    -- |The size of the smallest read/write block for the device, in bytes.
    bdBlockSize  :: Word64
    -- |The number of blocks in the device.
  , bdNumBlocks  :: Word64
    -- |Read a the given block number for the device, where block numbers run
    -- from 0 to (bdNumBlocks - 1). Smart / paranoid block device implementers
    -- should probably verify their inputs, just in case ...
  , bdReadBlock  :: Word64 -> m ByteString
    -- |Write a given block to the given block number. See the note for
    -- bdReadBlock for indexing and paranoia information.
  , bdWriteBlock :: Word64 -> ByteString -> m ()
    -- |Force any unfinished writes to be written to disk.
  , bdFlush      :: m ()
    -- |Shutdown the device cleanly.
  , bdShutdown   :: m ()
  }

-- ----------------------------------------------------------------------------
--
-- Wrapper that will rescale blocks to a more appropriate size. Note that
-- doing this will require that the desired block size is a multiple of the
-- underlying block size.
--
-- ----------------------------------------------------------------------------

-- |Create a new block device that rescales the block size of the underlying
-- device. This is helpful if, for example, your underlying device has a block
-- size of 512 bytes but you want to deal with it in terms of blocks of 4k.
--
-- Note that this returns a Maybe because it requires that the desired block
-- size be a multiple of the underlying block size. Also note that the
-- resulting block device may be smaller than the original block device, if
--    (bdNumBlocks old `mod` (bdBlockSize new `div` bdBlockSize old)) != 0
newRescaledBlockDevice :: Monad m =>
                          Word64 -> BlockDevice m ->
                          Maybe (BlockDevice m)
newRescaledBlockDevice bsize dev
  | bsize `mod` bdBlockSize dev /= 0 = Nothing
  | otherwise                        = Just res
 where
  res = dev {
    bdBlockSize  = bsize
  , bdNumBlocks  = blocks
  , bdReadBlock  = readBlock
  , bdWriteBlock = writeBlock
  }
  oldbs          = fromIntegral $! bdBlockSize dev
  ratio          = bsize `div` bdBlockSize dev
  blocks         = bdNumBlocks dev `div` ratio
  readBlock  i   = liftM BS.concat $ forM [start..end] (bdReadBlock dev)
                   where start = i * ratio; end = (i+1) * ratio - 1
  writeBlock i b = write (i * ratio) b
  --
  write i b | BS.null b = return ()
            | otherwise = do let (start, rest) = BS.splitAt oldbs b
                             bdWriteBlock dev i start
                             write (i + 1) rest


-- ----------------------------------------------------------------------------
--
-- Wrapper for automatically caching block reads and writes
--
-- ----------------------------------------------------------------------------

type BCM m      = StateT CacheState m
type BlockMap   = Map Word64 (ByteString, Bool, Word64)

data CacheState = BCS {
    _cache     :: BlockMap
  , _timestamp :: Word64
  }

-- |Given an existing block device, creates a new block device that will cache
-- reads and writes.
newCachedBlockDevice :: Monad m => Int -> BlockDevice m -> BlockDevice (BCM m)
newCachedBlockDevice size dev = dev {
    bdReadBlock  = readBlock
  , bdWriteBlock = writeBlock
  , bdFlush      = flush
  , bdShutdown   = lift $ bdShutdown dev
  }
 where
  readBlock  i   = runCacheOp $ \ cache ts ->
                     case Map.lookup i cache of
                       Just (res, dirty ,_) -> do
                         return (Map.insert i (res, dirty, ts) cache, res)
                       Nothing  -> do
                         cache' <- if cacheFull cache
                                     then evictEntry cache dev
                                     else return cache
                         b <- lift $ bdReadBlock dev i
                         return (Map.insert i (b,False,ts) cache', b)
  writeBlock i b = runCacheOp $ \ cache ts ->
                     case Map.lookup i cache of
                       Just _  ->
                         return (Map.insert i (b, True, ts) cache, ())
                       Nothing -> do
                         cache' <- if cacheFull cache
                                     then evictEntry cache dev
                                     else return cache
                         return (Map.insert i (b, True, ts) cache', ())
  flush          = do BCS cache _ <- get
                      forM_ (Map.toList cache) $ \ (i, (block, dirty, _)) ->
                        when dirty $ do
                          lift $ bdWriteBlock dev i block
  --
  cacheFull m = Map.size m >= size

-- |Run something in the BlockCache monad.
runBCM :: Monad m => BCM m a -> m a
runBCM m = evalStateT m (BCS Map.empty 0)

-- Run an operation that does something with the cache.
runCacheOp :: Monad m =>
              (BlockMap -> Word64 -> BCM m (BlockMap, a)) ->
              BCM m a
runCacheOp f = do
  BCS mp ts <- get
  (mp', res) <- f mp ts
  put (BCS mp' (ts + 1))
  return res

-- Evict the oldest entry from the Cache
evictEntry :: Monad m => BlockMap -> BlockDevice m -> BCM m BlockMap
evictEntry m d
  | oldestEntry == maxBound = fail "Your cache state has gone insane!"
  | dirty                   = do lift $ bdWriteBlock d oldestEntry block
                                 return $! Map.delete oldestEntry m
  | otherwise               = return $! Map.delete oldestEntry m
 where
  (oldestEntry, (block, dirty, _)) =
    Map.foldrWithKey (\ i igrp@(_, _, its) old@(_, (_, _, jts)) ->
                       if its < jts then (i, igrp) else old)
                     (maxBound, (undefined, undefined, maxBound))
                     m