diff --git a/Makefile.am b/Makefile.am index 2f95d16ebd1ce72db2f853e0930e0c2c07a756cd..cd76ac70d9248490ac8e75f307f907640f69cb94 100644 --- a/Makefile.am +++ b/Makefile.am @@ -483,6 +483,7 @@ HS_LIB_SRCS = \ htools/Ganeti/HTools/Program/Hspace.hs \ htools/Ganeti/HTools/Types.hs \ htools/Ganeti/Hash.hs \ + htools/Ganeti/JQueue.hs \ htools/Ganeti/JSON.hs \ htools/Ganeti/Jobs.hs \ htools/Ganeti/Logging.hs \ @@ -530,6 +531,7 @@ HS_TEST_SRCS = \ htest/Test/Ganeti/HTools/Types.hs \ htest/Test/Ganeti/JSON.hs \ htest/Test/Ganeti/Jobs.hs \ + htest/Test/Ganeti/JQueue.hs \ htest/Test/Ganeti/Luxi.hs \ htest/Test/Ganeti/Network.hs \ htest/Test/Ganeti/Objects.hs \ diff --git a/htest/Test/Ganeti/JQueue.hs b/htest/Test/Ganeti/JQueue.hs new file mode 100644 index 0000000000000000000000000000000000000000..29f525a356275e6a0fc1b9b704dddd6698556010 --- /dev/null +++ b/htest/Test/Ganeti/JQueue.hs @@ -0,0 +1,302 @@ +{-# LANGUAGE TemplateHaskell #-} + +{-| Unittests for the job queue functionality. + +-} + +{- + +Copyright (C) 2012 Google Inc. + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, but +WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +02110-1301, USA. + +-} + +module Test.Ganeti.JQueue (testJQueue) where + +import Control.Applicative +import Control.Monad (when) +import Data.Char (isAscii) +import Data.List (nub, sort) +import System.Directory +import System.FilePath +import System.IO.Temp +import System.Posix.Files +import Test.HUnit +import Test.QuickCheck as QuickCheck +import Test.QuickCheck.Monadic +import Text.JSON + +import Test.Ganeti.TestCommon +import Test.Ganeti.TestHelper +import Test.Ganeti.Types () +import Test.Ganeti.OpCodes + +import Ganeti.BasicTypes +import qualified Ganeti.Constants as C +import Ganeti.JQueue +import Ganeti.OpCodes +import Ganeti.Path +import Ganeti.Types as Types + +{-# ANN module "HLint: ignore Use camelCase" #-} + +-- * Helpers + +-- | noTimestamp in Just form. +justNoTs :: Maybe Timestamp +justNoTs = Just noTimestamp + +-- | Generates a simple queued opcode. +genQueuedOpCode :: Gen QueuedOpCode +genQueuedOpCode = + QueuedOpCode <$> pure (ValidOpCode $ wrapOpCode OpClusterQuery) <*> + arbitrary <*> pure JSNull <*> pure [] <*> + choose (C.opPrioLowest, C.opPrioHighest) <*> + pure justNoTs <*> pure justNoTs <*> pure justNoTs + +-- | Generates an static, empty job. +emptyJob :: (Monad m) => m QueuedJob +emptyJob = do + jid0 <- makeJobId 0 + return $ QueuedJob jid0 [] justNoTs justNoTs justNoTs + +-- | Generates a job ID. +genJobId :: Gen JobId +genJobId = do + p <- arbitrary::Gen (Types.NonNegative Int) + makeJobId $ fromNonNegative p + +-- * Test cases + +-- | Tests default priority value. +case_JobPriorityDef :: Assertion +case_JobPriorityDef = do + ej <- emptyJob + assertEqual "for default priority" C.opPrioDefault $ calcJobPriority ej + +-- | Test arbitrary priorities. +prop_JobPriority :: Property +prop_JobPriority = + forAll (listOf1 (genQueuedOpCode `suchThat` + (not . opStatusFinalized . qoStatus))) $ \ops -> do + jid0 <- makeJobId 0 + let job = QueuedJob jid0 ops justNoTs justNoTs justNoTs + calcJobPriority job ==? minimum (map qoPriority ops) + +-- | Tests default job status. +case_JobStatusDef :: Assertion +case_JobStatusDef = do + ej <- emptyJob + assertEqual "for job status" JOB_STATUS_SUCCESS $ calcJobStatus ej + +-- | Test some job status properties. +prop_JobStatus :: Property +prop_JobStatus = + forAll genJobId $ \jid -> + forAll genQueuedOpCode $ \op -> + let job1 = QueuedJob jid [op] justNoTs justNoTs justNoTs + st1 = calcJobStatus job1 + op_succ = op { qoStatus = OP_STATUS_SUCCESS } + op_err = op { qoStatus = OP_STATUS_ERROR } + op_cnl = op { qoStatus = OP_STATUS_CANCELING } + op_cnd = op { qoStatus = OP_STATUS_CANCELED } + -- computes status for a job with an added opcode before + st_pre_op pop = calcJobStatus (job1 { qjOps = pop:qjOps job1 }) + -- computes status for a job with an added opcode after + st_post_op pop = calcJobStatus (job1 { qjOps = qjOps job1 ++ [pop] }) + in conjoin + [ printTestCase "pre-success doesn't change status" + (st_pre_op op_succ ==? st1) + , printTestCase "post-success doesn't change status" + (st_post_op op_succ ==? st1) + , printTestCase "pre-error is error" + (st_pre_op op_err ==? JOB_STATUS_ERROR) + , printTestCase "pre-canceling is canceling" + (st_pre_op op_cnl ==? JOB_STATUS_CANCELING) + , printTestCase "pre-canceled is canceled" + (st_pre_op op_cnd ==? JOB_STATUS_CANCELED) + ] + +-- | Tests job status equivalence with Python. Very similar to OpCodes test. +case_JobStatusPri_py_equiv :: Assertion +case_JobStatusPri_py_equiv = do + let num_jobs = 2000::Int + sample_jobs <- sample' (vectorOf num_jobs $ do + num_ops <- choose (1, 5) + ops <- vectorOf num_ops genQueuedOpCode + jid <- genJobId + return $ QueuedJob jid ops justNoTs justNoTs + justNoTs) + let jobs = head sample_jobs + serialized = encode jobs + -- check for non-ASCII fields, usually due to 'arbitrary :: String' + mapM_ (\job -> when (any (not . isAscii) (encode job)) . + assertFailure $ "Job has non-ASCII fields: " ++ show job + ) jobs + py_stdout <- + runPython "from ganeti import jqueue\n\ + \from ganeti import serializer\n\ + \import sys\n\ + \job_data = serializer.Load(sys.stdin.read())\n\ + \decoded = [jqueue._QueuedJob.Restore(None, o, False, False)\n\ + \ for o in job_data]\n\ + \encoded = [(job.CalcStatus(), job.CalcPriority())\n\ + \ for job in decoded]\n\ + \print serializer.Dump(encoded)" serialized + >>= checkPythonResult + let deserialised = decode py_stdout::Text.JSON.Result [(String, Int)] + decoded <- case deserialised of + Text.JSON.Ok jobs' -> return jobs' + Error msg -> + assertFailure ("Unable to decode jobs: " ++ msg) + -- this already raised an expection, but we need it + -- for proper types + >> fail "Unable to decode jobs" + assertEqual "Mismatch in number of returned jobs" + (length decoded) (length jobs) + mapM_ (\(py_sp, job) -> + let hs_sp = (jobStatusToRaw $ calcJobStatus job, + calcJobPriority job) + in assertEqual ("Different result after encoding/decoding for " ++ + show job) py_sp hs_sp + ) $ zip decoded jobs + +-- | Tests listing of Job ids. +prop_ListJobIDs :: Property +prop_ListJobIDs = monadicIO $ do + jobs <- pick $ resize 10 (listOf1 genJobId `suchThat` (\l -> l == nub l)) + (e, f, g) <- + run . withSystemTempDirectory "jqueue-test." $ \tempdir -> do + empty_dir <- getJobIDs [tempdir] + mapM_ (\jid -> writeFile (tempdir </> jobFileName jid) "") jobs + full_dir <- getJobIDs [tempdir] + invalid_dir <- getJobIDs [tempdir </> "no-such-dir"] + return (empty_dir, sortJobIDs full_dir, invalid_dir) + stop $ conjoin [ printTestCase "empty directory" $ e ==? [] + , printTestCase "directory with valid names" $ + f ==? sortJobIDs jobs + , printTestCase "invalid directory" $ g ==? [] + ] + +-- | Tests loading jobs from disk. +prop_LoadJobs :: Property +prop_LoadJobs = monadicIO $ do + ops <- pick $ resize 5 (listOf1 genQueuedOpCode) + jid <- pick genJobId + let job = QueuedJob jid ops justNoTs justNoTs justNoTs + job_s = encode job + -- check that jobs in the right directories are parsed correctly + (missing, current, archived, missing_current, broken) <- + run . withSystemTempDirectory "jqueue-test." $ \tempdir -> do + let load a = loadJobFromDisk tempdir a jid + live_path = liveJobFile tempdir jid + arch_path = archivedJobFile tempdir jid + createDirectory $ tempdir </> jobQueueArchiveSubDir + createDirectory $ dropFileName arch_path + -- missing job + missing <- load True + writeFile live_path job_s + -- this should exist + current <- load False + removeFile live_path + writeFile arch_path job_s + -- this should exist (archived) + archived <- load True + -- this should be missing + missing_current <- load False + removeFile arch_path + writeFile live_path "invalid job" + broken <- load True + return (missing, current, archived, missing_current, broken) + stop $ conjoin [ missing ==? noSuchJob + , current ==? Ganeti.BasicTypes.Ok (job, False) + , archived ==? Ganeti.BasicTypes.Ok (job, True) + , missing_current ==? noSuchJob + , printTestCase "broken job" (isBad broken) + ] + +-- | Tests computing job directories. Creates random directories, +-- files and stale symlinks in a directory, and checks that we return +-- \"the right thing\". +prop_DetermineDirs :: Property +prop_DetermineDirs = monadicIO $ do + count <- pick $ choose (2, 10) + nums <- pick $ genUniquesList count + (arbitrary::Gen (QuickCheck.Positive Int)) + let (valid, invalid) = splitAt (count `div` 2) $ + map (\(QuickCheck.Positive i) -> show i) nums + (tempdir, non_arch, with_arch, invalid_root) <- + run . withSystemTempDirectory "jqueue-test." $ \tempdir -> do + let arch_dir = tempdir </> jobQueueArchiveSubDir + createDirectory arch_dir + mapM_ (createDirectory . (arch_dir </>)) valid + mapM_ (\p -> writeFile (arch_dir </> p) "") invalid + mapM_ (\p -> createSymbolicLink "/dev/null/no/such/file" + (arch_dir </> p <.> "missing")) invalid + non_arch <- determineJobDirectories tempdir False + with_arch <- determineJobDirectories tempdir True + invalid_root <- determineJobDirectories (tempdir </> "no-such-subdir") True + return (tempdir, non_arch, with_arch, invalid_root) + let arch_dir = tempdir </> jobQueueArchiveSubDir + stop $ conjoin [ non_arch ==? [tempdir] + , sort with_arch ==? sort (tempdir:map (arch_dir </>) valid) + , invalid_root ==? [tempdir </> "no-such-subdir"] + ] + +-- | Tests the JSON serialisation for 'InputOpCode'. +prop_InputOpCode :: MetaOpCode -> Int -> Property +prop_InputOpCode meta i = + conjoin [ readJSON (showJSON valid) ==? Text.JSON.Ok valid + , readJSON (showJSON invalid) ==? Text.JSON.Ok invalid + ] + where valid = ValidOpCode meta + invalid = InvalidOpCode (showJSON i) + +-- | Tests 'extractOpSummary'. +prop_extractOpSummary :: MetaOpCode -> Int -> Property +prop_extractOpSummary meta i = + conjoin [ printTestCase "valid opcode" $ + extractOpSummary (ValidOpCode meta) ==? summary + , printTestCase "invalid opcode, correct object" $ + extractOpSummary (InvalidOpCode jsobj) ==? summary + , printTestCase "invalid opcode, empty object" $ + extractOpSummary (InvalidOpCode emptyo) ==? invalid + , printTestCase "invalid opcode, object with invalid OP_ID" $ + extractOpSummary (InvalidOpCode invobj) ==? invalid + , printTestCase "invalid opcode, not jsobject" $ + extractOpSummary (InvalidOpCode jsinval) ==? invalid + ] + where summary = opSummary (metaOpCode meta) + jsobj = showJSON $ toJSObject [("OP_ID", + showJSON ("OP_" ++ summary))] + emptyo = showJSON $ toJSObject ([]::[(String, JSValue)]) + invobj = showJSON $ toJSObject [("OP_ID", showJSON False)] + jsinval = showJSON i + invalid = "INVALID_OP" + +testSuite "JQueue" + [ 'case_JobPriorityDef + , 'prop_JobPriority + , 'case_JobStatusDef + , 'prop_JobStatus + , 'case_JobStatusPri_py_equiv + , 'prop_ListJobIDs + , 'prop_LoadJobs + , 'prop_DetermineDirs + , 'prop_InputOpCode + , 'prop_extractOpSummary + ] diff --git a/htest/test.hs b/htest/test.hs index 3bb1294c6ef0f0337c8cf3b2f3cb079c6fe6ca76..d7848aa04f2b6793b9275a388c6f144142b6fd15 100644 --- a/htest/test.hs +++ b/htest/test.hs @@ -53,6 +53,7 @@ import Test.Ganeti.HTools.PeerMap import Test.Ganeti.HTools.Types import Test.Ganeti.JSON import Test.Ganeti.Jobs +import Test.Ganeti.JQueue import Test.Ganeti.Luxi import Test.Ganeti.Network import Test.Ganeti.Objects @@ -103,6 +104,7 @@ allTests = , testHTools_Types , testJSON , testJobs + , testJQueue , testLuxi , testNetwork , testObjects diff --git a/htools/Ganeti/JQueue.hs b/htools/Ganeti/JQueue.hs new file mode 100644 index 0000000000000000000000000000000000000000..39aa3fc604009f9354af9a726f8422fe428d3976 --- /dev/null +++ b/htools/Ganeti/JQueue.hs @@ -0,0 +1,282 @@ +{-# LANGUAGE TemplateHaskell #-} + +{-| Implementation of the job queue. + +-} + +{- + +Copyright (C) 2010, 2012 Google Inc. + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, but +WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +02110-1301, USA. + +-} + +module Ganeti.JQueue + ( QueuedOpCode(..) + , QueuedJob(..) + , InputOpCode(..) + , Timestamp + , noTimestamp + , opStatusFinalized + , extractOpSummary + , calcJobStatus + , calcJobPriority + , jobFileName + , liveJobFile + , archivedJobFile + , determineJobDirectories + , getJobIDs + , sortJobIDs + , loadJobFromDisk + , noSuchJob + ) where + +import Control.Exception +import Control.Monad +import Data.List +import Data.Ord (comparing) +-- workaround what seems to be a bug in ghc 7.4's TH shadowing code +import Prelude hiding (log, id) +import System.Directory +import System.FilePath +import System.IO.Error (isDoesNotExistError) +import System.Posix.Files +import qualified Text.JSON +import Text.JSON.Types + +import Ganeti.BasicTypes +import qualified Ganeti.Constants as C +import Ganeti.JSON +import Ganeti.Logging +import Ganeti.OpCodes +import Ganeti.Path +import Ganeti.THH +import Ganeti.Types + +-- * Data types + +-- | The ganeti queue timestamp type +type Timestamp = (Int, Int) + +-- | Missing timestamp type. +noTimestamp :: Timestamp +noTimestamp = (-1, -1) + +-- | An input opcode. +data InputOpCode = ValidOpCode MetaOpCode -- ^ OpCode was parsed successfully + | InvalidOpCode JSValue -- ^ Invalid opcode + deriving (Show, Eq) + +-- | JSON instance for 'InputOpCode', trying to parse it and if +-- failing, keeping the original JSValue. +instance Text.JSON.JSON InputOpCode where + showJSON (ValidOpCode mo) = Text.JSON.showJSON mo + showJSON (InvalidOpCode inv) = inv + readJSON v = case Text.JSON.readJSON v of + Text.JSON.Error _ -> return $ InvalidOpCode v + Text.JSON.Ok mo -> return $ ValidOpCode mo + +-- | Invalid opcode summary. +invalidOp :: String +invalidOp = "INVALID_OP" + +-- | Tries to extract the opcode summary from an 'InputOpCode'. This +-- duplicates some functionality from the 'opSummary' function in +-- "Ganeti.OpCodes". +extractOpSummary :: InputOpCode -> String +extractOpSummary (ValidOpCode metaop) = opSummary $ metaOpCode metaop +extractOpSummary (InvalidOpCode (JSObject o)) = + case fromObjWithDefault (fromJSObject o) "OP_ID" ("OP_" ++ invalidOp) of + Just s -> drop 3 s -- drop the OP_ prefix + Nothing -> invalidOp +extractOpSummary _ = invalidOp + +$(buildObject "QueuedOpCode" "qo" + [ simpleField "input" [t| InputOpCode |] + , simpleField "status" [t| OpStatus |] + , simpleField "result" [t| JSValue |] + , defaultField [| [] |] $ + simpleField "log" [t| [(Int, Timestamp, ELogType, JSValue)] |] + , simpleField "priority" [t| Int |] + , optionalNullSerField $ + simpleField "start_timestamp" [t| Timestamp |] + , optionalNullSerField $ + simpleField "exec_timestamp" [t| Timestamp |] + , optionalNullSerField $ + simpleField "end_timestamp" [t| Timestamp |] + ]) + +$(buildObject "QueuedJob" "qj" + [ simpleField "id" [t| JobId |] + , simpleField "ops" [t| [QueuedOpCode] |] + , optionalNullSerField $ + simpleField "received_timestamp" [t| Timestamp |] + , optionalNullSerField $ + simpleField "start_timestamp" [t| Timestamp |] + , optionalNullSerField $ + simpleField "end_timestamp" [t| Timestamp |] + ]) + +-- | Job file prefix. +jobFilePrefix :: String +jobFilePrefix = "job-" + +-- | Computes the filename for a given job ID. +jobFileName :: JobId -> FilePath +jobFileName jid = jobFilePrefix ++ show (fromJobId jid) + +-- | Parses a job ID from a file name. +parseJobFileId :: (Monad m) => FilePath -> m JobId +parseJobFileId path = + case stripPrefix jobFilePrefix path of + Nothing -> fail $ "Job file '" ++ path ++ + "' doesn't have the correct prefix" + Just suffix -> makeJobIdS suffix + +-- | Computes the full path to a live job. +liveJobFile :: FilePath -> JobId -> FilePath +liveJobFile rootdir jid = rootdir </> jobFileName jid + +-- | Computes the full path to an archives job. BROKEN. +archivedJobFile :: FilePath -> JobId -> FilePath +archivedJobFile rootdir jid = + let subdir = show (fromJobId jid `div` C.jstoreJobsPerArchiveDirectory) + in rootdir </> jobQueueArchiveSubDir </> subdir </> jobFileName jid + +-- | Map from opcode status to job status. +opStatusToJob :: OpStatus -> JobStatus +opStatusToJob OP_STATUS_QUEUED = JOB_STATUS_QUEUED +opStatusToJob OP_STATUS_WAITING = JOB_STATUS_WAITING +opStatusToJob OP_STATUS_SUCCESS = JOB_STATUS_SUCCESS +opStatusToJob OP_STATUS_RUNNING = JOB_STATUS_RUNNING +opStatusToJob OP_STATUS_CANCELING = JOB_STATUS_CANCELING +opStatusToJob OP_STATUS_CANCELED = JOB_STATUS_CANCELED +opStatusToJob OP_STATUS_ERROR = JOB_STATUS_ERROR + +-- | Computes a queued job's status. +calcJobStatus :: QueuedJob -> JobStatus +calcJobStatus QueuedJob { qjOps = ops } = + extractOpSt (map qoStatus ops) JOB_STATUS_QUEUED True + where + terminalStatus OP_STATUS_ERROR = True + terminalStatus OP_STATUS_CANCELING = True + terminalStatus OP_STATUS_CANCELED = True + terminalStatus _ = False + softStatus OP_STATUS_SUCCESS = True + softStatus OP_STATUS_QUEUED = True + softStatus _ = False + extractOpSt [] _ True = JOB_STATUS_SUCCESS + extractOpSt [] d False = d + extractOpSt (x:xs) d old_all + | terminalStatus x = opStatusToJob x -- abort recursion + | softStatus x = extractOpSt xs d new_all -- continue unchanged + | otherwise = extractOpSt xs (opStatusToJob x) new_all + where new_all = x == OP_STATUS_SUCCESS && old_all + +-- | Determine whether an opcode status is finalized. +opStatusFinalized :: OpStatus -> Bool +opStatusFinalized = (> OP_STATUS_RUNNING) + +-- | Compute a job's priority. +calcJobPriority :: QueuedJob -> Int +calcJobPriority QueuedJob { qjOps = ops } = + helper . map qoPriority $ filter (not . opStatusFinalized . qoStatus) ops + where helper [] = C.opPrioDefault + helper ps = minimum ps + +-- | Log but ignore an 'IOError'. +ignoreIOError :: a -> Bool -> String -> IOError -> IO a +ignoreIOError a ignore_noent msg e = do + unless (isDoesNotExistError e && ignore_noent) . + logWarning $ msg ++ ": " ++ show e + return a + +-- | Compute the list of existing archive directories. Note that I/O +-- exceptions are swallowed and ignored. +allArchiveDirs :: FilePath -> IO [FilePath] +allArchiveDirs rootdir = do + let adir = rootdir </> jobQueueArchiveSubDir + contents <- getDirectoryContents adir `Control.Exception.catch` + ignoreIOError [] False + ("Failed to list queue directory " ++ adir) + let fpaths = map (adir </>) $ filter (not . ("." `isPrefixOf`)) contents + filterM (\path -> + liftM isDirectory (getFileStatus (adir </> path)) + `Control.Exception.catch` + ignoreIOError False True + ("Failed to stat archive path " ++ path)) fpaths + +-- | Build list of directories containing job files. Note: compared to +-- the Python version, this doesn't ignore a potential lost+found +-- file. +determineJobDirectories :: FilePath -> Bool -> IO [FilePath] +determineJobDirectories rootdir archived = do + other <- if archived + then allArchiveDirs rootdir + else return [] + return $ rootdir:other + +-- | Computes the list of all jobs in the given directories. +getJobIDs :: [FilePath] -> IO [JobId] +getJobIDs = liftM concat . mapM getDirJobIDs + +-- | Sorts the a list of job IDs. +sortJobIDs :: [JobId] -> [JobId] +sortJobIDs = sortBy (comparing fromJobId) + +-- | Computes the list of jobs in a given directory. +getDirJobIDs :: FilePath -> IO [JobId] +getDirJobIDs path = do + contents <- getDirectoryContents path `Control.Exception.catch` + ignoreIOError [] False + ("Failed to list job directory " ++ path) + let jids = foldl (\ids file -> + case parseJobFileId file of + Nothing -> ids + Just new_id -> new_id:ids) [] contents + return $ reverse jids + +-- | Reads the job data from disk. +readJobDataFromDisk :: FilePath -> Bool -> JobId -> IO (Maybe (String, Bool)) +readJobDataFromDisk rootdir archived jid = do + let live_path = liveJobFile rootdir jid + archived_path = archivedJobFile rootdir jid + all_paths = if archived + then [(live_path, False), (archived_path, True)] + else [(live_path, False)] + foldM (\state (path, isarchived) -> + liftM (\r -> Just (r, isarchived)) (readFile path) + `Control.Exception.catch` + ignoreIOError state True + ("Failed to read job file " ++ path)) Nothing all_paths + +-- | Failed to load job error. +noSuchJob :: Result (QueuedJob, Bool) +noSuchJob = Bad "Can't load job file" + +-- | Loads a job from disk. +loadJobFromDisk :: FilePath -> Bool -> JobId -> IO (Result (QueuedJob, Bool)) +loadJobFromDisk rootdir archived jid = do + raw <- readJobDataFromDisk rootdir archived jid + -- note: we need some stricness below, otherwise the wrapping in a + -- Result will create too much lazyness, and not close the file + -- descriptors for the individual jobs + return $! case raw of + Nothing -> noSuchJob + Just (str, arch) -> + liftM (\qj -> (qj, arch)) . + fromJResult "Parsing job file" $ Text.JSON.decode str