packages feed

project-m36-1.2.0: src/lib/ProjectM36/Cache/Tuple.hs

-- | Define a file which can be used to cache tuples for arbitrary, flat tuple sets.
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE DerivingVia #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE ScopedTypeVariables #-}

module ProjectM36.Cache.Tuple where
import ProjectM36.Base
import ProjectM36.Serialise.Base ()
import ProjectM36.RelExprSize
import qualified Data.Vector as V
import qualified Data.ByteString as BS
import Network.ByteOrder
import Data.Time.Clock
import Codec.Winery as W
import System.IO
import GHC.Generics
import Control.Monad (when, foldM)
import Control.Exception
import qualified Streamly.Data.Stream.Prelude as SP
import qualified Streamly.Internal.Data.Stream as SD
import Control.Monad.IO.Class (liftIO, MonadIO)
import Data.Bifunctor
import Data.Proxy

{- file format for tuple cache
-bytecount for info at end of file
TupleCacheInfo

-use blockSizes to get the next block of bytes
-}
type FileMagic = BS.ByteString

newtype InvalidFileMagicException = InvalidFileMagicException BS.ByteString deriving (Show)
newtype TruncatedFileException = TruncatedFileException ByteCount deriving (Show)
data SerialisationSchemaMismatch = SerialisationSchemaMismatch deriving (Show)

instance Exception InvalidFileMagicException
instance Exception TruncatedFileException
instance Exception SerialisationSchemaMismatch

fileMagic :: FileMagic
fileMagic = "PM36CacheTuple_v000"

type ByteOffset = ByteCount

data TupleCacheInfo =
  TupleCacheInfo { blockSizes :: V.Vector (ByteOffset, ByteCount),
                   representing :: PinnedRelationalExpr,
                   created :: UTCTime,
                   tupleSchema :: W.Schema -- the schema is shared amongst all tuples
                 }
  deriving (Generic, Show)
  deriving Serialise via WineryRecord TupleCacheInfo

blockCount :: TupleCacheInfo -> Int
blockCount i = V.length (blockSizes i)

readByteCount :: Handle -> IO Int
readByteCount h = do
  bytes <- BS.hGet h 8
  when (BS.length bytes /= 8) $ throw (TruncatedFileException 8)
  pure (fromIntegral $ word64 bytes)

-- | Runs checks such as confirming that the file size can accommodate the blockSizes in the cache info, but without reading the entire file so that this can be done routinely.
validateQuickly :: Handle -> IO ()
validateQuickly = undefined

--read and deserialize block of bytes prefixed by a byte count
readBlock :: Serialise a => Handle -> IO a
readBlock h = do
  bc <- readByteCount h
  bytes <- BS.hGetNonBlocking h bc
  when (BS.length bytes /= bc) $ throw (InvalidFileMagicException bytes)
  case deserialise bytes of
    Left err -> throw err
    Right val -> pure val
  
-- | get the cache info from a tuple cache file, may throw IO exception or InvalidFileMagicException
readCacheInfo :: Handle -> IO TupleCacheInfo
readCacheInfo h = do
  --look for magic
  potentialMagic <- BS.hGetNonBlocking h (BS.length fileMagic)
  when (potentialMagic /= fileMagic) $ throw (InvalidFileMagicException potentialMagic)
  readBlock h 

-- since this is just for caching, IO errors can be logged and ignored due to cache creation failure
writeTupleStream :: Handle -> PinnedRelationalExpr -> ByteCount -> [RelationTuple] -> IO ()
writeTupleStream h expr groupSize tuples = do
  --how can I write the tuple info if I only have a stream of the tuples :/ should the tuple info be at the back of the file then?
  --create slices and record sizes for metadata
  --write tuple info
  --write metadata plus metadata size at end
  now <- getCurrentTime
  BS.hPutStr h fileMagic
  let writeTupleBlock tuples' = do
          let tupleListBytes = serialiseOnly tuples'
              bytesCount = fromIntegral (BS.length tupleListBytes)
          BS.hPutStr h tupleListBytes
          pure bytesCount
      tupleBlockWriter (accsize, offset', accTuples, metadata) nextTuple = do
        -- add a new grouping, if we go over the size limit
        let estimatedTupleSize = size nextTuple
        if accsize + estimatedTupleSize >= groupSize then do
          tupleListByteCount <- writeTupleBlock (accTuples <> [nextTuple])
          pure (0,
                offset' + tupleListByteCount,
                [],
                metadata <> [(offset', tupleListByteCount)])
          else do
          pure (accsize + estimatedTupleSize,
                offset',
                accTuples <> [nextTuple],
                metadata)
      writeFinalBlock x@(_, _, [], _) = pure x
      writeFinalBlock (acc, offset', accTuples, metadata) = do
        bytesWritten <- writeTupleBlock accTuples
        pure (acc, offset', [], metadata <> [(offset', bytesWritten)])
        
  (_, _, _, blockSizeMetadata) <- foldM tupleBlockWriter (0,0,mempty,mempty) tuples >>= writeFinalBlock
  --write remaining tuples which didn't get get us over the last threshold
  --create block metadata at end of file, offset by file magic at beginning of file
  let tupleCacheInfo = TupleCacheInfo { blockSizes = V.fromList (map (first offsetByMagic) blockSizeMetadata),
                                        representing = expr,
                                        created = now,
                                        tupleSchema = schema (Proxy :: Proxy RelationTuple)
                                        }
      offsetByMagic = (+) (fromIntegral (BS.length fileMagic))
      tInfoData = serialise tupleCacheInfo
  BS.hPutStr h tInfoData
  BS.hPutStr h (bytestring64 (fromIntegral (BS.length tInfoData)))
  pure ()

readTupleStream :: MonadIO m => Handle -> SP.Stream m RelationTuple
readTupleStream h = SD.unCross $ do
  --skip filemagic
  --jump to end to read tuple cache info
  --deserialise blocks from metadata
  tupleCacheInfo <- liftIO $ do
    potentialMagic <- BS.hGetNonBlocking h (BS.length fileMagic)
    when (potentialMagic /= fileMagic) $ throw (InvalidFileMagicException potentialMagic)
    --get size of tuple cache info
    hSeek h SeekFromEnd (-8)
    infoSize <- readByteCount h
    hSeek h SeekFromEnd (-(fromIntegral (8 + infoSize)))
    tcacheInfo <- deserialise <$> BS.hGet h infoSize
    case tcacheInfo of
      Left err -> throw err
      Right info -> do
        when (tupleSchema info /= schema (Proxy :: Proxy RelationTuple)) $ throw SerialisationSchemaMismatch
        --print (blockSizes info)
        pure info
  let readTupleBlock (offset', byteLength) = liftIO $ do
--        print ("readTupleBlock", offset', byteLength)
        hSeek h AbsoluteSeek (fromIntegral offset')
        tuples <- deserialiseOnly' <$> BS.hGet h (fromIntegral byteLength)
        case tuples of
          Left err -> throw err
          Right tuples' -> do
--            print ("readTupleBlock", tuples')
            pure $ SP.fromList tuples'
  SD.mkCross $ SP.concatMapM readTupleBlock (SP.fromList (V.toList (blockSizes tupleCacheInfo)))
  
deserialiseOnly' :: forall s. Serialise s => BS.ByteString -> Either WineryException s
deserialiseOnly' bytes = do
  dec <- getDecoder (schema (Proxy :: Proxy s))
  pure (evalDecoder dec bytes)