on master help sign in
11 files changed+181−19
since splitsnapshots
unifiedsplit
CHANGELOG.md+1 examples/OddJobsCliExample.lhs+2−2 odd-jobs.cabal+2 package.yaml+1−1 src/OddJobs/ConfigBuilder.hs+10 src/OddJobs/Endpoints.hs+12 src/OddJobs/Job.hs+89−12 src/OddJobs/Migrations.hs+3−1 src/OddJobs/Types.hs+12−2 src/OddJobs/Web.hs+25−1 test/Test.hs+24
CHANGELOG.md+1
@@ -2,6 +2,7 @@
22
33## 0.2.3
44+ Upgrade resource pool
5++ Add ability to kill jobs
56
67## 0.2.2
78
examples/OddJobsCliExample.lhs+2−2
@@ -72,11 +72,11 @@ In this example, the core job-runner function is in the `IO` monad. In all proba
7272myJobRunner :: Job -> IO ()
7373myJobRunner job = do
7474 throwParsePayload job >>= \case
75− SendWelcomeEmail _userId -> do
75+ SendWelcomeEmail userId -> do
7676 putStrLn $ "This should call the function that actually sends the welcome email. " <>
7777 "\nWe are purposely waiting 60 seconds before completing this job so that graceful shutdown can be demonstrated."
7878 delaySeconds (Seconds 60)
79− putStrLn "60 second wait is now over..."
79+ putStrLn $ "SendWelcomeEmail to user: " <> show userId <> " complete (60 second wait is now over...)"
8080 SendPasswordResetEmail _tkn ->
8181 putStrLn "This should call the function that actually sends the password-reset email"
8282 SetupSampleData _userId -> do
odd-jobs.cabal+2
@@ -75,6 +75,7 @@ library
7575 , async ==2.2.4
7676 , base >=4.7 && <5
7777 , bytestring
78+ , containers
7879 , directory
7980 , either
8081 , fast-logger
@@ -137,6 +138,7 @@ executable devel
137138 , async ==2.2.4
138139 , base >=4.7 && <5
139140 , bytestring
141+ , containers
140142 , directory
141143 , either
142144 , fast-logger
package.yaml+1−1
@@ -85,6 +85,7 @@ dependencies:
8585 - servant-server
8686 - servant-lucid
8787 - warp
88+ - containers
8889 - unordered-containers
8990 - optparse-applicative
9091 - filepath
@@ -149,4 +150,3 @@ tests:
149150 - mmorph
150151 - lifted-base
151152 - lifted-async
152− - containers
src/OddJobs/ConfigBuilder.hs+10
@@ -144,6 +144,10 @@ defaultLogStr jobTypeFn logLevel logEvent =
144144 LogJobTimeout j@Job{jobLockedAt, jobLockedBy} ->
145145 "Timeout | " <> jobToLogStr j <> " | lockedBy=" <> toLogStr (maybe "unknown" unJobRunnerName jobLockedBy) <>
146146 " lockedAt=" <> toLogStr (maybe "unknown" show jobLockedAt)
147+ LogKillJobSuccess j ->
148+ "Kill Job Success | " <> jobToLogStr j
149+ LogKillJobFailed j ->
150+ "Kill Job Failed | " <> jobToLogStr j <> "(the job might have completed or timed out)"
147151 LogPoll ->
148152 "Polling jobs table"
149153 LogWebUIRequest ->
@@ -321,6 +325,12 @@ defaultJsonLogEvent logEvent =
321325 LogJobTimeout job ->
322326 Aeson.object [ "tag" Aeson..= ("LogJobTimeout" :: Text)
323327 , "contents" Aeson..= defaultJsonJob job ]
328+ LogKillJobSuccess job ->
329+ Aeson.object [ "tag" Aeson..= ("LogKillJobSuccess" :: Text)
330+ , "contents" Aeson..= defaultJsonJob job ]
331+ LogKillJobFailed job ->
332+ Aeson.object [ "tag" Aeson..= ("LogKillJobFailed" :: Text)
333+ , "contents" Aeson..= defaultJsonJob job ]
324334 LogPoll ->
325335 Aeson.object [ "tag" Aeson..= ("LogJobPoll" :: Text)]
326336 LogWebUIRequest ->
src/OddJobs/Endpoints.hs+12
@@ -60,6 +60,7 @@ data Routes route = Routes
6060 , rEnqueue :: route :- "enqueue" :> Capture "jobId" JobId :> Post '[HTML] NoContent
6161 , rRunNow :: route :- "run" :> Capture "jobId" JobId :> Post '[HTML] NoContent
6262 , rCancel :: route :- "cancel" :> Capture "jobId" JobId :> Post '[HTML] NoContent
63+ , rKill :: route :- "kill" :> Capture "jobId" JobId :> Post '[HTML] NoContent
6364 , rRefreshJobTypes :: route :- "refresh-job-types" :> Post '[HTML] NoContent
6465 , rRefreshJobRunners :: route :- "refresh-job-runners" :> Post '[HTML] NoContent
6566 } deriving (Generic)
@@ -114,6 +115,7 @@ server cfg env nt =
114115 { rFilterResults = nt . filterResults cfg env
115116 , rEnqueue = nt . enqueueJob cfg env
116117 , rCancel = nt . cancelJob cfg env
118+ , rKill = nt . killJob cfg env
117119 , rRunNow = nt . runJobNow cfg env
118120 , rRefreshJobTypes = nt $ refreshJobTypes cfg env
119121 , rRefreshJobRunners = nt $ refreshJobRunners cfg env
@@ -126,6 +128,7 @@ server2 cfg env = Routes
126128 { rFilterResults = filterResults cfg env
127129 , rEnqueue = enqueueJob cfg env
128130 , rCancel = cancelJob cfg env
131+ , rKill = killJob cfg env
129132 , rRunNow = runJobNow cfg env
130133 , rRefreshJobTypes = refreshJobTypes cfg env
131134 , rRefreshJobRunners = refreshJobRunners cfg env
@@ -156,6 +159,14 @@ cancelJob UIConfig{..} env jid = do
156159 liftIO $ withResource uicfgDbPool $ \conn -> void $ cancelJobIO conn uicfgTableName jid
157160 redirectToHome env
158161
162+killJob :: UIConfig
163+ -> Env
164+ -> JobId
165+ -> Handler NoContent
166+killJob UIConfig{..} env jid = do
167+ liftIO $ withResource uicfgDbPool $ \conn -> void $ killJobIO conn uicfgTableName jid
168+ redirectToHome env
169+
159170runJobNow :: UIConfig
160171 -> Env
161172 -> JobId
@@ -201,6 +212,7 @@ routes linkFn = Web.Routes
201212 , Web.rEnqueue = rEnqueue
202213 , Web.rRunNow = rRunNow
203214 , Web.rCancel = rCancel
215+ , Web.rKill = rKill
204216 , Web.rRefreshJobTypes = rRefreshJobTypes
205217 , Web.rRefreshJobRunners = rRefreshJobRunners
206218 , Web.rStaticAsset = linkFn
src/OddJobs/Job.hs+89−12
@@ -65,6 +65,7 @@ module OddJobs.Job
6565 , runJobNowIO
6666 , unlockJobIO
6767 , cancelJobIO
68+ , killJobIO
6869 , jobDbColumns
6970 , jobPollingIO
7071 , concatJobDbColumns
@@ -110,14 +111,16 @@ import Data.Aeson hiding (Success)
110111import qualified Data.Aeson as Aeson
111112import qualified Data.Aeson.Types as Aeson (Parser, parseMaybe)
112113import Data.String.Conv (StringConv(..), toS)
113−import Data.Functor (void)
114+import Data.Functor ((<&>), void)
114115import Control.Monad (forever, forM_, join)
115116import Data.Maybe (isNothing, maybe, fromMaybe, listToMaybe, mapMaybe)
116117import Data.Either (either)
117118import Control.Monad.Reader
118119import GHC.Generics
120+import Data.Map (Map)
119121import qualified Data.HashMap.Strict as HM
120122import qualified Data.List as DL
123+import qualified Data.Map as DM
121124import qualified Data.ByteString as BS
122125import qualified Data.ByteString.Lazy as BSL
123126import System.FilePath (FilePath)
@@ -169,7 +172,7 @@ class (MonadUnliftIO m, MonadBaseControl IO m) => HasJobRunner m where
169172
170173data RunnerEnv = RunnerEnv
171174 { envConfig :: !Config
172− , envJobThreadsRef :: !(IORef [Async ()])
175+ , envJobThreadsRef :: !(IORef (Map JobId (Async ())))
173176 }
174177
175178type RunnerM = ReaderT RunnerEnv IO
@@ -218,7 +221,7 @@ instance HasJobRunner RunnerM where
218221startJobRunner :: Config -> IO ()
219222startJobRunner jm = do
220223 traceM "startJobRunner top"
221− r <- newIORef []
224+ r <- newIORef DM.empty
222225 let monitorEnv = RunnerEnv
223226 { envConfig = jm
224227 , envJobThreadsRef = r
@@ -346,7 +349,7 @@ deleteJobIO conn tname jid = do
346349runJobNowIO :: Connection -> TableName -> JobId -> IO (Maybe Job)
347350runJobNowIO conn tname jid = do
348351 t <- getCurrentTime
349− updateJobHelper tname conn (Queued, [Queued, Retry, Failed], Just t, jid)
352+ updateJobHelper tname conn (Queued, [Queued, Retry, Failed, Cancelled], Just t, jid)
350353
351354-- | TODO: First check in all job-runners if this job is still running, or not,
352355-- and somehow send an uninterruptibleCancel to that thread.
@@ -358,7 +361,11 @@ unlockJobIO conn tname jid = do
358361
359362cancelJobIO :: Connection -> TableName -> JobId -> IO (Maybe Job)
360363cancelJobIO conn tname jid =
361− updateJobHelper tname conn (Failed, [Queued, Retry], Nothing, jid)
364+ updateJobHelper tname conn (Cancelled, [Queued, Retry], Nothing, jid)
365+
366+killJobIO :: Connection -> TableName -> JobId -> IO (Maybe Job)
367+killJobIO conn tname jid =
368+ updateJobHelper tname conn (Cancelled, [Locked], Nothing, jid)
362369
363370updateJobHelper :: TableName
364371 -> Connection
@@ -383,13 +390,17 @@ runJobWithTimeout :: (HasJobRunner m)
383390 => Seconds
384391 -> Job
385392 -> m ()
386−runJobWithTimeout timeoutSec job = do
393+runJobWithTimeout timeoutSec job@Job{jobId} = do
387394 threadsRef <- envJobThreadsRef <$> getRunnerEnv
388395 jobRunner_ <- getJobRunner
389396
390397 a <- async $ liftIO $ jobRunner_ job
391398
392− _x <- atomicModifyIORef' threadsRef $ \threads -> (a:threads, DL.map asyncThreadId (a:threads))
399+ _x <- atomicModifyIORef' threadsRef $ \threads ->
400+ ( DM.insert jobId a threads
401+ , DL.map asyncThreadId $ DM.elems $ DM.insert jobId a threads
402+ )
403+
393404 -- liftIO $ putStrLn $ "Threads: " <> show x
394405 log LevelDebug $ LogText $ toS $ "Spawned job in " <> show (asyncThreadId a)
395406
@@ -399,7 +410,7 @@ runJobWithTimeout timeoutSec job = do
399410
400411 void $ finally
401412 (waitEitherCancel a t)
402− (atomicModifyIORef' threadsRef $ \threads -> (DL.delete a threads, ()))
413+ (atomicModifyIORef' threadsRef $ \threads -> (DM.delete jobId threads, ()))
403414
404415
405416-- | runs a job, blocks for as long as it's in progress
@@ -455,6 +466,25 @@ runJob jid = do
455466
456467 pure ()
457468
469+killJob :: (HasJobRunner m) => JobId -> m ()
470+killJob jid = do
471+ threadsRef <- envJobThreadsRef <$> getRunnerEnv
472+ threads <- liftIO $ readIORef threadsRef
473+ mJob <- findJobById jid
474+
475+ case (mJob, jid `DM.lookup` threads) of
476+ (Just job, Just thread) -> do
477+ log LevelInfo $ LogKillJobSuccess job
478+ void $ finally
479+ (uninterruptibleCancel thread)
480+ (atomicModifyIORef' threadsRef $ \threads' -> (DM.delete jid threads', ()))
481+
482+ (Just job, Nothing) -> do
483+ log LevelInfo $ LogKillJobFailed job
484+
485+ (Nothing, _) ->
486+ log LevelError $ LogText $ "Unable to find job in db to kill, jobId = " <> toS (show jid)
487+
458488-- TODO: This might have a resource leak.
459489restartUponCrash :: (HasJobRunner m, Show a) => Text -> m a -> m ()
460490restartUponCrash name_ action = do
@@ -480,9 +510,11 @@ jobMonitor = do
480510 traceM "jobMonitor top"
481511 a1 <- async $ restartUponCrash "Job poller" jobPoller
482512 a2 <- async $ restartUponCrash "Job event listener" jobEventListener
513+ a3 <- async $ restartUponCrash "Job Kill poller" killJobPoller
483514 traceM "jobMonitor after async"
484− finally (void $ waitAnyCatch [a1, a2]) $ do
515+ finally (void $ waitAnyCatch [a1, a2, a3]) $ do
485516 log LevelInfo (LogText "Stopping jobPoller and jobEventListener threads.")
517+ cancel a3
486518 cancel a2
487519 cancel a1
488520 log LevelInfo (LogText "Waiting for jobs to complete.")
@@ -503,11 +535,19 @@ jobPollingWithResourceSql =
503535 \ ORDER BY attempts ASC, run_at ASC LIMIT 1) \
504536 \ RETURNING id"
505537
538+-- | Ref: 'killJobPoller'
539+killJobPollingSql :: Query
540+killJobPollingSql =
541+ "UPDATE ? SET locked_at = NULL, locked_by = NULL \
542+ \ WHERE id IN (SELECT id FROM ? WHERE status = ? AND locked_by = ? AND locked_at <= ? \
543+ \ ORDER BY locked_at ASC LIMIT 1 FOR UPDATE \
544+ \ ) RETURNING id"
545+
506546waitForJobs :: (HasJobRunner m)
507547 => m ()
508548waitForJobs = do
509− threadsRef <- envJobThreadsRef <$> getRunnerEnv
510− readIORef threadsRef >>= \case
549+ curJobs <- getRunnerEnv >>= (readIORef . envJobThreadsRef) <&> DM.elems
550+ case curJobs of
511551 [] -> log LevelInfo $ LogText "All job-threads exited"
512552 as -> do
513553 tid <- myThreadId
@@ -526,7 +566,7 @@ getConcurrencyControlFn :: (HasJobRunner m)
526566getConcurrencyControlFn = getConcurrencyControl >>= \case
527567 UnlimitedConcurrentJobs -> pure $ const $ pure PollAny
528568 MaxConcurrentJobs maxJobs -> pure $ const $ do
529− curJobs <- getRunnerEnv >>= (readIORef . envJobThreadsRef)
569+ curJobs <- getRunnerEnv >>= (readIORef . envJobThreadsRef) <&> DM.elems
530570 pure $ pollIf $ DL.length curJobs < maxJobs
531571 ResourceLimits resCfg -> pure $ const $ pure $ PollWithResources resCfg
532572 DynamicConcurrency fn -> pure $ const $ pollIf <$> liftIO fn
@@ -630,6 +670,43 @@ pollRunJob processName mResCfg = do
630670 delayAction = delaySeconds =<< getPollingInterval
631671 noDelayAction = pure ()
632672
673+-- | Executes 'killJobPollingSql' every 'cfgPollingInterval' seconds to pick up jobs
674+-- that are cancelled and need to be killed. Uses @UPDATE@ along with @SELECT...
675+-- ..FOR UPDATE@ to efficiently find a job that matches /all/ of the following
676+-- conditions:
677+--
678+-- * 'jobStatus' should be 'cancelled'
679+-- * 'jobLockedAt' should be in the past
680+-- * 'jobLockedBy' should be the current job worker name
681+
682+killJobPoller :: (HasJobRunner m) => m ()
683+killJobPoller = do
684+ processName <- liftIO jobWorkerName
685+ pool <- getDbPool
686+ tname <- getTableName
687+
688+ let pollJobToKill conn = join $ mask_ $ do
689+ currentTime <- liftIO getCurrentTime
690+ result <- liftIO $ PGS.query conn killJobPollingSql
691+ (tname, tname, Cancelled, processName, currentTime)
692+
693+ case result of
694+ [] ->
695+ pure delayAction
696+
697+ [Only (jobId :: JobId)] -> do
698+ void $ async $ killJob jobId
699+ pure noDelayAction
700+
701+ x ->
702+ error $ "I was supposed to get only a single row, but got: " ++ show x
703+
704+ withResource pool (forever . pollJobToKill)
705+
706+ where
707+ delayAction = delaySeconds =<< getPollingInterval
708+ noDelayAction = pure ()
709+
633710-- | Uses PostgreSQL's LISTEN/NOTIFY to be immediately notified of newly created
634711-- jobs.
635712jobEventListener :: (HasJobRunner m)
src/OddJobs/Migrations.hs+3−1
@@ -22,7 +22,9 @@ createJobTableQuery = "CREATE TABLE IF NOT EXISTS ?" <>
2222 ", attempts int not null default 0" <>
2323 ", locked_at timestamp with time zone null" <>
2424 ", locked_by text null" <>
25− ", constraint incorrect_locking_info CHECK ((status <> 'locked' and locked_at is null and locked_by is null) or (status = 'locked' and locked_at is not null and locked_by is not null))" <>
25+ ", constraint incorrect_locking_info CHECK (" <>
26+ "(locked_at is null and locked_by is null and status <> 'locked') or " <>
27+ "(locked_at is not null and locked_by is not null and (status = 'locked' or status = 'cancelled')))" <>
2628 ");" <>
2729 "create index if not exists ? on ?(created_at);" <>
2830 "create index if not exists ? on ?(updated_at);" <>
src/OddJobs/Types.hs+12−2
@@ -35,7 +35,7 @@ import Control.Monad.Logger (LogLevel)
3535-- myJobsTable :: TableName
3636-- myJobsTable = "my_jobs"
3737-- @
38−--
38+--
3939-- This should also work for table names qualified by the schema name. For example:
4040--
4141-- @
@@ -72,6 +72,11 @@ data LogEvent
7272 | LogJobFailed !Job !SomeException !FailureMode !NominalDiffTime
7373 -- | Emitted when a job times out and is picked-up again for execution
7474 | LogJobTimeout !Job
75+ -- | Emitted when user kills a job and the job thread sucessfully cancelled thereafter.
76+ | LogKillJobSuccess !Job
77+ -- | Emitted when user kills a job and the job thread is not found in the threadRef
78+ -- | (most likely the job has either got completed or timed out).
79+ | LogKillJobFailed !Job
7580 -- | Emitted whenever 'OddJobs.Job.jobPoller' polls the DB table
7681 | LogPoll
7782 -- | TODO
@@ -182,8 +187,11 @@ data Status
182187 -- | Jobs in 'Queued' status /may/ be picked up by the job-runner on the basis
183188 -- of the 'jobRunAt' field.
184189 | Queued
185− -- | Jobs in 'Failed' status will will not be retried by the job-runner.
190+ -- | Jobs in 'Failed' status will not be retried by the job-runner.
186191 | Failed
192+ -- | Jobs with 'Cancelled' status are cancelled by the user and will not be
193+ -- retried by the job-runner
194+ | Cancelled
187195 -- | Jobs in 'Retry' status will be retried by the job-runner on the basis of
188196 -- the 'jobRunAt' field.
189197 | Retry
@@ -227,6 +235,7 @@ instance ToText Status where
227235 Queued -> "queued"
228236 Retry -> "retry"
229237 Failed -> "failed"
238+ Cancelled -> "cancelled"
230239 Locked -> "locked"
231240
232241instance (StringConv Text a) => FromText (Either a Status) where
@@ -234,6 +243,7 @@ instance (StringConv Text a) => FromText (Either a Status) where
234243 "success" -> Right Success
235244 "queued" -> Right Queued
236245 "failed" -> Right Failed
246+ "cancelled" -> Right Cancelled
237247 "retry" -> Right Retry
238248 "locked" -> Right Locked
239249 x -> Left $ toS $ "Unknown job status: " <> x
src/OddJobs/Web.hs+25−1
@@ -116,6 +116,7 @@ instance ToHttpApiData Filter where
116116-- , rEnqueue :: route :- "enqueue" :> Capture "jobId" JobId :> Post '[HTML] NoContent
117117-- , rRunNow :: route :- "run" :> Capture "jobId" JobId :> Post '[HTML] NoContent
118118-- , rCancel :: route :- "cancel" :> Capture "jobId" JobId :> Post '[HTML] NoContent
119+-- , rKill :: route :- "kill" :> Capture "jobId" JobId :> Post '[HTML] NoContent
119120-- , rRefreshJobTypes :: route :- "refresh-job-types" :> Post '[HTML] NoContent
120121-- , rRefreshJobRunners :: route :- "refresh-job-runners" :> Post '[HTML] NoContent
121122-- } deriving (Generic)
@@ -126,6 +127,7 @@ data Routes = Routes
126127 , rEnqueue :: JobId -> Text
127128 , rRunNow :: JobId -> Text
128129 , rCancel :: JobId -> Text
130+ , rKill :: JobId -> Text
129131 , rRefreshJobTypes :: Text
130132 , rRefreshJobRunners :: Text
131133 , rStaticAsset :: Text -> Text
@@ -382,6 +384,7 @@ jobRow routes t (job@Job{..}, jobHtml) = do
382384 let statusFn = case jobStatus of
383385 Job.Success -> statusSuccess
384386 Job.Failed -> statusFailed
387+ Job.Cancelled -> statusCancelled
385388 Job.Queued -> if jobRunAt > t
386389 then statusFuture
387390 else statusWaiting
@@ -394,11 +397,12 @@ jobRow routes t (job@Job{..}, jobHtml) = do
394397 let actionsFn = case jobStatus of
395398 Job.Success -> const mempty
396399 Job.Failed -> actionsFailed
400+ Job.Cancelled -> actionsCancelled
397401 Job.Queued -> if jobRunAt > t
398402 then actionsFuture
399403 else actionsWaiting
400404 Job.Retry -> actionsRetry
401− Job.Locked -> const mempty
405+ Job.Locked -> actionsLocked
402406 actionsFn routes job
403407
404408
@@ -407,6 +411,15 @@ actionsFailed Routes{..} Job{..} = do
407411 form_ [ action_ (rEnqueue jobId), method_ "post" ] $ do
408412 button_ [ class_ "btn btn-secondary", type_ "submit" ] "Enqueue again"
409413
414+actionsCancelled :: Routes -> Job -> Html ()
415+actionsCancelled Routes{..} Job{..} = case (jobLockedAt, jobLockedBy) of
416+ (Just _, Just _) -> do
417+ span_ [ class_ "badge badge-light" ] "Killing job"
418+
419+ _ -> do
420+ form_ [ action_ (rEnqueue jobId), method_ "post" ] $ do
421+ button_ [ class_ "btn btn-secondary", type_ "submit" ] "Enqueue again"
422+
410423actionsRetry :: Routes -> Job -> Html ()
411424actionsRetry Routes{..} Job{..} = do
412425 form_ [ action_ (rRunNow jobId), method_ "post" ] $ do
@@ -422,6 +435,11 @@ actionsWaiting Routes{..} Job{..} = do
422435 form_ [ action_ (rCancel jobId), method_ "post" ] $ do
423436 button_ [ class_ "btn btn-danger", type_ "submit" ] "Cancel"
424437
438+actionsLocked :: Routes -> Job -> Html ()
439+actionsLocked Routes{..} Job{..} = do
440+ form_ [ action_ (rKill jobId), method_ "post" ] $ do
441+ button_ [ class_ "btn btn-warning", type_ "submit" ] "Kill"
442+
425443statusSuccess :: UTCTime -> Job -> Html ()
426444statusSuccess t Job{..} = do
427445 span_ [ class_ "badge badge-success" ] "Success"
@@ -436,6 +454,12 @@ statusFailed t Job{..} = do
436454 span_ [ class_ "job-run-time" ] $ do
437455 abbr_ [ title_ (showText jobUpdatedAt) ] $ toHtml $ "Failed " <> humanReadableTime' t jobUpdatedAt <> " after " <> show jobAttempts <> " attempts"
438456
457+statusCancelled :: UTCTime -> Job -> Html ()
458+statusCancelled t Job{..} = do
459+ span_ [ class_ "badge badge-danger" ] "Cancelled"
460+ span_ [ class_ "job-run-time" ] $ do
461+ abbr_ [ title_ (showText jobUpdatedAt) ] $ toHtml $ "Cancelled " <> humanReadableTime' t jobUpdatedAt
462+
439463statusFuture :: UTCTime -> Job -> Html ()
440464statusFuture t Job{..} = do
441465 span_ [ class_ "badge badge-secondary" ] "Future"
test/Test.hs+24
@@ -92,6 +92,7 @@ tests appPool jobPool = testGroup "All tests"
9292 , testOnJobTimeout appPool jobPool
9393 ]
9494 , testResourceLimitedScheduling appPool jobPool
95+ , testKillJob appPool jobPool
9596 ]
9697 -- , testGroup "property tests" [ testEverything appPool jobPool
9798 -- -- , propFilterJobs appPool jobPool
@@ -166,6 +167,8 @@ logEventToJob le = case le of
166167 Job.LogJobSuccess j _ -> Just j
167168 Job.LogJobFailed j _ _ _ -> Just j
168169 Job.LogJobTimeout j -> Just j
170+ Job.LogKillJobSuccess j -> Just j
171+ Job.LogKillJobFailed j -> Just j
169172 Job.LogPoll -> Nothing
170173 Job.LogWebUIRequest -> Nothing
171174 Job.LogText _ -> Nothing
@@ -195,6 +198,8 @@ assertJobIdStatus conn tname logRef msg st jid = do
195198 Job.LogJobSuccess j _ -> jid == Job.jobId j
196199 Job.LogJobFailed j _ _ _ -> jid == Job.jobId j
197200 Job.LogJobTimeout j -> jid == Job.jobId j
201+ Job.LogKillJobSuccess _ -> False
202+ Job.LogKillJobFailed _ -> False
198203 Job.LogWebUIRequest -> False
199204 Job.LogText _ -> False
200205 Job.LogPoll -> False
@@ -205,6 +210,12 @@ assertJobIdStatus conn tname logRef msg st jid = do
205210 Job.LogJobFailed j _ _ _ -> jid == Job.jobId j
206211 _ -> False
207212
213+ Job.Cancelled ->
214+ assertBool (msg <> ": Job killed event not found in job-logs for JobId=" <> show jid) $
215+ flip DL.any logs $ \case
216+ Job.LogKillJobSuccess j -> jid == Job.jobId j
217+ _ -> False
218+
208219 Job.Retry ->
209220 assertBool (msg <> ": Failed event not found in job-logs for JobId=" <> show jid) $
210221 flip DL.any logs $ \case
@@ -627,6 +638,19 @@ setup' jobPool defaultLimit action =
627638 , Job.cfgDeleteSuccessfulJobs = False
628639 }
629640
641+testKillJob appPool jobPool = testCase "killing a ongoing job" $ do
642+ withRandomTable jobPool $ \tname -> withNamedJobMonitor tname jobPool Prelude.id $ \logRef -> do
643+ Pool.withResource appPool $ \conn -> do
644+ Job{jobId = jid} <- Job.createJob conn tname (PayloadSucceed 60)
645+ delaySeconds $ Job.defaultPollingInterval + Seconds 2
646+
647+ assertJobIdStatus conn tname logRef "Job should be running" Job.Locked jid
648+
649+ _ <- Job.killJobIO conn tname jid
650+ delaySeconds $ Job.defaultPollingInterval + Seconds 2
651+
652+ assertJobIdStatus conn tname logRef "Job is cancelled and the job thread should be killed" Job.Cancelled jid
653+
630654data JobEvent = JobStart
631655 | JobRetry
632656 | JobSuccess