Skip to content

Commit 3754bdf

Browse files
authored
Merge pull request #544 from bgamari/doc-builder-retry
BuildClient: Add simple retry policy for failed doc builds
2 parents 14f0f04 + 1c1472c commit 3754bdf

File tree

2 files changed

+63
-14
lines changed

2 files changed

+63
-14
lines changed

BuildClient.hs

Lines changed: 62 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import Distribution.Client.Cron (cron, rethrowSignalsAsExceptions,
1111

1212
import Distribution.Package
1313
import Distribution.Text
14+
import qualified Text.PrettyPrint as Disp
1415
import Distribution.Verbosity
1516
import Distribution.Simple.Utils hiding (intercalate)
1617
import Distribution.Version (Version(..))
@@ -19,10 +20,12 @@ import Data.List
1920
import Data.Maybe
2021
import Data.IORef
2122
import Data.Time
23+
import Control.Applicative ((<$>), (<*>))
2224
import Control.Exception
2325
import Control.Monad
2426
import Control.Monad.Trans
2527
import qualified Data.ByteString.Lazy as BS
28+
import qualified Data.Map as M
2629
import qualified Data.Set as S
2730

2831
import qualified Codec.Compression.GZip as GZip
@@ -55,7 +58,9 @@ data BuildOpts = BuildOpts {
5558
bo_dryRun :: Bool,
5659
bo_prune :: Bool,
5760
bo_username :: Maybe String,
58-
bo_password :: Maybe String
61+
bo_password :: Maybe String,
62+
bo_buildAttempts :: Int
63+
-- ^ how many times to attempt to rebuild a failing package
5964
}
6065

6166
data BuildConfig = BuildConfig {
@@ -134,6 +139,23 @@ initialise opts uri auxUris
134139
where
135140
readMissingOpt prompt = maybe (putStrLn prompt >> getLine) return
136141

142+
143+
-- | Parse the @00-index.cache@ file of the available package repositories.
144+
parseRepositoryIndices :: IO (S.Set PackageIdentifier)
145+
parseRepositoryIndices = do
146+
cabalDir <- getAppUserDataDirectory "cabal/packages"
147+
cacheDirs <- listDirectory cabalDir
148+
indexFiles <- filterM doesFileExist $ map (\dir -> cabalDir </> dir </> "00-index.cache") cacheDirs
149+
S.unions <$> mapM readCache indexFiles
150+
where
151+
readCache fname =
152+
S.fromList . mapMaybe parseLine . lines <$> readFile fname
153+
parseLine line
154+
| "pkg:" : name : ver : _ <- words line
155+
= PackageIdentifier <$> simpleParse name <*> simpleParse ver
156+
| otherwise
157+
= Nothing
158+
137159
writeConfig :: BuildOpts -> BuildConfig -> IO ()
138160
writeConfig opts BuildConfig {
139161
bc_srcURI = uri,
@@ -388,6 +410,11 @@ getDocumentationStats verbosity config didFail = do
388410
buildOnce :: BuildOpts -> [PackageId] -> IO ()
389411
buildOnce opts pkgs = keepGoing $ do
390412
config <- readConfig opts
413+
-- Due to caching sometimes the package repository state may lag behind the
414+
-- documentation index. Consequently, we make sure that the packages we are
415+
-- going to build actually appear in the repository before building. See
416+
-- #543.
417+
repoIndex <- parseRepositoryIndices
391418

392419
notice verbosity "Initialising"
393420
(has_failed, mark_as_failed, persist_failed) <- mkPackageFailed opts
@@ -406,6 +433,7 @@ buildOnce opts pkgs = keepGoing $ do
406433
-- Find those files *not* marked as having documentation in our cache
407434
let toBuild :: [DocInfo]
408435
toBuild = filter shouldBuild
436+
. filter (flip S.member repoIndex . docInfoPackage)
409437
. latestFirst
410438
. map (sortBy (flip (comparing docInfoPackageVersion)))
411439
. groupBy (equating docInfoPackageName)
@@ -486,32 +514,44 @@ buildOnce opts pkgs = keepGoing $ do
486514
unless (update_ec == ExitSuccess) $
487515
die "Could not 'cabal update' from specified server"
488516

489-
490-
-- Builds a little memoised function that can tell us whether a
491-
-- particular package failed to build its documentation
517+
-- | Builds a little memoised function that can tell us whether a
518+
-- particular package failed to build its documentation, a function to mark a
519+
-- package as having failed, and a function to write the final failed list back
520+
-- to disk.
492521
mkPackageFailed :: BuildOpts
493522
-> IO (PackageId -> IO Bool, PackageId -> IO (), IO ())
494523
mkPackageFailed opts = do
495524
init_failed <- readFailedCache (bo_stateDir opts)
496525
cache_var <- newIORef init_failed
497526

498527
let mark_as_failed pkg_id = atomicModifyIORef cache_var $ \already_failed ->
499-
(S.insert pkg_id already_failed, ())
500-
has_failed pkg_id = liftM (pkg_id `S.member`) $ readIORef cache_var
528+
(M.insertWith (+) pkg_id 1 already_failed, ())
529+
has_failed pkg_id = f <$> readIORef cache_var
530+
where f cache = M.findWithDefault 0 pkg_id cache > bo_buildAttempts opts
501531
persist = readIORef cache_var >>= writeFailedCache (bo_stateDir opts)
502532

503533
return (has_failed, mark_as_failed, persist)
504534
where
505-
readFailedCache :: FilePath -> IO (S.Set PackageId)
535+
readFailedCache :: FilePath -> IO (M.Map PackageId Int)
506536
readFailedCache cache_dir = do
507537
pkgstrs <- handleDoesNotExist [] $ liftM lines $ readFile (cache_dir </> "failed")
508-
case validatePackageIds pkgstrs of
538+
let (pkgids, attempts) = unzip $ map (parseLine . words) pkgstrs
539+
where
540+
parseLine [pkg_id] = (pkg_id, 1)
541+
parseLine [pkg_id, attempts']
542+
| [(n,_)] <- reads attempts' = (pkg_id, n)
543+
| otherwise = (pkg_id, 1)
544+
parseLine other = error $ "failed to parse failed list line: "++show other
545+
case validatePackageIds pkgids of
509546
Left theError -> die theError
510-
Right pkgs -> return (S.fromList pkgs)
547+
Right pkgs -> return (M.fromList $ zip pkgs attempts)
511548

512-
writeFailedCache :: FilePath -> S.Set PackageId -> IO ()
549+
writeFailedCache :: FilePath -> M.Map PackageId Int -> IO ()
513550
writeFailedCache cache_dir pkgs =
514-
writeFile (cache_dir </> "failed") $ unlines $ map display $ S.toList pkgs
551+
writeFile (cache_dir </> "failed")
552+
$ unlines
553+
$ map (\(pkgid,n) -> show $ disp pkgid Disp.<+> disp n)
554+
$ M.assocs pkgs
515555

516556

517557
-- | Build documentation and return @(Just tgz)@ for the built tgz file
@@ -778,7 +818,8 @@ data BuildFlags = BuildFlags {
778818
flagInterval :: Maybe String,
779819
flagPrune :: Bool,
780820
flagUsername :: Maybe String,
781-
flagPassword :: Maybe String
821+
flagPassword :: Maybe String,
822+
flagBuildAttempts :: Maybe Int
782823
}
783824

784825
emptyBuildFlags :: BuildFlags
@@ -795,6 +836,7 @@ emptyBuildFlags = BuildFlags {
795836
, flagPrune = False
796837
, flagUsername = Nothing
797838
, flagPassword = Nothing
839+
, flagBuildAttempts = Nothing
798840
}
799841

800842
buildFlagDescrs :: [OptDescr (BuildFlags -> BuildFlags)]
@@ -848,6 +890,12 @@ buildFlagDescrs =
848890
, Option [] ["init-password"]
849891
(ReqArg (\passwd opts -> opts { flagPassword = Just passwd }) "PASSWORD")
850892
"The password of the Hackage user to run the build as (used with init)"
893+
894+
, Option [] ["build-attempts"]
895+
(ReqArg (\attempts opts -> case reads attempts of
896+
[(attempts', "")] -> opts { flagBuildAttempts = Just attempts' }
897+
_ -> error "Can't parse attempt count") "ATTEMPTS")
898+
"How many times to attempt to build a package before giving up"
851899
]
852900

853901
validateOpts :: [String] -> IO (Mode, BuildOpts)
@@ -869,7 +917,8 @@ validateOpts args = do
869917
bo_dryRun = flagDryRun flags,
870918
bo_prune = flagPrune flags,
871919
bo_username = flagUsername flags,
872-
bo_password = flagPassword flags
920+
bo_password = flagPassword flags,
921+
bo_buildAttempts = fromMaybe 10 $ flagBuildAttempts flags
873922
}
874923

875924
mode = case args' of

hackage-server.cabal

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -431,7 +431,7 @@ executable hackage-build
431431
build-depends:
432432
base,
433433
containers, array, vector, bytestring, text, pretty,
434-
filepath, directory, process >= 1.0,
434+
filepath, directory >= 1.2.5, process >= 1.0,
435435
time,
436436
time-locale-compat >= 0.1.0.1,
437437
tar, zlib,

0 commit comments

Comments
 (0)