packages feed

conjure-0.1: src/Conjure/Logic/PeerManager.hs

-----------------------------------------------------------------------------
-- |
-- Module      :  Conjure.Logic.PeerManager
-- Copyright   :  (c) Lemmih 2005, 2006
-- License     :  BSD-like
--
-- Maintainer  :  lemmih@gmail.com
-- Stability   :  experimental
-- Portability :  non-portable (requires STM)
--
{-

The described algorihm is NOT yet implemented.

-}
{-

Ideology overview:

  This bittorrent implementation builds on a few assumptions:
    * High download rates are paramount.
    * We can only download from as many leechers as we upload to.
    * Seeding is a luxury. We will only do it when we can afford it.

  There are three types of uploads:
    * Regular uploads. For peers that we're downloading from.
    * Optimistic uploads. For peers that we aren't downloading from.
    * Seed uploads. For torrents that we're seeding.

  Controversial ideas:
    * Limit upload speeds. Many medium uploads are "better" than a few
      fast ones.
    * Put peers using conjure first in the seed list.

-}
{-
WARNING: THIS TEXT IS OUT DATED AND MAY BE INACCUATE.

Choke algorithm:

Factors:
  N(10) = seconds between regular (non-optimistic and non-seeding) unchokes.
  X(11) = max number of uploads.
  Z(10) = max number of uploads - K.
  G = number of intersted peers that we're downloading from.
  J = number of uploads. (including regular, optimistic and seed uploads)
  Y = number of seed uploads.
  M(30) = seconds between optimistic unchokes.
  K(1) = min number of optimistic uploads.
  L(60) = seconds to seed before selecting a new peer.
  I(30) = seconds to (optimistically) upload before choking.

Every N seconds we:
  * sort peer list by their download speed.
  * remove uninterested peers.
  * Make sure the fastes (min G Z) are unchoked (and unchoke them if they aren't).
  * Choke slower peers.
  * J is updated.
  * if J > Z -- if total uploads > max uploads
       then stop surplus seed uploads.
       else initiate (Z - J) new seed uploads.
            J is updated.
            when Z < J
                 initiate (Z - J) new optimistic uploads.

Seed uploads continues for L seconds. After that it appends its peer
to the end of the seed list and starts uploading to the first in the
seed list. If the new peer is the same as before, no choke/unchoke
action is needed.

Every M seconds we:
  * Initiate K new optimistic uploads at random.

Optimistic uploads continues for I seconds. After that they choke the
connection unless the peer has started uploaded to us.

We're intested in a peer if:
  he got a piece we want.
  the current number of downloads < the max number of downloads.

-}
-----------------------------------------------------------------------------
module Conjure.Logic.PeerManager
    ( runMainChoker )
    where

import Conjure.Types
import Conjure.Utils
import Conjure.Protocol.PWP.Types
import Conjure.STM.PeerCtrl
import Conjure.Debug()

import GHC.Conc

import Control.Monad.State
import Text.Printf()
import Data.List

import qualified Data.Set as Set
import Data.Set (Set)

data Classification
    = Idle Integer Integer     -- download age, upload age
    | Optimistic Int           -- bps
    | Upload Int               -- bps
    | DownloadUpload (Int,Int) -- (down bps, up bps)
    | Download Int Bool        -- bps, remote interest
      deriving (Eq, Show, Ord)

type ClassifiedPeer = (Classification, ConnectedPeer)


getDownloadBps :: Classification -> Int
getDownloadBps (DownloadUpload bps) = fst bps
getDownloadBps (Download bps _) = bps
getDownloadBps _ = 0

isDownload :: Classification -> Bool
isDownload (Download _ _) = True
isDownload (DownloadUpload _) = True
isDownload _ = False

isUpload :: Classification -> Bool
isUpload (Upload _) = True
isUpload (DownloadUpload _) = True
isUpload _ = False

isIdle :: Classification -> Bool
isIdle (Idle _ _) = True
isIdle _ = False

isOptimistic :: Classification -> Bool
isOptimistic (Optimistic _) = True
isOptimistic _ = False

onFst :: (a -> a -> t1) -> t -> (a, b) -> t1
onFst f _ b = f (fst b) (fst b)

orderByUploadAge :: Classification -> Classification -> Ordering
orderByUploadAge (Idle _ age1) (Idle _ age2) = compare age1 age2
orderByUploadAge    _ _                      = EQ

orderByImportance :: Classification -> Classification -> Ordering
orderByImportance = w
    where w (Idle age1 _) (Idle age2 _) = compare age2 age1 -- Old idles are worth more.
          w (Idle _ _) _ = LT
          w _ (Idle _ _) = GT
          w (Upload bps1) (Upload bps2) = compare bps1 bps2
          w (Upload _) _ = LT
          w _ (Upload _) = GT
          w (Optimistic bps1) (Optimistic bps2) = compare bps1 bps2
          w (Optimistic _) _ = LT
          w _ (Optimistic _) = GT
          w x y = compare (getDownloadBps x) (getDownloadBps y)

classifyPeer :: Integer -> ConnectedPeer -> STM (Classification, ConnectedPeer)
classifyPeer now peer
    = do c <- classifyPeer' now peer
         return (c,peer)
classifyPeer' :: Integer -> ConnectedPeer -> STM Classification
classifyPeer' now peer
    = do lChoke <- getLocalChoke peer
         rChoke <- getRemoteChoke peer
         lInterest <- getLocalInterest peer
         rInterest <- getRemoteInterest peer
         lastDownload <- getTimingAge now (cpDownloadTimings peer)
         lastUpload <- getTimingAge now (cpUploadTimings peer)
         downBps <- getTiming now 20000 (cpDownloadTimings peer)
         upBps <- getTiming now 20000 (cpUploadTimings peer)
         return $ case (lChoke, rChoke, lInterest, rInterest) of
                    (False,False,True,True)    -> DownloadUpload (downBps, upBps)
                    (_, False, True, _)        -> Download downBps rInterest
                    (False, _, _, True)
                        | lastDownload < 30000 -> Optimistic upBps
                        | otherwise            -> Upload upBps
                    _                          -> Idle lastDownload lastUpload



choke :: ConnectedPeer -> STM ()
choke connPeer
    = do sendMessage connPeer Choke
         setLocalChoke connPeer True

unchoke :: ConnectedPeer -> STM ()
unchoke connPeer
    = do sendMessage connPeer Unchoke
         setLocalChoke connPeer False


data CState =
    CState { toChoke :: Set ConnectedPeer
           , toUnchoke :: Set ConnectedPeer }

type Unchoker a = StateT CState STM a

markForChoke :: ConnectedPeer -> Unchoker ()
markForChoke peer = modify (\s -> s{toChoke = Set.insert peer (toChoke s)
                                   ,toUnchoke = Set.delete peer (toUnchoke s)})

markForUnchoke :: ConnectedPeer -> Unchoker ()
markForUnchoke peer = modify (\s -> s{toChoke = Set.delete peer (toChoke s)
                                     ,toUnchoke = Set.insert peer (toUnchoke s)})

runMainChoker :: TVar Int -- 'N'. Use an MVar?
              -> TVar [ConnectedPeer]
              -> TVar (Int, Int, Int, Int)
              -> IO ()
runMainChoker nSecs connPeersVar opts
    = forever $
      do atomically $ do st <- execStateT (mainUnchoker connPeersVar opts) (CState Set.empty Set.empty)
                         mapM_ choke (Set.toList $ toChoke st)
                         mapM_ unchoke (Set.toList $ toUnchoke st)
         secs <- atomically (readTVar nSecs)
         threadDelay $ secs * 10^(6::Int)

mainUnchoker :: TVar [ConnectedPeer]
             -> TVar (Int,Int,Int,Int)
             -> Unchoker ()
mainUnchoker peerListVar opts
    = do (maxPeers, maxDownloads, maxUploads, minOptimistic) <- lift $ readTVar opts
         peerList <- lift $ readTVar peerListVar
         now <- lift $ unsafeIOToSTM getCurrentTime
         -- Classify peers
         classified <- lift $ mapM (classifyPeer now) peerList
         maintained <- maintainDownloads classified
         trimmed    <- trimPeers maintained maxPeers maxDownloads maxUploads
         launchOptimistic trimmed now maxUploads minOptimistic
         return ()


{-
  Keep downloads running by unchoking interested peers.
-}
maintainDownloads :: [ClassifiedPeer] -> Unchoker [ClassifiedPeer]
maintainDownloads = mapM worker
    where worker (Download bps True, peer)
              = do markForUnchoke peer
                   return (DownloadUpload (bps, 0), peer)
          worker x = return x

{-
  Trim the total number of peers, the number of downloads and the number of uploads.
-}
trimPeers :: [ClassifiedPeer] -> Int -> Int -> Int -> Unchoker [ClassifiedPeer]
trimPeers classified maxPeers maxDownloads maxUploads
    = do let byImportance = reverse (sortBy (onFst orderByImportance) classified)
             (okPeers, excessPeers) = splitAt maxPeers byImportance
         -- disconnect from the excess peers
         lift $ mapM_ (disconnectPeer.snd) excessPeers
         -- Take the downloads and sort them by speed.
         let byDownload = reverse $ sort $ filter (isDownload.fst) okPeers
             (_, excessDownloads) = splitAt maxDownloads byDownload
         -- stop downloads from excess downloads
         lift $ mapM_ (disconnectPeer.snd) excessDownloads
         let okPeers' = filter (`notElem` excessDownloads) okPeers
             byUpload = reverse $ sort $ filter (isUpload.fst) okPeers'
             (_, excessUploads) = splitAt maxUploads byUpload
         -- stop uploads to excess uploads
         mapM_ (markForChoke.snd) excessUploads
         return $ filter (`notElem` excessUploads) okPeers'

{-
  Launch new optimistic uploads
-}
launchOptimistic :: [(Classification, ConnectedPeer)] -> Integer -> Int -> Int -> Unchoker ()
launchOptimistic classified now maxUploads minOptimistic
    = do -- Start new optimistic uploads if we can.
         -- We start more than 'minOptimistic' uploads if we're low on
         -- normal uploads. This is hugely beneficial when starting a new download.
         let uploads = length (filter (isUpload.fst) classified)
         idles <- lift $ fmap (sortBy (onFst orderByUploadAge)) $
                  filterM (getRemoteInterest.snd) (filter (isIdle.fst) classified)
         let initUploads = abs (maxUploads - uploads)
             currentOptimistic = length (filter (isOptimistic.fst) classified)
             newOptimistic = abs (minOptimistic - currentOptimistic)
         forM_ (take (initUploads+newOptimistic) idles) $ \(_, peer) ->
             do lift $ recordTiming (cpDownloadTimings peer) now 0 -- Mark now as start of optimistic upload.
                markForUnchoke peer
         return ()