faktory 1.1.2.6 → 1.1.2.7
raw patch · 9 files changed
+53/−19 lines, 9 filesPVP: major bump suggested
API removals or changes: PVP suggests a major version bump
API changes (from Hackage documentation)
+ Faktory.Job: instance Data.Foldable.Foldable Faktory.Job.Job
+ Faktory.Job: instance Data.Traversable.Traversable Faktory.Job.Job
+ Faktory.Job: instance GHC.Base.Functor Faktory.Job.Job
+ Faktory.Job: instance GHC.Show.Show arg => GHC.Show.Show (Faktory.Job.Job arg)
+ Faktory.JobFailure: instance GHC.Show.Show Faktory.JobFailure.JobFailure
+ Faktory.Prelude: class a ~# b => (a :: k) ~ (b :: k)
+ Faktory.Prelude: forAccumM :: (Monad m, Traversable t) => s -> t a -> (s -> a -> m (s, b)) -> m (s, t b)
+ Faktory.Prelude: liftA2 :: Applicative f => (a -> b -> c) -> f a -> f b -> f c
+ Faktory.Prelude: mapAccumM :: (Monad m, Traversable t) => (s -> a -> m (s, b)) -> s -> t a -> m (s, t b)
+ Faktory.Settings: [settingsOnFailed] :: WorkerSettings -> SomeException -> IO ()
- Faktory.Prelude: (!!) :: [a] -> Int -> a
+ Faktory.Prelude: (!!) :: HasCallStack => [a] -> Int -> a
- Faktory.Prelude: bracket :: MonadMask m => m a -> (a -> m b) -> (a -> m c) -> m c
+ Faktory.Prelude: bracket :: (HasCallStack, MonadMask m) => m a -> (a -> m b) -> (a -> m c) -> m c
- Faktory.Prelude: bracketOnError :: MonadMask m => m a -> (a -> m b) -> (a -> m c) -> m c
+ Faktory.Prelude: bracketOnError :: (HasCallStack, MonadMask m) => m a -> (a -> m b) -> (a -> m c) -> m c
- Faktory.Prelude: bracketOnError_ :: MonadMask m => m a -> m b -> m c -> m c
+ Faktory.Prelude: bracketOnError_ :: (HasCallStack, MonadMask m) => m a -> m b -> m c -> m c
- Faktory.Prelude: bracketWithError :: MonadMask m => m a -> (Maybe SomeException -> a -> m b) -> (a -> m c) -> m c
+ Faktory.Prelude: bracketWithError :: (HasCallStack, MonadMask m) => m a -> (Maybe SomeException -> a -> m b) -> (a -> m c) -> m c
- Faktory.Prelude: bracket_ :: MonadMask m => m a -> m b -> m c -> m c
+ Faktory.Prelude: bracket_ :: (HasCallStack, MonadMask m) => m a -> m b -> m c -> m c
- Faktory.Prelude: catch :: (MonadCatch m, Exception e) => m a -> (e -> m a) -> m a
+ Faktory.Prelude: catch :: (HasCallStack, MonadCatch m, Exception e) => m a -> (e -> m a) -> m a
- Faktory.Prelude: catchAny :: MonadCatch m => m a -> (SomeException -> m a) -> m a
+ Faktory.Prelude: catchAny :: (HasCallStack, MonadCatch m) => m a -> (SomeException -> m a) -> m a
- Faktory.Prelude: catchAnyDeep :: (MonadCatch m, MonadIO m, NFData a) => m a -> (SomeException -> m a) -> m a
+ Faktory.Prelude: catchAnyDeep :: (HasCallStack, MonadCatch m, MonadIO m, NFData a) => m a -> (SomeException -> m a) -> m a
- Faktory.Prelude: catchAsync :: (MonadCatch m, Exception e) => m a -> (e -> m a) -> m a
+ Faktory.Prelude: catchAsync :: (HasCallStack, MonadCatch m, Exception e) => m a -> (e -> m a) -> m a
- Faktory.Prelude: catchDeep :: (MonadCatch m, MonadIO m, Exception e, NFData a) => m a -> (e -> m a) -> m a
+ Faktory.Prelude: catchDeep :: (HasCallStack, MonadCatch m, MonadIO m, Exception e, NFData a) => m a -> (e -> m a) -> m a
- Faktory.Prelude: catchIO :: MonadCatch m => m a -> (IOException -> m a) -> m a
+ Faktory.Prelude: catchIO :: (HasCallStack, MonadCatch m) => m a -> (IOException -> m a) -> m a
- Faktory.Prelude: catchIOError :: MonadCatch m => m a -> (IOError -> m a) -> m a
+ Faktory.Prelude: catchIOError :: (HasCallStack, MonadCatch m) => m a -> (IOError -> m a) -> m a
- Faktory.Prelude: catchJust :: (MonadCatch m, Exception e) => (e -> Maybe b) -> m a -> (b -> m a) -> m a
+ Faktory.Prelude: catchJust :: (HasCallStack, MonadCatch m, Exception e) => (e -> Maybe b) -> m a -> (b -> m a) -> m a
- Faktory.Prelude: catches :: (MonadCatch m, MonadThrow m) => m a -> [Handler m a] -> m a
+ Faktory.Prelude: catches :: (HasCallStack, MonadCatch m, MonadThrow m) => m a -> [Handler m a] -> m a
- Faktory.Prelude: catchesAsync :: (MonadCatch m, MonadThrow m) => m a -> [Handler m a] -> m a
+ Faktory.Prelude: catchesAsync :: (HasCallStack, MonadCatch m, MonadThrow m) => m a -> [Handler m a] -> m a
- Faktory.Prelude: catchesDeep :: (MonadCatch m, MonadThrow m, MonadIO m, NFData a) => m a -> [Handler m a] -> m a
+ Faktory.Prelude: catchesDeep :: (HasCallStack, MonadCatch m, MonadThrow m, MonadIO m, NFData a) => m a -> [Handler m a] -> m a
- Faktory.Prelude: class Bounded a
+ Faktory.Prelude: class () => Bounded a
- Faktory.Prelude: class Enum a
+ Faktory.Prelude: class () => Enum a
- Faktory.Prelude: class Eq a
+ Faktory.Prelude: class () => Eq a
- Faktory.Prelude: class Foldable (t :: TYPE LiftedRep -> Type)
+ Faktory.Prelude: class () => Foldable (t :: Type -> Type)
- Faktory.Prelude: class Functor (f :: Type -> Type)
+ Faktory.Prelude: class () => Functor (f :: Type -> Type)
- Faktory.Prelude: class Num a
+ Faktory.Prelude: class () => Num a
- Faktory.Prelude: class Read a
+ Faktory.Prelude: class () => Read a
- Faktory.Prelude: class Semigroup a
+ Faktory.Prelude: class () => Semigroup a
- Faktory.Prelude: class Show a
+ Faktory.Prelude: class () => Show a
- Faktory.Prelude: class Typeable (a :: k)
+ Faktory.Prelude: class () => Typeable (a :: k)
- Faktory.Prelude: cycle :: [a] -> [a]
+ Faktory.Prelude: cycle :: HasCallStack => [a] -> [a]
- Faktory.Prelude: data AsyncExceptionWrapper
+ Faktory.Prelude: data () => AsyncExceptionWrapper
- Faktory.Prelude: data Bool
+ Faktory.Prelude: data () => Bool
- Faktory.Prelude: data Char
+ Faktory.Prelude: data () => Char
- Faktory.Prelude: data Double
+ Faktory.Prelude: data () => Double
- Faktory.Prelude: data Either a b
+ Faktory.Prelude: data () => Either a b
- Faktory.Prelude: data Float
+ Faktory.Prelude: data () => Float
- Faktory.Prelude: data Handler (m :: Type -> Type) a
+ Faktory.Prelude: data () => Handler (m :: Type -> Type) a
- Faktory.Prelude: data IO a
+ Faktory.Prelude: data () => IO a
- Faktory.Prelude: data IOException
+ Faktory.Prelude: data () => IOException
- Faktory.Prelude: data Int
+ Faktory.Prelude: data () => Int
- Faktory.Prelude: data Integer
+ Faktory.Prelude: data () => Integer
- Faktory.Prelude: data Maybe a
+ Faktory.Prelude: data () => Maybe a
- Faktory.Prelude: data Ordering
+ Faktory.Prelude: data () => Ordering
- Faktory.Prelude: data SomeAsyncException
+ Faktory.Prelude: data () => SomeAsyncException
- Faktory.Prelude: data SomeException
+ Faktory.Prelude: data () => SomeException
- Faktory.Prelude: data StringException
+ Faktory.Prelude: data () => StringException
- Faktory.Prelude: data SyncExceptionWrapper
+ Faktory.Prelude: data () => SyncExceptionWrapper
- Faktory.Prelude: data Text
+ Faktory.Prelude: data () => Text
- Faktory.Prelude: data Word
+ Faktory.Prelude: data () => Word
- Faktory.Prelude: finally :: MonadMask m => m a -> m b -> m a
+ Faktory.Prelude: finally :: (HasCallStack, MonadMask m) => m a -> m b -> m a
- Faktory.Prelude: generalBracket :: MonadMask m => m a -> (a -> ExitCase b -> m c) -> (a -> m b) -> m (b, c)
+ Faktory.Prelude: generalBracket :: (MonadMask m, HasCallStack) => m a -> (a -> ExitCase b -> m c) -> (a -> m b) -> m (b, c)
- Faktory.Prelude: handle :: (MonadCatch m, Exception e) => (e -> m a) -> m a -> m a
+ Faktory.Prelude: handle :: (HasCallStack, MonadCatch m, Exception e) => (e -> m a) -> m a -> m a
- Faktory.Prelude: handleAny :: MonadCatch m => (SomeException -> m a) -> m a -> m a
+ Faktory.Prelude: handleAny :: (HasCallStack, MonadCatch m) => (SomeException -> m a) -> m a -> m a
- Faktory.Prelude: handleAnyDeep :: (MonadCatch m, MonadIO m, NFData a) => (SomeException -> m a) -> m a -> m a
+ Faktory.Prelude: handleAnyDeep :: (HasCallStack, MonadCatch m, MonadIO m, NFData a) => (SomeException -> m a) -> m a -> m a
- Faktory.Prelude: handleAsync :: (MonadCatch m, Exception e) => (e -> m a) -> m a -> m a
+ Faktory.Prelude: handleAsync :: (HasCallStack, MonadCatch m, Exception e) => (e -> m a) -> m a -> m a
- Faktory.Prelude: handleDeep :: (MonadCatch m, Exception e, MonadIO m, NFData a) => (e -> m a) -> m a -> m a
+ Faktory.Prelude: handleDeep :: (HasCallStack, MonadCatch m, Exception e, MonadIO m, NFData a) => (e -> m a) -> m a -> m a
- Faktory.Prelude: handleIO :: MonadCatch m => (IOException -> m a) -> m a -> m a
+ Faktory.Prelude: handleIO :: (HasCallStack, MonadCatch m) => (IOException -> m a) -> m a -> m a
- Faktory.Prelude: handleIOError :: MonadCatch m => (IOError -> m a) -> m a -> m a
+ Faktory.Prelude: handleIOError :: (HasCallStack, MonadCatch m) => (IOError -> m a) -> m a -> m a
- Faktory.Prelude: handleJust :: (MonadCatch m, Exception e) => (e -> Maybe b) -> (b -> m a) -> m a -> m a
+ Faktory.Prelude: handleJust :: (HasCallStack, MonadCatch m, Exception e) => (e -> Maybe b) -> (b -> m a) -> m a -> m a
- Faktory.Prelude: head :: [a] -> a
+ Faktory.Prelude: head :: HasCallStack => [a] -> a
- Faktory.Prelude: impureThrow :: Exception e => e -> a
+ Faktory.Prelude: impureThrow :: (HasCallStack, Exception e) => e -> a
- Faktory.Prelude: infixl 6 -
+ Faktory.Prelude: infixl 6 +
- Faktory.Prelude: infixr 1 =<<
+ Faktory.Prelude: infixr 1 <=<
- Faktory.Prelude: infixr 8 ^
+ Faktory.Prelude: infixr 8 ^^
- Faktory.Prelude: init :: [a] -> [a]
+ Faktory.Prelude: init :: HasCallStack => [a] -> [a]
- Faktory.Prelude: last :: [a] -> a
+ Faktory.Prelude: last :: HasCallStack => [a] -> a
- Faktory.Prelude: mask :: MonadMask m => ((forall a. () => m a -> m a) -> m b) -> m b
+ Faktory.Prelude: mask :: (MonadMask m, HasCallStack) => ((forall a. () => m a -> m a) -> m b) -> m b
- Faktory.Prelude: mask_ :: MonadMask m => m a -> m a
+ Faktory.Prelude: mask_ :: (HasCallStack, MonadMask m) => m a -> m a
- Faktory.Prelude: onException :: MonadMask m => m a -> m b -> m a
+ Faktory.Prelude: onException :: (HasCallStack, MonadMask m) => m a -> m b -> m a
- Faktory.Prelude: tail :: [a] -> [a]
+ Faktory.Prelude: tail :: HasCallStack => [a] -> [a]
- Faktory.Prelude: throw :: (MonadThrow m, Exception e) => e -> m a
+ Faktory.Prelude: throw :: (HasCallStack, MonadThrow m, Exception e) => e -> m a
- Faktory.Prelude: throwIO :: (MonadThrow m, Exception e) => e -> m a
+ Faktory.Prelude: throwIO :: (HasCallStack, MonadThrow m, Exception e) => e -> m a
- Faktory.Prelude: throwM :: (MonadThrow m, Exception e) => e -> m a
+ Faktory.Prelude: throwM :: (HasCallStack, MonadThrow m, Exception e) => e -> m a
- Faktory.Prelude: try :: (MonadCatch m, Exception e) => m a -> m (Either e a)
+ Faktory.Prelude: try :: (HasCallStack, MonadCatch m, Exception e) => m a -> m (Either e a)
- Faktory.Prelude: tryAny :: MonadCatch m => m a -> m (Either SomeException a)
+ Faktory.Prelude: tryAny :: (HasCallStack, MonadCatch m) => m a -> m (Either SomeException a)
- Faktory.Prelude: tryAnyDeep :: (MonadCatch m, MonadIO m, NFData a) => m a -> m (Either SomeException a)
+ Faktory.Prelude: tryAnyDeep :: (HasCallStack, MonadCatch m, MonadIO m, NFData a) => m a -> m (Either SomeException a)
- Faktory.Prelude: tryAsync :: (MonadCatch m, Exception e) => m a -> m (Either e a)
+ Faktory.Prelude: tryAsync :: (HasCallStack, MonadCatch m, Exception e) => m a -> m (Either e a)
- Faktory.Prelude: tryDeep :: (MonadCatch m, MonadIO m, Exception e, NFData a) => m a -> m (Either e a)
+ Faktory.Prelude: tryDeep :: (HasCallStack, MonadCatch m, MonadIO m, Exception e, NFData a) => m a -> m (Either e a)
- Faktory.Prelude: tryIO :: MonadCatch m => m a -> m (Either IOException a)
+ Faktory.Prelude: tryIO :: (HasCallStack, MonadCatch m) => m a -> m (Either IOException a)
- Faktory.Prelude: tryJust :: (MonadCatch m, Exception e) => (e -> Maybe b) -> m a -> m (Either b a)
+ Faktory.Prelude: tryJust :: (HasCallStack, MonadCatch m, Exception e) => (e -> Maybe b) -> m a -> m (Either b a)
- Faktory.Prelude: uninterruptibleMask :: MonadMask m => ((forall a. () => m a -> m a) -> m b) -> m b
+ Faktory.Prelude: uninterruptibleMask :: (MonadMask m, HasCallStack) => ((forall a. () => m a -> m a) -> m b) -> m b
- Faktory.Prelude: uninterruptibleMask_ :: MonadMask m => m a -> m a
+ Faktory.Prelude: uninterruptibleMask_ :: (HasCallStack, MonadMask m) => m a -> m a
- Faktory.Prelude: withException :: (MonadMask m, Exception e) => m a -> (e -> m b) -> m a
+ Faktory.Prelude: withException :: (HasCallStack, MonadMask m, Exception e) => m a -> (e -> m b) -> m a
- Faktory.Settings: WorkerSettings :: Queue -> Maybe WorkerId -> Int -> WorkerSettings
+ Faktory.Settings: WorkerSettings :: Queue -> Maybe WorkerId -> Int -> (SomeException -> IO ()) -> WorkerSettings
- Network.Connection.Compat: data Connection
+ Network.Connection.Compat: data () => Connection
- Network.Connection.Compat: data ConnectionContext
+ Network.Connection.Compat: data () => ConnectionContext
- Network.Connection.Compat: data ConnectionParams
+ Network.Connection.Compat: data () => ConnectionParams
- Network.Connection.Compat: data HostCannotConnect
+ Network.Connection.Compat: data () => HostCannotConnect
- Network.Connection.Compat: data HostNotResolved
+ Network.Connection.Compat: data () => HostNotResolved
- Network.Connection.Compat: data LineTooLong
+ Network.Connection.Compat: data () => LineTooLong
- Network.Connection.Compat: data ProxySettings
+ Network.Connection.Compat: data () => ProxySettings
- Network.Connection.Compat: data TLSSettings
+ Network.Connection.Compat: data () => TLSSettings
Files
- CHANGELOG.md +6/−1
- faktory.cabal +1/−1
- library/Faktory/Job.hs +1/−0
- library/Faktory/JobFailure.hs +1/−0
- library/Faktory/Settings.hs +2/−0
- library/Faktory/Worker.hs +9/−4
- tests/Faktory/Ent/BatchSpec.hs +2/−2
- tests/Faktory/Test.hs +19/−11
- tests/FaktorySpec.hs +12/−0
CHANGELOG.md view
@@ -1,4 +1,9 @@-## [_Unreleased_](https://github.com/frontrowed/faktory_worker_haskell/compare/v1.1.2.6...main)+## [_Unreleased_](https://github.com/frontrowed/faktory_worker_haskell/compare/v1.1.2.7...main)++## [v1.1.2.7](https://github.com/frontrowed/faktory_worker_haskell/compare/v1.1.2.6...v1.1.2.7)++- Fix handling decoding errors of the Job payload. Previously, it would log an+ error and neither `ACK` nor `FAIL` the Job. Now it `FAIL`s the Job. ## [v1.1.2.6](https://github.com/frontrowed/faktory_worker_haskell/compare/v1.1.2.5...v1.1.2.6)
faktory.cabal view
@@ -1,6 +1,6 @@ cabal-version: 1.18 name: faktory-version: 1.1.2.6+version: 1.1.2.7 license: MIT license-file: LICENSE copyright: 2018 Freckle Education
library/Faktory/Job.hs view
@@ -48,6 +48,7 @@ , jobOptions :: JobOptions , jobFailure :: Maybe JobFailure }+ deriving stock (Show, Functor, Foldable, Traversable) -- | Perform a Job with the given options --
library/Faktory/JobFailure.hs view
@@ -15,6 +15,7 @@ , jfErrorType :: Maybe Text , jfBacktrace :: Maybe [Text] }+ deriving stock (Show) -- brittany-disable-next-binding
library/Faktory/Settings.hs view
@@ -55,6 +55,7 @@ { settingsQueue :: Queue , settingsId :: Maybe WorkerId , settingsIdleDelay :: Int+ , settingsOnFailed :: SomeException -> IO () } defaultWorkerSettings :: WorkerSettings@@ -63,6 +64,7 @@ { settingsQueue = defaultQueue , settingsId = Nothing , settingsIdleDelay = 1+ , settingsOnFailed = \_ -> pure () } envWorkerSettings :: IO WorkerSettings
library/Faktory/Worker.hs view
@@ -15,6 +15,7 @@ import Control.Concurrent (killThread) import Data.Aeson import Data.Aeson.Casing+import Data.Aeson.Types (parseEither) import qualified Data.Text as T import Faktory.Client import Faktory.Job (Job, JobId, jobArg, jobJid, jobReserveForMicroseconds)@@ -89,7 +90,8 @@ processorLoop client settings workerSettings f = do let namespace = connectionInfoNamespace $ settingsConnection settings- processAndAck job = do+ processAndAck job' = do+ job <- decodeJob job' mResult <- timeout (jobReserveForMicroseconds job) $ f job case mResult of Nothing -> settingsLogError settings "Job reservation period expired."@@ -107,18 +109,21 @@ Right (Just job) -> processAndAck job `catches` [ Handler $ \(ex :: WorkerHalt) -> throw ex- , Handler $ \(ex :: SomeException) ->+ , Handler $ \(ex :: SomeException) -> do+ settingsOnFailed workerSettings ex failJob client job $ T.pack $ show ex ] +decodeJob :: (HasCallStack, FromJSON arg) => Job Value -> IO (Job arg)+decodeJob = either throwString pure . traverse (parseEither parseJSON)+ -- | <https://github.com/contribsys/faktory/wiki/Worker-Lifecycle#heartbeat> heartBeat :: Client -> WorkerId -> IO () heartBeat client workerId = do threadDelaySeconds 25 command_ client "BEAT" [encode $ BeatPayload workerId] -fetchJob- :: FromJSON args => Client -> Queue -> IO (Either String (Maybe (Job args)))+fetchJob :: Client -> Queue -> IO (Either String (Maybe (Job Value))) fetchJob client queue = commandJSON client "FETCH" [queueArg queue] ackJob :: HasCallStack => Client -> Job args -> IO ()
tests/Faktory/Ent/BatchSpec.hs view
@@ -15,7 +15,7 @@ spec = do describe "jobBatchId" $ do it "parses the batchId out of batch jobs" $ do- (jobs, batchId) <- withWorker id $ withProducer $ \producer -> do+ (jobs, _, batchId) <- withWorker id $ withProducer $ \producer -> do c <- buildJob @Text mempty producer "c" d <- buildJob @Text mempty producer "d" let options = description "foo" <> complete c <> success d@@ -100,7 +100,7 @@ fmap jobArg jobs `shouldMatchList` ["a", "b", "c", "d", "HALT"] it "supports BATCH STATUS" $ do- (_, batchId) <- withWorker id $ withProducer $ \producer -> do+ (_, _, batchId) <- withWorker id $ withProducer $ \producer -> do c <- buildJob @Text mempty producer "c" d <- buildJob @Text mempty producer "d" let options = description "foo" <> complete c <> success d
tests/Faktory/Test.hs view
@@ -1,5 +1,3 @@-{-# LANGUAGE TupleSections #-}- module Faktory.Test ( module X , workerTestCase@@ -34,10 +32,8 @@ => (WorkerSettings -> WorkerSettings) -> (Producer -> IO ()) -> IO [Job Text]-workerTestCaseWith editSettings run = do- a <- startWorker editSettings- withProducer run- haltWorker a+workerTestCaseWith editSettings =+ fmap (\(x, _, _) -> x) . withWorker editSettings . withProducer withProducer :: (Producer -> IO a) -> IO a withProducer f = bracket newProducerEnv closeProducer f@@ -46,29 +42,41 @@ :: HasCallStack => (WorkerSettings -> WorkerSettings) -> IO a- -> IO ([Job Text], a)+ -> IO ([Job Text], [SomeException], a) withWorker editSettings f = do a <- startWorker editSettings result <- f- (,result) <$> haltWorker a+ (processed, failed) <- haltWorker a+ pure (processed, failed, result) startWorker- :: HasCallStack => (WorkerSettings -> WorkerSettings) -> IO (Async [Job Text])+ :: HasCallStack+ => (WorkerSettings -> WorkerSettings)+ -> IO (Async ([Job Text], [SomeException])) startWorker editSettings = do withProducer $ void . flush settings <- envSettings workerSettings <- editSettings <$> envWorkerSettings async $ do processedJobs <- newMVar []+ failedJobs <- newMVar [] - runWorker settings workerSettings $ \faktoryJob -> do+ let workerSettings' =+ workerSettings+ { settingsOnFailed = \ex ->+ modifyMVar_ failedJobs $ pure . (ex :)+ }++ runWorker settings workerSettings' $ \faktoryJob -> do let job = jobArg faktoryJob when (job == "WAIT") $ threadDelay 3000000 modifyMVar_ processedJobs $ pure . (faktoryJob :) when (job == "BOOM") $ throw $ userError "BOOM" when (job == "HALT") $ throw WorkerHalt - readMVar processedJobs+ (,)+ <$> readMVar processedJobs+ <*> readMVar failedJobs haltWorker :: Async a -> IO a haltWorker a = do
tests/FaktorySpec.hs view
@@ -63,3 +63,15 @@ void $ perform @Text (reserveFor 4) producer "WAIT" fmap jobArg jobs `shouldMatchList` ["WAIT", "HALT"]++ it "fails jobs that raise exceptions" $ do+ (_, failed, _) <- withWorker id $ withProducer $ \producer -> do+ void $ perform @Text mempty producer "BOOM"++ failed `shouldSatisfy` (== 1) . length++ it "fails jobs that didn't parse" $ do+ (_, failed, _) <- withWorker id $ withProducer $ \producer -> do+ void $ perform @Int mempty producer 42++ failed `shouldSatisfy` (== 1) . length