packages feed

cabal-cache-1.0.5.3: src/HaskellWorks/CabalCache/Concurrent/DownloadQueue.hs

{-# LANGUAGE BlockArguments    #-}
{-# LANGUAGE DataKinds         #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards   #-}
{-# LANGUAGE TypeApplications  #-}

module HaskellWorks.CabalCache.Concurrent.DownloadQueue
  ( DownloadStatus(..)
  , createDownloadQueue
  , runQueue
  ) where

import Control.Monad.Catch          (MonadMask(..))
import Control.Monad.IO.Class       (MonadIO(..))
import Data.Function                ((&))
import Data.Set                     ((\\))
import HaskellWorks.CabalCache.Show (tshow)

import qualified Control.Concurrent.STM                  as STM
import qualified Control.Monad.Catch                     as CMC
import qualified Data.Relation                           as R
import qualified Data.Set                                as S
import qualified HaskellWorks.CabalCache.Concurrent.Type as Z
import qualified HaskellWorks.CabalCache.IO.Console      as CIO
import qualified Network.AWS                             as AWS
import qualified System.IO                               as IO

data DownloadStatus = DownloadSuccess | DownloadFailure deriving (Eq, Show)

createDownloadQueue :: [(Z.ProviderId, Z.ConsumerId)] -> STM.STM Z.DownloadQueue
createDownloadQueue dependencies = do
  tDependencies <- STM.newTVar (R.fromList dependencies)
  tDownloading  <- STM.newTVar S.empty
  tFailures     <- STM.newTVar S.empty
  return Z.DownloadQueue {..}

takeReady :: Z.DownloadQueue -> STM.STM (Maybe Z.PackageId)
takeReady Z.DownloadQueue {..} = do
  dependencies  <- STM.readTVar tDependencies
  downloading   <- STM.readTVar tDownloading
  failures      <- STM.readTVar tFailures

  -- The packages that need to be downloaded.  This set can shrink when packages
  -- have been downloaded, or grow when a download unlocks another depdendency for
  -- download.  When downloads fail, they are not removed from the set, but are
  -- tracked separatedly in failures.
  let queued = R.ran dependencies \\ R.dom dependencies

  -- Packages that are ready for download.  These are packages that have been queued
  -- but are not currently downloaded nor have failed download.
  let ready = queued \\ downloading \\ failures

  case S.lookupMin ready of
    Just packageId -> do
      STM.writeTVar tDownloading (S.insert packageId downloading)
      return (Just packageId)
    Nothing -> if S.null (queued \\ failures)
      then return Nothing
      else STM.retry

commit :: Z.DownloadQueue -> Z.PackageId -> STM.STM ()
commit Z.DownloadQueue {..} packageId = do
  dependencies  <- STM.readTVar tDependencies
  downloading   <- STM.readTVar tDownloading

  STM.writeTVar tDownloading  $ S.delete packageId downloading
  STM.writeTVar tDependencies $ R.withoutRan (S.singleton packageId) dependencies

failDownload :: Z.DownloadQueue -> Z.PackageId -> STM.STM ()
failDownload Z.DownloadQueue {..} packageId = do
  downloading <- STM.readTVar tDownloading
  failures    <- STM.readTVar tFailures

  STM.writeTVar tDownloading  $ S.delete packageId downloading
  STM.writeTVar tFailures     $ S.insert packageId failures

runQueue :: (MonadIO m, MonadMask m) => Z.DownloadQueue -> (Z.PackageId -> m DownloadStatus) -> m ()
runQueue downloadQueue f = do
  maybePackageId <- (liftIO $ STM.atomically $ takeReady downloadQueue)

  case maybePackageId of
    Just packageId -> do
      downloadStatus <- f packageId
        & do CMC.handle @_ @AWS.Error \e -> do
              liftIO $ CIO.hPutStrLn IO.stderr $ "Failed download due to exception: " <> tshow e
              pure DownloadFailure
        & do CMC.handleAll \e -> do
              liftIO $ CIO.hPutStrLn IO.stderr $ "Aborting due to unexpected exception during download: " <> tshow e
              liftIO $ STM.atomically $ failDownload downloadQueue packageId
              CMC.throwM e
      case downloadStatus of
        DownloadSuccess -> do liftIO $ STM.atomically $ commit downloadQueue packageId
        DownloadFailure -> do liftIO $ STM.atomically $ failDownload downloadQueue packageId
      runQueue downloadQueue f

    Nothing -> return ()