{-# LANGUAGE TemplateHaskell, StandaloneDeriving, GeneralizedNewtypeDeriving, DeriveDataTypeable, TypeFamilies, FlexibleInstances, MultiParamTypeClasses, BangPatterns #-} module Distribution.Server.Features.DownloadCount.State where import Data.Time.Calendar (Day(..)) import Data.Version (Version) import Data.Typeable (Typeable) import Data.Foldable (forM_) import Control.Arrow (first) import Control.Monad (liftM) import Data.List (foldl', groupBy) import Data.Function (on) import Control.Monad.Reader (ask, asks) import Control.Monad.State (get, put) import qualified Data.Map.Lazy as Map import System.FilePath (()) import System.Directory ( getDirectoryContents , createDirectoryIfMissing ) import Control.Applicative ((<$>)) import qualified Data.ByteString.Lazy as BSL import System.IO (withFile, IOMode (..), hPutStr) import System.IO.Unsafe (unsafeInterleaveIO) import Text.CSV (printCSV) import Control.Exception (evaluate) import Data.Acid (Update, Query, makeAcidic) import Data.SafeCopy (base, deriveSafeCopy, safeGet, safePut) import Data.Serialize.Get (runGetLazy) import Data.Serialize.Put (runPutLazy) import Distribution.Package ( PackageId , PackageName , packageName , packageVersion ) import Distribution.Text (simpleParse, display) import Distribution.Simple.Utils (writeFileAtomic) import Distribution.Server.Framework.Instances () import Distribution.Server.Framework.MemSize import Distribution.Server.Util.CountingMap {------------------------------------------------------------------------------ Data types ------------------------------------------------------------------------------} data InMemStats = InMemStats { inMemToday :: !Day , inMemCounts :: !(SimpleCountingMap PackageId) } deriving (Show, Eq, Typeable) newtype OnDiskStats = OnDiskStats { onDiskStats :: NestedCountingMap PackageName OnDiskPerPkg } deriving (Show, Eq, MemSize) instance CountingMap (PackageName, (Day, Version)) OnDiskStats where cmEmpty = OnDiskStats $ cmEmpty cmTotal (OnDiskStats ncm) = cmTotal ncm cmInsert kl n (OnDiskStats ncm) = OnDiskStats $ cmInsert kl n ncm cmFind k (OnDiskStats ncm) = cmFind k ncm cmUnion (OnDiskStats a) (OnDiskStats b) = OnDiskStats (cmUnion a b) cmToList (OnDiskStats ncm) = cmToList ncm cmToCSV (OnDiskStats ncm) = cmToCSV ncm cmInsertRecord r (OnDiskStats ncm) = first OnDiskStats `liftM` cmInsertRecord r ncm newtype OnDiskPerPkg = OnDiskPerPkg { onDiskPerPkgCounts :: NestedCountingMap Day (SimpleCountingMap Version) } deriving (Show, Eq, Ord, MemSize) instance CountingMap (Day, Version) OnDiskPerPkg where cmEmpty = OnDiskPerPkg $ cmEmpty cmTotal (OnDiskPerPkg ncm) = cmTotal ncm cmInsert kl n (OnDiskPerPkg ncm) = OnDiskPerPkg $ cmInsert kl n ncm cmFind k (OnDiskPerPkg ncm) = cmFind k ncm cmUnion (OnDiskPerPkg a) (OnDiskPerPkg b) = OnDiskPerPkg (cmUnion a b) cmToList (OnDiskPerPkg ncm) = cmToList ncm cmToCSV (OnDiskPerPkg ncm) = cmToCSV ncm cmInsertRecord r (OnDiskPerPkg ncm) = first OnDiskPerPkg `liftM` cmInsertRecord r ncm newtype RecentDownloads = RecentDownloads { recentDownloads :: SimpleCountingMap PackageName } deriving (Show, Eq, MemSize) instance CountingMap PackageName RecentDownloads where cmEmpty = RecentDownloads $ cmEmpty cmTotal (RecentDownloads ncm) = cmTotal ncm cmInsert kl n (RecentDownloads ncm) = RecentDownloads $ cmInsert kl n ncm cmFind k (RecentDownloads ncm) = cmFind k ncm cmUnion (RecentDownloads a) (RecentDownloads b) = RecentDownloads (cmUnion a b) cmToList (RecentDownloads ncm) = cmToList ncm cmToCSV (RecentDownloads ncm) = cmToCSV ncm cmInsertRecord r (RecentDownloads ncm) = first RecentDownloads `liftM` cmInsertRecord r ncm newtype TotalDownloads = TotalDownloads { totalDownloads :: SimpleCountingMap PackageName } deriving (Show, Eq, MemSize) instance CountingMap PackageName TotalDownloads where cmEmpty = TotalDownloads $ cmEmpty cmTotal (TotalDownloads ncm) = cmTotal ncm cmInsert kl n (TotalDownloads ncm) = TotalDownloads $ cmInsert kl n ncm cmFind k (TotalDownloads ncm) = cmFind k ncm cmUnion (TotalDownloads a) (TotalDownloads b) = TotalDownloads (cmUnion a b) cmToList (TotalDownloads ncm) = cmToList ncm cmToCSV (TotalDownloads ncm) = cmToCSV ncm cmInsertRecord r (TotalDownloads ncm) = first TotalDownloads `liftM` cmInsertRecord r ncm {------------------------------------------------------------------------------ Initial instances ------------------------------------------------------------------------------} initInMemStats :: Day -> InMemStats initInMemStats day = InMemStats { inMemToday = day , inMemCounts = cmEmpty } type DayRange = (Day, Day) initRecentAndTotalDownloads :: DayRange -> OnDiskStats -> (RecentDownloads, TotalDownloads) initRecentAndTotalDownloads dayRange (OnDiskStats (NCM _ m)) = foldl' (\(recent, total) (pname, pstats) -> let !recent' = accumRecentDownloads dayRange pname pstats recent !total' = accumTotalDownloads pname pstats total in (recent', total')) (emptyRecentDownloads, emptyTotalDownloads) (Map.toList m) emptyRecentDownloads :: RecentDownloads emptyRecentDownloads = RecentDownloads cmEmpty accumRecentDownloads :: DayRange -> PackageName -> OnDiskPerPkg -> RecentDownloads -> RecentDownloads accumRecentDownloads dayRange pkgName (OnDiskPerPkg (NCM _ perDay)) | let rangeTotal = sum (map cmTotal (lookupRange dayRange perDay)) , rangeTotal > 0 = cmInsert pkgName rangeTotal | otherwise = id lookupRange :: Ord k => (k,k) -> Map.Map k a -> [a] lookupRange (l,u) m = let (_,ml,above) = Map.splitLookup l m (middle,mu,_) = Map.splitLookup u above in maybe [] (\x->[x]) ml ++ Map.elems middle ++ maybe [] (\x->[x]) mu emptyTotalDownloads :: TotalDownloads emptyTotalDownloads = TotalDownloads cmEmpty accumTotalDownloads :: PackageName -> OnDiskPerPkg -> TotalDownloads -> TotalDownloads accumTotalDownloads pkgName (OnDiskPerPkg perPkg) = cmInsert pkgName (cmTotal perPkg) {------------------------------------------------------------------------------ Pure updates/queries ------------------------------------------------------------------------------} updateHistory :: InMemStats -> OnDiskStats -> OnDiskStats updateHistory (InMemStats day perPkg) (OnDiskStats (NCM _ m)) = OnDiskStats (NCM 0 (Map.unionWith cmUnion m updatesMap)) where updatesMap :: Map.Map PackageName OnDiskPerPkg updatesMap = Map.fromList [ (pkgname, applyUpdates pkgs) | pkgs <- groupBy ((==) `on` (packageName . fst)) (cmToList perPkg :: [(PackageId, Int)]) , let pkgname = packageName (fst (head pkgs)) ] applyUpdates :: [(PackageId, Int)] -> OnDiskPerPkg applyUpdates pkgs = foldr (.) id [ cmInsert (day, packageVersion pkgId) count | (pkgId, count) <- pkgs ] cmEmpty {------------------------------------------------------------------------------ MemSize ------------------------------------------------------------------------------} instance MemSize InMemStats where memSize (InMemStats a b) = memSize2 a b {------------------------------------------------------------------------------ Serializing on-disk stats ------------------------------------------------------------------------------} readOnDiskStats :: FilePath -> IO OnDiskStats readOnDiskStats stateDir = do createDirectoryIfMissing True stateDir pkgStrs <- getDirectoryContents stateDir OnDiskStats . NCM 0 . Map.fromList <$> sequence [ do onDiskPerPkg <- unsafeInterleaveIO $ either (const cmEmpty) id <$> readOnDiskPerPkg pkgFile return (pkgName, onDiskPerPkg) | Just pkgName <- map simpleParse pkgStrs , let pkgFile = stateDir display pkgName ] readOnDiskPerPkg :: FilePath -> IO (Either String OnDiskPerPkg) readOnDiskPerPkg pkgFile = withFile pkgFile ReadMode $ \h -> -- By evaluating the Either result from the parser we force -- all contents to be read evaluate =<< (runGetLazy safeGet <$> BSL.hGetContents h) writeOnDiskStats :: FilePath -> OnDiskStats -> IO () writeOnDiskStats stateDir (OnDiskStats (NCM _ onDisk)) = do createDirectoryIfMissing True stateDir forM_ (Map.toList onDisk) $ \(pkgName, perPkg) -> do let pkgFile = stateDir display pkgName writeFileAtomic pkgFile $ runPutLazy (safePut perPkg) {------------------------------------------------------------------------------ The append-only all-time log ------------------------------------------------------------------------------} appendToLog :: FilePath -> InMemStats -> IO () appendToLog stateDir (InMemStats _ inMemStats) = withFile (stateDir "log") AppendMode $ \h -> hPutStr h $ printCSV (cmToCSV inMemStats) reconstructLog :: FilePath -> OnDiskStats -> IO () reconstructLog stateDir onDisk = withFile (stateDir "log") WriteMode $ \h -> hPutStr h $ printCSV (cmToCSV onDisk) {------------------------------------------------------------------------------ ACID stuff ------------------------------------------------------------------------------} deriveSafeCopy 0 'base ''InMemStats deriveSafeCopy 0 'base ''OnDiskPerPkg getInMemStats :: Query InMemStats InMemStats getInMemStats = ask replaceInMemStats :: InMemStats -> Update InMemStats () replaceInMemStats = put recordedToday :: Query InMemStats Day recordedToday = asks inMemToday registerDownload :: PackageId -> Update InMemStats () registerDownload pkgId = do InMemStats day counts <- get put $ InMemStats day (cmInsert pkgId 1 counts) makeAcidic ''InMemStats [ 'getInMemStats , 'replaceInMemStats , 'recordedToday , 'registerDownload ]