HaskellTorrent-0.1: src/Process/PieceMgr.hs
module Process.PieceMgr
( PieceMgrMsg(..)
, PieceMgrChannel
, ChokeInfoChannel
, ChokeInfoMsg(..)
, Blocks(..)
, start
, createPieceDb
)
where
import Control.Concurrent
import Control.Concurrent.CML
import Control.Monad.State
import Data.List
import qualified Data.ByteString as B
import qualified Data.Map as M
import qualified Data.Set as S
import qualified Data.IntSet as IS
import Prelude hiding (log)
import System.Random
import System.Random.Shuffle
import Process.FS hiding (start)
import Process.Status as STP hiding (start)
import Supervisor
import Torrent
import Process
----------------------------------------------------------------------
-- | The Piece Database tracks the current state of the Torrent with respect to pieces.
-- In the database, we book-keep what pieces are missing, what are done and what are
-- currently in the process of being downloaded. The crucial moment is when we think
-- we have a full piece: we check it against its SHA1 and if it is good, we can mark
-- that piece as done.
--
-- Better implementations for selecting among the pending Pieces is probably crucial
-- to an effective client, but we keep it simple for now.
data PieceDB = PieceDB
{ pendingPieces :: IS.IntSet -- ^ Pieces currently pending download
, donePiece :: IS.IntSet -- ^ Pieces that are done
, donePush :: [ChokeInfoMsg] -- ^ Pieces that should be pushed to the Choke Mgr.
, inProgress :: M.Map PieceNum InProgressPiece -- ^ Pieces in progress
, downloading :: [(PieceNum, Block)] -- ^ Blocks we are currently downloading
, infoMap :: PieceMap -- ^ Information about pieces
, endGaming :: Bool -- ^ If we have done any endgame work this is true
} deriving Show
-- | The InProgressPiece data type describes pieces in progress of being downloaded.
-- we keep track of blocks which are pending as well as blocks which are done. We
-- also keep track of a count of the blocks. When a block is done, we cons it unto
-- @ipHaveBlocks@. When @ipHave == ipDone@, we check the piece for correctness. The
-- field @ipHaveBlocks@ could in principle be omitted, but for now it is kept since
-- we can use it for asserting implementation correctness. We note that both the
-- check operations are then O(1) and probably fairly fast.
data InProgressPiece = InProgressPiece
{ ipDone :: Int -- ^ Number of blocks when piece is done
, ipHaveBlocks :: S.Set Block -- ^ The blocks we have
, ipPendingBlocks :: [Block] -- ^ Blocks still pending
} deriving Show
-- INTERFACE
----------------------------------------------------------------------
-- | When the PieceMgrP returns blocks to a peer, it will return them in either
-- "Leech Mode" or in "Endgame mode". The "Leech mode" is when the client is
-- leeching like normal. The "Endgame mode" is when the client is entering the
-- endgame. This means that the Peer should act differently to the blocks.
data Blocks = Leech [(PieceNum, Block)]
| Endgame [(PieceNum, Block)]
-- | Messages for RPC towards the PieceMgr.
data PieceMgrMsg = GrabBlocks Int IS.IntSet (Channel Blocks)
-- ^ Ask for grabbing some blocks
| StoreBlock PieceNum Block B.ByteString
-- ^ Ask for storing a block on the file system
| PutbackBlocks [(PieceNum, Block)]
-- ^ Put these blocks back for retrieval
| AskInterested IS.IntSet (Channel Bool)
-- ^ Ask if any of these pieces are interesting
| GetDone (Channel [PieceNum])
-- ^ Get the pieces which are already done
data ChokeInfoMsg = PieceDone PieceNum
| BlockComplete PieceNum Block
| TorrentComplete
deriving (Eq, Show)
type PieceMgrChannel = Channel PieceMgrMsg
type ChokeInfoChannel = Channel ChokeInfoMsg
data PieceMgrCfg = PieceMgrCfg
{ pieceMgrCh :: PieceMgrChannel
, fspCh :: FSPChannel
, chokeCh :: ChokeInfoChannel
, statusCh :: StatusChan
}
instance Logging PieceMgrCfg where
logName _ = "Process.PieceMgr"
type PieceMgrProcess v = Process PieceMgrCfg PieceDB v
start :: PieceMgrChannel -> FSPChannel -> ChokeInfoChannel -> StatusChan -> PieceDB
-> SupervisorChan -> IO ThreadId
start mgrC fspC chokeC statC db supC =
spawnP (PieceMgrCfg mgrC fspC chokeC statC) db
(catchP (forever pgm)
(defaultStopHandler supC))
where pgm = do
assertPieceDB
dl <- gets donePush
(if dl == []
then receiveEvt
else chooseP [receiveEvt, sendEvt (head dl)]) >>= syncP
sendEvt elem = do
ev <- sendPC chokeCh elem
wrapP ev remDone
remDone :: () -> Process PieceMgrCfg PieceDB ()
remDone () = modify (\db -> db { donePush = tail (donePush db) })
receiveEvt = do
ev <- recvPC pieceMgrCh
wrapP ev (\msg ->
case msg of
GrabBlocks n eligible c ->
do debugP "Grabbing Blocks"
blocks <- grabBlocks' n eligible
debugP "Grabbed..."
syncP =<< sendP c blocks
StoreBlock pn blk d ->
do debugP $ "Storing block: " ++ show (pn, blk)
storeBlock pn blk d
modify (\s -> s { downloading = downloading s \\ [(pn, blk)] })
endgameBroadcast pn blk
done <- updateProgress pn blk
when done
(do assertPieceComplete pn
pend <- gets pendingPieces
iprog <- gets inProgress
infoP $ "Piece #" ++ show pn
++ " completed, there are "
++ (show $ IS.size pend) ++ " pending "
++ (show $ M.size iprog) ++ " in progress"
l <- gets infoMap >>=
(\pm -> case M.lookup pn pm of
Nothing -> fail "Storeblock: M.lookup"
Just x -> return $ len x)
sendPC statusCh (CompletedPiece l) >>= syncP
pieceOk <- checkPiece pn
case pieceOk of
Nothing ->
do fail "PieceMgrP: Piece Nonexisting!"
Just True -> do completePiece pn
markDone pn
checkFullCompletion
Just False -> putbackPiece pn)
PutbackBlocks blks ->
mapM_ putbackBlock blks
GetDone c -> do done <- liftM IS.toList $ gets donePiece
syncP =<< sendP c done
AskInterested pieces retC -> do
inProg <- liftM (IS.fromList . M.keys) $ gets inProgress
pend <- gets pendingPieces
-- @i@ is the intersection with with we need and the peer has.
let i = IS.null $ IS.intersection pieces
$ IS.union inProg pend
syncP =<< sendP retC (not i))
storeBlock n blk contents = syncP =<< (sendPC fspCh $ WriteBlock n blk contents)
endgameBroadcast pn blk =
gets endGaming >>=
flip when (modify (\db -> db { donePush = (BlockComplete pn blk) : donePush db }))
markDone pn = do
modify (\db -> db { donePush = (PieceDone pn) : donePush db })
checkPiece n = do
ch <- liftIO channel
syncP =<< (sendPC fspCh $ CheckPiece n ch)
syncP =<< recvP ch (const True)
-- HELPERS
----------------------------------------------------------------------
createPieceDb :: PiecesDoneMap -> PieceMap -> PieceDB
createPieceDb mmap pmap = PieceDB pending done [] M.empty [] pmap False
where pending = filt (==False)
done = filt (==True)
filt f = IS.fromList . M.keys $ M.filter f mmap
----------------------------------------------------------------------
-- | The call @completePiece db pn@ will mark that the piece @pn@ is completed
completePiece :: PieceNum -> PieceMgrProcess ()
completePiece pn = modify (\db -> db { inProgress = M.delete pn (inProgress db),
donePiece = IS.insert pn $ donePiece db })
-- | Handle torrent completion
checkFullCompletion :: PieceMgrProcess ()
checkFullCompletion = do
doneP <- gets donePiece
im <- gets infoMap
when (M.size im == IS.size doneP)
(do liftIO $ putStrLn "Torrent Completed"
sendPC statusCh STP.TorrentCompleted >>= syncP
sendPC chokeCh TorrentComplete >>= syncP)
-- | The call @putBackPiece db pn@ will mark the piece @pn@ as not being complete
-- and put it back into the download queue again.
putbackPiece :: PieceNum -> PieceMgrProcess ()
putbackPiece pn = modify (\db -> db { inProgress = M.delete pn (inProgress db),
pendingPieces = IS.insert pn $ pendingPieces db })
-- | Put back a block for downloading.
-- TODO: This is rather slow, due to the (\\) call, but hopefully happens rarely.
putbackBlock :: (PieceNum, Block) -> PieceMgrProcess ()
putbackBlock (pn, blk) = do
done <- gets donePiece
unless (IS.member pn done) -- Happens at endgame, stray block
$ modify (\db -> db { inProgress = ndb (inProgress db)
, downloading = downloading db \\ [(pn, blk)]})
where ndb db = M.alter f pn db
-- The first of these might happen in the endgame
f Nothing = fail "The 'impossible' happened"
f (Just ipp) = Just ipp { ipPendingBlocks = blk : ipPendingBlocks ipp }
-- | Assert that a Piece is Complete. Can be omitted when we know it works
-- and we want a faster client.
assertPieceComplete :: PieceNum -> PieceMgrProcess ()
assertPieceComplete pn = do
inprog <- gets inProgress
ipp <- case M.lookup pn inprog of
Nothing -> fail "assertPieceComplete: Could not lookup piece number"
Just x -> return x
dl <- gets downloading
pm <- gets infoMap
sz <- case M.lookup pn pm of
Nothing -> fail "assertPieceComplete: Could not lookup piece in piecemap"
Just x -> return $ len x
unless (assertAllDownloaded dl pn)
(fail "Could not assert that all pieces were downloaded when completing a piece")
unless (assertComplete ipp sz)
(fail $ "Could not assert completion of the piece #" ++ show pn
++ " with block state " ++ show ipp)
where assertComplete ip sz = checkContents 0 (fromIntegral sz) (S.toAscList (ipHaveBlocks ip))
-- Check a single block under assumptions of a cursor at offs
checkBlock (offs, left, state) blk = (offs + blockSize blk,
left - blockSize blk,
state && offs == blockOffset blk)
checkContents os l blks = case foldl checkBlock (os, l, True) blks of
(_, 0, True) -> True
_ -> False
assertAllDownloaded blocks pn = all (\(pn', _) -> pn /= pn') blocks
-- | Update the progress on a Piece. When we get a block from the piece, we will
-- track this in the Piece Database. This function returns a pair @(complete, nDb)@
-- where @complete@ is @True@ if the piece is percieved to be complete and @False@
-- otherwise.
updateProgress :: PieceNum -> Block -> PieceMgrProcess Bool
updateProgress pn blk = do
ipdb <- gets inProgress
case M.lookup pn ipdb of
Nothing -> do debugP "updateProgress can't find progress block, error?"
return False
Just pg ->
let blkSet = ipHaveBlocks pg
in if blk `S.member` blkSet
then return False -- Stray block download.
-- Will happen without FAST extension
-- at times
else checkComplete pg { ipHaveBlocks = S.insert blk blkSet }
where checkComplete pg = do
modify (\db -> db { inProgress = M.adjust (const pg) pn (inProgress db) })
debugP $ "Iphave : " ++ show (ipHave pg) ++ " ipDone: " ++ show (ipDone pg)
return (ipHave pg == ipDone pg)
ipHave = S.size . ipHaveBlocks
blockPiece :: BlockSize -> PieceSize -> [Block]
blockPiece blockSz pieceSize = build pieceSize 0 []
where build 0 os accum = reverse accum
build leftBytes os accum | leftBytes >= blockSz =
build (leftBytes - blockSz)
(os + blockSz)
$ Block os blockSz : accum
| otherwise = build 0 (os + leftBytes) $ Block os leftBytes : accum
-- | The call @grabBlocks' n eligible db@ tries to pick off up to @n@ pieces from
-- the @n@. In doing so, it will only consider pieces in @eligible@. It returns a
-- pair @(blocks, db')@, where @blocks@ are the blocks it picked and @db'@ is the resulting
-- db with these blocks removed.
grabBlocks' :: Int -> IS.IntSet -> PieceMgrProcess Blocks
grabBlocks' k eligible = do
blocks <- tryGrabProgress k eligible []
pend <- gets pendingPieces
if blocks == [] && IS.null pend
then do blks <- grabEndGame k eligible
modify (\db -> db { endGaming = True })
debugP $ "PieceMgr entered endgame."
return $ Endgame blks
else do modify (\s -> s { downloading = blocks ++ (downloading s) })
return $ Leech blocks
where
-- Grabbing blocks is a state machine implemented by tail calls
-- Try grabbing pieces from the pieces in progress first
tryGrabProgress 0 _ captured = return captured
tryGrabProgress n ps captured = do
inProg <- gets inProgress
let is = IS.intersection ps (IS.fromList $ M.keys inProg)
case IS.null is of
True -> tryGrabPending n ps captured
False -> grabFromProgress n ps (head $ IS.elems is) captured
-- The Piece @p@ was found, grab it
grabFromProgress n ps p captured = do
inprog <- gets inProgress
ipp <- case M.lookup p inprog of
Nothing -> fail "grabFromProgress: could not lookup piece"
Just x -> return x
let (grabbed, rest) = splitAt n (ipPendingBlocks ipp)
nIpp = ipp { ipPendingBlocks = rest }
-- This rather ugly piece of code should be substituted with something better
if grabbed == []
-- All pieces are taken, try the next one.
then tryGrabProgress n (IS.delete p ps) captured
else do modify (\db -> db { inProgress = M.insert p nIpp inprog })
tryGrabProgress (n - length grabbed) ps ([(p,g) | g <- grabbed] ++ captured)
-- Try grabbing pieces from the pending blocks
tryGrabPending n ps captured = do
pending <- gets pendingPieces
let isn = IS.intersection ps pending
case IS.null isn of
True -> return $ captured -- No (more) pieces to download, return
False -> do
h <- pickRandom (IS.toList isn)
infMap <- gets infoMap
inProg <- gets inProgress
blockList <- createBlock h
let sz = length blockList
ipp = InProgressPiece sz S.empty blockList
modify (\db -> db { pendingPieces = IS.delete h (pendingPieces db),
inProgress = M.insert h ipp inProg })
tryGrabProgress n ps captured
grabEndGame n ps = do -- In endgame we are allowed to grab from the downloaders
dls <- liftM (filter (\(p, _) -> IS.member p ps)) $ gets downloading
g <- liftIO newStdGen
let shuffled = shuffle' dls (length dls) g
return $ take n shuffled
pickRandom pieces = do
n <- liftIO $ getStdRandom (\gen -> randomR (0, length pieces - 1) gen)
return $ pieces !! n
createBlock :: Int -> PieceMgrProcess [Block]
createBlock pn = do
gets infoMap >>= (\im -> case M.lookup pn im of
Nothing -> fail "createBlock: could not lookup piece"
Just ipp -> return $ cBlock ipp)
where cBlock = blockPiece defaultBlockSize . fromInteger . len
assertPieceDB :: PieceMgrProcess ()
assertPieceDB = assertSets >> assertInProgress >> assertDownloading
where
-- If a piece is pending in the database, we have the following rules:
--
-- - It is not done.
-- - It is not being downloaded
-- - It is not in progresss.
--
-- If a piece is done, we have the following rules:
--
-- - It is not in progress.
-- - There are no more downloading blocks.
assertSets = do
pending <- gets pendingPieces
done <- gets donePiece
down <- liftM (IS.fromList . map fst) $ gets downloading
iprog <- liftM (IS.fromList . M.keys) $ gets inProgress
let pdis = IS.intersection pending done
pdownis = IS.intersection pending down
piprogis = IS.intersection pending iprog
doneprogis = IS.intersection done iprog
donedownis = IS.intersection done down
unless (IS.null pdis)
(fail $ "Pending/Done violation of pieces: " ++ show pdis)
unless (IS.null pdownis)
(fail $ "Pending/Downloading violation of pieces: " ++ show pdownis)
unless (IS.null piprogis)
(fail $ "Pending/InProgress violation of pieces: " ++ show piprogis)
unless (IS.null doneprogis)
(fail $ "Done/InProgress violation of pieces: " ++ show doneprogis)
unless (IS.null donedownis)
(fail $ "Done/Downloading violation of pieces: " ++ show donedownis)
-- If a piece is in Progress, we have:
--
-- - There is a relationship with what pieces are downloading
-- - If a block is ipPending, it is not in the downloading list
-- - If a block is ipHave, it is not in the downloading list
assertInProgress = do
inProg <- gets inProgress
mapM_ checkInProgress $ M.toList inProg
checkInProgress (pn, ipp) = do
when ( (S.size $ ipHaveBlocks ipp) >= ipDone ipp)
(fail $ "Piece in progress " ++ show pn
++ " has downloaded more blocks than the piece has")
assertDownloading = do
down <- gets downloading
mapM_ checkDownloading down
checkDownloading (pn, blk) = do
prog <- gets inProgress
case M.lookup pn prog of
Nothing -> fail $ "Piece " ++ show pn ++ " not in progress while We think it was"
Just ipp -> do
when (blk `elem` ipPendingBlocks ipp)
(fail $ "P/Blk " ++ show (pn, blk) ++ " is in the Pending Block list")
when (S.member blk $ ipHaveBlocks ipp)
(fail $ "P/Blk " ++ show (pn, blk) ++ " is in the HaveBlocks set")