Statistics
| Branch: | Tag: | Revision:

root / src / Ganeti / JQScheduler.hs @ ea174b21

History | View | Annotate | Download (10.8 kB)

1 48e4da5c Klaus Aehlig
{-| Implementation of a reader for the job queue.
2 48e4da5c Klaus Aehlig
3 48e4da5c Klaus Aehlig
-}
4 48e4da5c Klaus Aehlig
5 48e4da5c Klaus Aehlig
{-
6 48e4da5c Klaus Aehlig
7 48e4da5c Klaus Aehlig
Copyright (C) 2013 Google Inc.
8 48e4da5c Klaus Aehlig
9 48e4da5c Klaus Aehlig
This program is free software; you can redistribute it and/or modify
10 48e4da5c Klaus Aehlig
it under the terms of the GNU General Public License as published by
11 48e4da5c Klaus Aehlig
the Free Software Foundation; either version 2 of the License, or
12 48e4da5c Klaus Aehlig
(at your option) any later version.
13 48e4da5c Klaus Aehlig
14 48e4da5c Klaus Aehlig
This program is distributed in the hope that it will be useful, but
15 48e4da5c Klaus Aehlig
WITHOUT ANY WARRANTY; without even the implied warranty of
16 48e4da5c Klaus Aehlig
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
17 48e4da5c Klaus Aehlig
General Public License for more details.
18 48e4da5c Klaus Aehlig
19 48e4da5c Klaus Aehlig
You should have received a copy of the GNU General Public License
20 48e4da5c Klaus Aehlig
along with this program; if not, write to the Free Software
21 48e4da5c Klaus Aehlig
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
22 48e4da5c Klaus Aehlig
02110-1301, USA.
23 48e4da5c Klaus Aehlig
24 48e4da5c Klaus Aehlig
-}
25 48e4da5c Klaus Aehlig
26 48e4da5c Klaus Aehlig
module Ganeti.JQScheduler
27 48e4da5c Klaus Aehlig
  ( JQStatus
28 48e4da5c Klaus Aehlig
  , emptyJQStatus
29 48e4da5c Klaus Aehlig
  , initJQScheduler
30 48e4da5c Klaus Aehlig
  , enqueueNewJobs
31 48e4da5c Klaus Aehlig
  ) where
32 48e4da5c Klaus Aehlig
33 48e4da5c Klaus Aehlig
import Control.Arrow
34 48e4da5c Klaus Aehlig
import Control.Concurrent
35 1c532d2d Klaus Aehlig
import Control.Exception
36 48e4da5c Klaus Aehlig
import Control.Monad
37 48e4da5c Klaus Aehlig
import Data.List
38 b81650b0 Klaus Aehlig
import Data.Maybe
39 48e4da5c Klaus Aehlig
import Data.IORef
40 ed6cf449 Klaus Aehlig
import System.INotify
41 48e4da5c Klaus Aehlig
42 48e4da5c Klaus Aehlig
import Ganeti.BasicTypes
43 48e4da5c Klaus Aehlig
import Ganeti.Constants as C
44 48e4da5c Klaus Aehlig
import Ganeti.JQueue as JQ
45 48e4da5c Klaus Aehlig
import Ganeti.Logging
46 48e4da5c Klaus Aehlig
import Ganeti.Path
47 48e4da5c Klaus Aehlig
import Ganeti.Types
48 48e4da5c Klaus Aehlig
import Ganeti.Utils
49 48e4da5c Klaus Aehlig
50 ed6cf449 Klaus Aehlig
data JobWithStat = JobWithStat { jINotify :: Maybe INotify
51 ed6cf449 Klaus Aehlig
                               , jStat :: FStat
52 ed6cf449 Klaus Aehlig
                               , jJob :: QueuedJob
53 ed6cf449 Klaus Aehlig
                               }
54 48e4da5c Klaus Aehlig
data Queue = Queue { qEnqueued :: [JobWithStat], qRunning :: [JobWithStat] }
55 48e4da5c Klaus Aehlig
56 48e4da5c Klaus Aehlig
{-| Representation of the job queue
57 48e4da5c Klaus Aehlig
58 48e4da5c Klaus Aehlig
We keep two lists of jobs (together with information about the last
59 48e4da5c Klaus Aehlig
fstat result observed): the jobs that are enqueued, but not yet handed
60 48e4da5c Klaus Aehlig
over for execution, and the jobs already handed over for execution. They
61 48e4da5c Klaus Aehlig
are kept together in a single IORef, so that we can atomically update
62 48e4da5c Klaus Aehlig
both, in particular when scheduling jobs to be handed over for execution.
63 48e4da5c Klaus Aehlig
64 48e4da5c Klaus Aehlig
-}
65 48e4da5c Klaus Aehlig
66 48e4da5c Klaus Aehlig
data JQStatus = JQStatus
67 48e4da5c Klaus Aehlig
  { jqJobs :: IORef Queue
68 48e4da5c Klaus Aehlig
  }
69 48e4da5c Klaus Aehlig
70 48e4da5c Klaus Aehlig
71 48e4da5c Klaus Aehlig
emptyJQStatus :: IO JQStatus
72 48e4da5c Klaus Aehlig
emptyJQStatus = do
73 48e4da5c Klaus Aehlig
  jqJ <- newIORef Queue {qEnqueued=[], qRunning=[]}
74 48e4da5c Klaus Aehlig
  return JQStatus { jqJobs=jqJ }
75 48e4da5c Klaus Aehlig
76 48e4da5c Klaus Aehlig
-- | Apply a function on the running jobs.
77 48e4da5c Klaus Aehlig
onRunningJobs :: ([JobWithStat] -> [JobWithStat]) -> Queue -> Queue
78 48e4da5c Klaus Aehlig
onRunningJobs f queue = queue {qRunning=f $ qRunning queue}
79 48e4da5c Klaus Aehlig
80 48e4da5c Klaus Aehlig
-- | Apply a function on the queued jobs.
81 48e4da5c Klaus Aehlig
onQueuedJobs :: ([JobWithStat] -> [JobWithStat]) -> Queue -> Queue
82 48e4da5c Klaus Aehlig
onQueuedJobs f queue = queue {qEnqueued=f $ qEnqueued queue}
83 48e4da5c Klaus Aehlig
84 48e4da5c Klaus Aehlig
-- | Obtain a JobWithStat from a QueuedJob.
85 48e4da5c Klaus Aehlig
unreadJob :: QueuedJob -> JobWithStat
86 ed6cf449 Klaus Aehlig
unreadJob job = JobWithStat {jJob=job, jStat=nullFStat, jINotify=Nothing}
87 48e4da5c Klaus Aehlig
88 48e4da5c Klaus Aehlig
-- | Reload interval for polling the running jobs for updates in microseconds.
89 48e4da5c Klaus Aehlig
watchInterval :: Int
90 48e4da5c Klaus Aehlig
watchInterval = C.luxidJobqueuePollInterval * 1000000 
91 48e4da5c Klaus Aehlig
92 48e4da5c Klaus Aehlig
-- | Maximal number of jobs to be running at the same time.
93 48e4da5c Klaus Aehlig
maxRunningJobs :: Int
94 48e4da5c Klaus Aehlig
maxRunningJobs = C.luxidMaximalRunningJobs 
95 48e4da5c Klaus Aehlig
96 48e4da5c Klaus Aehlig
-- | Wrapper function to atomically update the jobs in the queue status.
97 48e4da5c Klaus Aehlig
modifyJobs :: JQStatus -> (Queue -> Queue) -> IO ()
98 48e4da5c Klaus Aehlig
modifyJobs qstat f = atomicModifyIORef (jqJobs qstat) (flip (,) ()  . f)
99 48e4da5c Klaus Aehlig
100 48e4da5c Klaus Aehlig
-- | Reread a job from disk, if the file has changed.
101 48e4da5c Klaus Aehlig
readJobStatus :: JobWithStat -> IO (Maybe JobWithStat)
102 ed6cf449 Klaus Aehlig
readJobStatus jWS@(JobWithStat {jStat=fstat, jJob=job})  = do
103 48e4da5c Klaus Aehlig
  let jid = qjId job
104 48e4da5c Klaus Aehlig
  qdir <- queueDir
105 48e4da5c Klaus Aehlig
  let fpath = liveJobFile qdir jid
106 48e4da5c Klaus Aehlig
  logDebug $ "Checking if " ++ fpath ++ " changed on disk."
107 350f0759 Klaus Aehlig
  changedResult <- try $ needsReload fstat fpath
108 350f0759 Klaus Aehlig
                   :: IO (Either IOError (Maybe FStat))
109 350f0759 Klaus Aehlig
  let changed = either (const $ Just nullFStat) id changedResult
110 48e4da5c Klaus Aehlig
  case changed of
111 48e4da5c Klaus Aehlig
    Nothing -> do
112 48e4da5c Klaus Aehlig
      logDebug $ "File " ++ fpath ++ " not changed on disk."
113 48e4da5c Klaus Aehlig
      return Nothing
114 48e4da5c Klaus Aehlig
    Just fstat' -> do
115 48e4da5c Klaus Aehlig
      let jids = show $ fromJobId jid
116 350f0759 Klaus Aehlig
      logInfo $ "Rereading job "  ++ jids
117 350f0759 Klaus Aehlig
      readResult <- loadJobFromDisk qdir True jid
118 48e4da5c Klaus Aehlig
      case readResult of
119 48e4da5c Klaus Aehlig
        Bad s -> do
120 48e4da5c Klaus Aehlig
          logWarning $ "Failed to read job " ++ jids ++ ": " ++ s
121 48e4da5c Klaus Aehlig
          return Nothing
122 48e4da5c Klaus Aehlig
        Ok (job', _) -> do
123 48e4da5c Klaus Aehlig
          logDebug
124 48e4da5c Klaus Aehlig
            $ "Read job " ++ jids ++ ", staus is " ++ show (calcJobStatus job')
125 ed6cf449 Klaus Aehlig
          return . Just $ jWS {jStat=fstat', jJob=job'}
126 ed6cf449 Klaus Aehlig
                          -- jINotify unchanged
127 48e4da5c Klaus Aehlig
128 48e4da5c Klaus Aehlig
-- | Update a job in the job queue, if it is still there. This is the
129 48e4da5c Klaus Aehlig
-- pure function for inserting a previously read change into the queue.
130 48e4da5c Klaus Aehlig
-- as the change contains its time stamp, we don't have to worry about a
131 48e4da5c Klaus Aehlig
-- later read change overwriting a newer read state. If this happens, the
132 48e4da5c Klaus Aehlig
-- fstat value will be outdated, so the next poller run will fix this.
133 48e4da5c Klaus Aehlig
updateJobStatus :: JobWithStat -> [JobWithStat] -> [JobWithStat]
134 48e4da5c Klaus Aehlig
updateJobStatus job' =
135 48e4da5c Klaus Aehlig
  let jid = qjId $ jJob job' in
136 48e4da5c Klaus Aehlig
  map (\job -> if qjId (jJob job) == jid then job' else job)
137 48e4da5c Klaus Aehlig
138 48e4da5c Klaus Aehlig
-- | Update a single job by reading it from disk, if necessary.
139 48e4da5c Klaus Aehlig
updateJob :: JQStatus -> JobWithStat -> IO ()
140 48e4da5c Klaus Aehlig
updateJob state jb = do
141 48e4da5c Klaus Aehlig
  jb' <- readJobStatus jb
142 48e4da5c Klaus Aehlig
  maybe (return ()) (modifyJobs state . onRunningJobs . updateJobStatus) jb'
143 ea174b21 Klaus Aehlig
  when (maybe True (jobFinalized . jJob) jb') . (>> return ()) . forkIO $ do
144 ea174b21 Klaus Aehlig
    logDebug "Scheduler noticed a job to have finished."
145 ea174b21 Klaus Aehlig
    cleanupFinishedJobs state
146 ea174b21 Klaus Aehlig
    scheduleSomeJobs state
147 48e4da5c Klaus Aehlig
148 48e4da5c Klaus Aehlig
-- | Sort out the finished jobs from the monitored part of the queue.
149 48e4da5c Klaus Aehlig
-- This is the pure part, splitting the queue into a remaining queue
150 48e4da5c Klaus Aehlig
-- and the jobs that were removed.
151 a2977f53 Klaus Aehlig
sortoutFinishedJobs :: Queue -> (Queue, [JobWithStat])
152 48e4da5c Klaus Aehlig
sortoutFinishedJobs queue =
153 1b3bde96 Klaus Aehlig
  let (fin, run') = partition (jobFinalized . jJob) . qRunning $ queue
154 a2977f53 Klaus Aehlig
  in (queue {qRunning=run'}, fin)
155 48e4da5c Klaus Aehlig
156 48e4da5c Klaus Aehlig
-- | Actually clean up the finished jobs. This is the IO wrapper around
157 48e4da5c Klaus Aehlig
-- the pure `sortoutFinishedJobs`.
158 48e4da5c Klaus Aehlig
cleanupFinishedJobs :: JQStatus -> IO ()
159 48e4da5c Klaus Aehlig
cleanupFinishedJobs qstate = do
160 48e4da5c Klaus Aehlig
  finished <- atomicModifyIORef (jqJobs qstate) sortoutFinishedJobs
161 a2977f53 Klaus Aehlig
  let showJob = show . ((fromJobId . qjId) &&& calcJobStatus) . jJob
162 48e4da5c Klaus Aehlig
      jlist = commaJoin $ map showJob finished
163 48e4da5c Klaus Aehlig
  unless (null finished)
164 48e4da5c Klaus Aehlig
    . logInfo $ "Finished jobs: " ++ jlist
165 cc5ab470 Klaus Aehlig
  mapM_ (maybe (return ()) killINotify . jINotify) finished
166 48e4da5c Klaus Aehlig
167 b81650b0 Klaus Aehlig
-- | Watcher task for a job, to update it on file changes. It also
168 b81650b0 Klaus Aehlig
-- reinstantiates itself upon receiving an Ignored event.
169 b81650b0 Klaus Aehlig
jobWatcher :: JQStatus -> JobWithStat -> Event -> IO ()
170 b81650b0 Klaus Aehlig
jobWatcher state jWS e = do
171 b81650b0 Klaus Aehlig
  let jid = qjId $ jJob jWS
172 b81650b0 Klaus Aehlig
      jids = show $ fromJobId jid
173 b81650b0 Klaus Aehlig
  logInfo $ "Scheduler notified of change of job " ++ jids
174 b81650b0 Klaus Aehlig
  logDebug $ "Scheulder notify event for " ++ jids ++ ": " ++ show e
175 b81650b0 Klaus Aehlig
  let inotify = jINotify jWS
176 b81650b0 Klaus Aehlig
  when (e == Ignored  && isJust inotify) $ do
177 b81650b0 Klaus Aehlig
    qdir <- queueDir
178 b81650b0 Klaus Aehlig
    let fpath = liveJobFile qdir jid
179 b81650b0 Klaus Aehlig
    _ <- addWatch (fromJust inotify) [Modify, Delete] fpath
180 b81650b0 Klaus Aehlig
           (jobWatcher state jWS)
181 b81650b0 Klaus Aehlig
    return ()
182 b81650b0 Klaus Aehlig
  updateJob state jWS
183 b81650b0 Klaus Aehlig
184 b81650b0 Klaus Aehlig
-- | Attach the job watcher to a running job.
185 b81650b0 Klaus Aehlig
attachWatcher :: JQStatus -> JobWithStat -> IO ()
186 b81650b0 Klaus Aehlig
attachWatcher state jWS = when (isNothing $ jINotify jWS) $ do
187 b81650b0 Klaus Aehlig
  inotify <- initINotify
188 b81650b0 Klaus Aehlig
  qdir <- queueDir
189 b81650b0 Klaus Aehlig
  let fpath = liveJobFile qdir . qjId $ jJob jWS
190 b81650b0 Klaus Aehlig
      jWS' = jWS { jINotify=Just inotify }
191 b81650b0 Klaus Aehlig
  logDebug $ "Attaching queue watcher for " ++ fpath
192 b81650b0 Klaus Aehlig
  _ <- addWatch inotify [Modify, Delete] fpath $ jobWatcher state jWS'
193 b81650b0 Klaus Aehlig
  modifyJobs state . onRunningJobs $ updateJobStatus jWS'
194 b81650b0 Klaus Aehlig
195 48e4da5c Klaus Aehlig
-- | Decide on which jobs to schedule next for execution. This is the
196 48e4da5c Klaus Aehlig
-- pure function doing the scheduling.
197 a2977f53 Klaus Aehlig
selectJobsToRun :: Queue -> (Queue, [JobWithStat])
198 48e4da5c Klaus Aehlig
selectJobsToRun queue =
199 48e4da5c Klaus Aehlig
  let n = maxRunningJobs - length (qRunning queue)
200 48e4da5c Klaus Aehlig
      (chosen, remain) = splitAt n (qEnqueued queue)
201 a2977f53 Klaus Aehlig
  in (queue {qEnqueued=remain, qRunning=qRunning queue ++ chosen}, chosen)
202 48e4da5c Klaus Aehlig
203 1c532d2d Klaus Aehlig
-- | Requeue jobs that were previously selected for execution
204 1c532d2d Klaus Aehlig
-- but couldn't be started.
205 a2977f53 Klaus Aehlig
requeueJobs :: JQStatus -> [JobWithStat] -> IOError -> IO ()
206 1c532d2d Klaus Aehlig
requeueJobs qstate jobs err = do
207 a2977f53 Klaus Aehlig
  let jids = map (qjId . jJob) jobs
208 1c532d2d Klaus Aehlig
      jidsString = commaJoin $ map (show . fromJobId) jids
209 1c532d2d Klaus Aehlig
      rmJobs = filter ((`notElem` jids) . qjId . jJob)
210 1c532d2d Klaus Aehlig
  logWarning $ "Starting jobs failed: " ++ show err
211 1c532d2d Klaus Aehlig
  logWarning $ "Rescheduling jobs: " ++ jidsString
212 1c532d2d Klaus Aehlig
  modifyJobs qstate (onRunningJobs rmJobs)
213 a2977f53 Klaus Aehlig
  modifyJobs qstate (onQueuedJobs $ (++) jobs)
214 1c532d2d Klaus Aehlig
215 48e4da5c Klaus Aehlig
-- | Schedule jobs to be run. This is the IO wrapper around the
216 48e4da5c Klaus Aehlig
-- pure `selectJobsToRun`.
217 48e4da5c Klaus Aehlig
scheduleSomeJobs :: JQStatus -> IO ()
218 48e4da5c Klaus Aehlig
scheduleSomeJobs qstate = do
219 48e4da5c Klaus Aehlig
  chosen <- atomicModifyIORef (jqJobs qstate) selectJobsToRun
220 a2977f53 Klaus Aehlig
  let jobs = map jJob chosen
221 7dd21737 Klaus Aehlig
  unless (null chosen) . logInfo . (++) "Starting jobs: " . commaJoin
222 a2977f53 Klaus Aehlig
    $ map (show . fromJobId . qjId) jobs
223 b81650b0 Klaus Aehlig
  mapM_ (attachWatcher qstate) chosen
224 a2977f53 Klaus Aehlig
  result <- try $ JQ.startJobs jobs
225 1c532d2d Klaus Aehlig
  either (requeueJobs qstate chosen) return result
226 48e4da5c Klaus Aehlig
227 48e4da5c Klaus Aehlig
-- | Format the job queue status in a compact, human readable way.
228 48e4da5c Klaus Aehlig
showQueue :: Queue -> String
229 48e4da5c Klaus Aehlig
showQueue (Queue {qEnqueued=waiting, qRunning=running}) =
230 48e4da5c Klaus Aehlig
  let showids = show . map (fromJobId . qjId . jJob)
231 48e4da5c Klaus Aehlig
  in "Waiting jobs: " ++ showids waiting 
232 48e4da5c Klaus Aehlig
       ++ "; running jobs: " ++ showids running
233 48e4da5c Klaus Aehlig
234 48e4da5c Klaus Aehlig
-- | Time-based watcher for updating the job queue.
235 48e4da5c Klaus Aehlig
onTimeWatcher :: JQStatus -> IO ()
236 48e4da5c Klaus Aehlig
onTimeWatcher qstate = forever $ do
237 48e4da5c Klaus Aehlig
  threadDelay watchInterval
238 fe50bb65 Klaus Aehlig
  logDebug "Job queue watcher timer fired"
239 48e4da5c Klaus Aehlig
  jobs <- readIORef (jqJobs qstate)
240 48e4da5c Klaus Aehlig
  mapM_ (updateJob qstate) $ qRunning jobs
241 48e4da5c Klaus Aehlig
  cleanupFinishedJobs qstate
242 48e4da5c Klaus Aehlig
  jobs' <- readIORef (jqJobs qstate)
243 48e4da5c Klaus Aehlig
  logInfo $ showQueue jobs'
244 48e4da5c Klaus Aehlig
  scheduleSomeJobs qstate
245 48e4da5c Klaus Aehlig
246 2713b91a Klaus Aehlig
-- | Read a single, non-archived, job, specified by its id, from disk.
247 2713b91a Klaus Aehlig
readJobFromDisk :: JobId -> IO (Result JobWithStat)
248 2713b91a Klaus Aehlig
readJobFromDisk jid = do
249 2713b91a Klaus Aehlig
  qdir <- queueDir
250 2713b91a Klaus Aehlig
  let fpath = liveJobFile qdir jid
251 2713b91a Klaus Aehlig
  logDebug $ "Reading " ++ fpath
252 2713b91a Klaus Aehlig
  tryFstat <- try $ getFStat fpath :: IO (Either IOError FStat)
253 2713b91a Klaus Aehlig
  let fstat = either (const nullFStat) id tryFstat
254 2713b91a Klaus Aehlig
  loadResult <- JQ.loadJobFromDisk qdir False jid
255 ed6cf449 Klaus Aehlig
  return $ liftM (JobWithStat Nothing fstat . fst) loadResult
256 2713b91a Klaus Aehlig
257 2713b91a Klaus Aehlig
-- | Read all non-finalized jobs from disk.
258 2713b91a Klaus Aehlig
readJobsFromDisk :: IO [JobWithStat]
259 2713b91a Klaus Aehlig
readJobsFromDisk = do
260 2713b91a Klaus Aehlig
  logInfo "Loading job queue"
261 2713b91a Klaus Aehlig
  qdir <- queueDir
262 2713b91a Klaus Aehlig
  eitherJids <- JQ.getJobIDs [qdir]
263 2713b91a Klaus Aehlig
  let jids = either (const []) JQ.sortJobIDs eitherJids
264 2713b91a Klaus Aehlig
      jidsstring = commaJoin $ map (show . fromJobId) jids
265 2713b91a Klaus Aehlig
  logInfo $ "Non-archived jobs on disk: " ++ jidsstring
266 2713b91a Klaus Aehlig
  jobs <- mapM readJobFromDisk jids
267 2713b91a Klaus Aehlig
  return $ justOk jobs
268 2713b91a Klaus Aehlig
269 48e4da5c Klaus Aehlig
-- | Set up the job scheduler. This will also start the monitoring
270 48e4da5c Klaus Aehlig
-- of changes to the running jobs.
271 48e4da5c Klaus Aehlig
initJQScheduler :: JQStatus -> IO ()
272 48e4da5c Klaus Aehlig
initJQScheduler qstate = do
273 2713b91a Klaus Aehlig
  alljobs <- readJobsFromDisk
274 2713b91a Klaus Aehlig
  let jobs = filter (not . jobFinalized . jJob) alljobs
275 2713b91a Klaus Aehlig
      (running, queued) = partition (jobStarted . jJob) jobs
276 2713b91a Klaus Aehlig
  modifyJobs qstate (onQueuedJobs (++ queued) . onRunningJobs (++ running))
277 2713b91a Klaus Aehlig
  jqjobs <- readIORef (jqJobs qstate)
278 2713b91a Klaus Aehlig
  logInfo $ showQueue jqjobs
279 2713b91a Klaus Aehlig
  scheduleSomeJobs qstate
280 48e4da5c Klaus Aehlig
  logInfo "Starting time-based job queue watcher"
281 48e4da5c Klaus Aehlig
  _ <- forkIO $ onTimeWatcher qstate
282 48e4da5c Klaus Aehlig
  return ()
283 48e4da5c Klaus Aehlig
284 48e4da5c Klaus Aehlig
-- | Enqueue new jobs. This will guarantee that the jobs will be executed
285 48e4da5c Klaus Aehlig
-- eventually.
286 48e4da5c Klaus Aehlig
enqueueNewJobs :: JQStatus -> [QueuedJob] -> IO ()
287 48e4da5c Klaus Aehlig
enqueueNewJobs state jobs = do
288 48e4da5c Klaus Aehlig
  logInfo . (++) "New jobs enqueued: " . commaJoin
289 48e4da5c Klaus Aehlig
    $ map (show . fromJobId . qjId) jobs
290 48e4da5c Klaus Aehlig
  let jobs' = map unreadJob jobs
291 48e4da5c Klaus Aehlig
  modifyJobs state (onQueuedJobs (++ jobs'))
292 48e4da5c Klaus Aehlig
  scheduleSomeJobs state