packages feed

faktory 1.0.2.3 → 1.0.3.0

raw patch · 8 files changed

+346/−106 lines, 8 filesdep +errorsPVP ok

version bump matches the API change (PVP)

Dependencies added: errors

API changes (from Hackage documentation)

+ Faktory.Ent.Tracking: CustomTrack :: Int -> CustomTrack
+ Faktory.Ent.Tracking: JobDetails :: JobId -> Maybe Int -> Maybe Text -> JobState -> Maybe UTCTime -> JobDetails
+ Faktory.Ent.Tracking: JobStateDead :: JobState
+ Faktory.Ent.Tracking: JobStateEnqueued :: JobState
+ Faktory.Ent.Tracking: JobStateFailed :: JobState
+ Faktory.Ent.Tracking: JobStateSuccess :: JobState
+ Faktory.Ent.Tracking: JobStateUnknown :: JobState
+ Faktory.Ent.Tracking: JobStateWorking :: JobState
+ Faktory.Ent.Tracking: SetJobDetails :: JobId -> Maybe Int -> Maybe Text -> Maybe UTCTime -> SetJobDetails
+ Faktory.Ent.Tracking: [jdDesc] :: JobDetails -> Maybe Text
+ Faktory.Ent.Tracking: [jdJid] :: JobDetails -> JobId
+ Faktory.Ent.Tracking: [jdPercent] :: JobDetails -> Maybe Int
+ Faktory.Ent.Tracking: [jdState] :: JobDetails -> JobState
+ Faktory.Ent.Tracking: [jdUpdatedAt] :: JobDetails -> Maybe UTCTime
+ Faktory.Ent.Tracking: [sjdDesc] :: SetJobDetails -> Maybe Text
+ Faktory.Ent.Tracking: [sjdJid] :: SetJobDetails -> JobId
+ Faktory.Ent.Tracking: [sjdPercent] :: SetJobDetails -> Maybe Int
+ Faktory.Ent.Tracking: [sjdReserveUntil] :: SetJobDetails -> Maybe UTCTime
+ Faktory.Ent.Tracking: [track] :: CustomTrack -> Int
+ Faktory.Ent.Tracking: data JobDetails
+ Faktory.Ent.Tracking: data JobState
+ Faktory.Ent.Tracking: data SetJobDetails
+ Faktory.Ent.Tracking: instance Data.Aeson.Types.FromJSON.FromJSON Faktory.Ent.Tracking.JobDetails
+ Faktory.Ent.Tracking: instance Data.Aeson.Types.ToJSON.ToJSON Faktory.Ent.Tracking.CustomTrack
+ Faktory.Ent.Tracking: instance Data.Aeson.Types.ToJSON.ToJSON Faktory.Ent.Tracking.SetJobDetails
+ Faktory.Ent.Tracking: instance GHC.Generics.Generic Faktory.Ent.Tracking.CustomTrack
+ Faktory.Ent.Tracking: instance GHC.Generics.Generic Faktory.Ent.Tracking.JobDetails
+ Faktory.Ent.Tracking: instance GHC.Generics.Generic Faktory.Ent.Tracking.SetJobDetails
+ Faktory.Ent.Tracking: newtype CustomTrack
+ Faktory.Ent.Tracking: trackGet :: Producer -> JobId -> IO (Either String (Maybe JobDetails))
+ Faktory.Ent.Tracking: trackGetHush :: Producer -> JobId -> IO JobDetails
+ Faktory.Ent.Tracking: trackPerform :: (HasCallStack, ToJSON arg) => JobOptions -> Producer -> arg -> IO JobId
+ Faktory.Ent.Tracking: trackSet :: HasCallStack => Producer -> SetJobDetails -> IO ()
+ Faktory.JobState: JobStateDead :: JobState
+ Faktory.JobState: JobStateEnqueued :: JobState
+ Faktory.JobState: JobStateFailed :: JobState
+ Faktory.JobState: JobStateSuccess :: JobState
+ Faktory.JobState: JobStateUnknown :: JobState
+ Faktory.JobState: JobStateWorking :: JobState
+ Faktory.JobState: data JobState
+ Faktory.JobState: instance Data.Aeson.Types.FromJSON.FromJSON Faktory.JobState.JobState
+ Faktory.JobState: instance Data.Aeson.Types.ToJSON.ToJSON Faktory.JobState.JobState
+ Faktory.JobState: instance GHC.Classes.Eq Faktory.JobState.JobState
+ Faktory.JobState: instance GHC.Enum.Bounded Faktory.JobState.JobState
+ Faktory.JobState: instance GHC.Enum.Enum Faktory.JobState.JobState
+ Faktory.JobState: instance GHC.Show.Show Faktory.JobState.JobState

Files

CHANGELOG.md view
@@ -1,6 +1,10 @@-## [*Unreleased*](https://github.com/frontrowed/faktory_worker_haskell/compare/v1.0.2.3...main)+## [*Unreleased*](https://github.com/frontrowed/faktory_worker_haskell/compare/v1.0.3.0...main)  None++## [v1.0.3.0](https://github.com/frontrowed/faktory_worker_haskell/compare/v1.0.2.3...v1.0.3.0)++- Support for `TRACK` (Enterprise only)  ## [v1.0.2.3](https://github.com/frontrowed/faktory_worker_haskell/compare/v1.0.2.2...v1.0.2.3) 
faktory.cabal view
@@ -1,6 +1,6 @@ cabal-version:   1.18 name:            faktory-version:         1.0.2.3+version:         1.0.3.0 license:         MIT license-file:    LICENSE copyright:       2018 Freckle Education@@ -54,9 +54,11 @@         Faktory.Client         Faktory.Connection         Faktory.Ent.Batch+        Faktory.Ent.Tracking         Faktory.Job         Faktory.Job.Custom         Faktory.JobOptions+        Faktory.JobState         Faktory.Prelude         Faktory.Producer         Faktory.Protocol@@ -88,6 +90,7 @@         bytestring >=0.10.12.0,         connection >=0.3.1,         cryptonite >=0.27,+        errors >=2.3.0,         megaparsec >=9.0.1,         memory >=0.15.0,         mtl >=2.2.2,@@ -171,7 +174,10 @@     hs-source-dirs:     tests     other-modules:         Faktory.ConnectionSpec+        Faktory.Ent.BatchSpec+        Faktory.Ent.TrackingSpec         Faktory.JobOptionsSpec+        Faktory.Test         FaktorySpec         Paths_faktory @@ -206,7 +212,7 @@ test-suite readme     type:               exitcode-stdio-1.0     main-is:            README.lhs-    hs-source-dirs:     ./.+    hs-source-dirs:     ./     other-modules:      Paths_faktory     default-language:   Haskell2010     default-extensions:
+ library/Faktory/Ent/Tracking.hs view
@@ -0,0 +1,95 @@+-- | Support for the @TRACK@ command (Enterprise only)+--+-- <https://github.com/contribsys/faktory/wiki/Ent-Tracking>+--+module Faktory.Ent.Tracking+  ( CustomTrack(..)+  , trackPerform++  , JobDetails(..)+  , JobState(..)+  , trackGet+  , trackGetHush++  , SetJobDetails(..)+  , trackSet+  ) where++import Faktory.Prelude++import Control.Error.Util (hush)+import Data.Aeson+import Data.Aeson.Casing+import qualified Data.ByteString.Lazy.Char8 as BSL8+import Data.Maybe (fromMaybe)+import Data.Time (UTCTime)+import Faktory.Client (commandJSON, commandOK)+import Faktory.Job (JobId, JobOptions, custom, perform)+import Faktory.JobState (JobState(..))+import Faktory.Producer+import GHC.Generics (Generic)+import GHC.Stack (HasCallStack)++newtype CustomTrack = CustomTrack+  { track :: Int+  }+  deriving stock Generic+  deriving anyclass ToJSON++-- | 'perform', but adding @{ custom: { track: 1 } }@+--+-- Equivalent to:+--+-- @+-- 'perform' ('custom' $ 'CustomTrack' 1)+-- @+--+trackPerform+  :: (HasCallStack, ToJSON arg) => JobOptions -> Producer -> arg -> IO JobId+trackPerform options = perform (options <> custom (CustomTrack 1))++data JobDetails = JobDetails+  { jdJid :: JobId+  , jdPercent :: Maybe Int+  , jdDesc :: Maybe Text+  , jdState :: JobState+  , jdUpdatedAt :: Maybe UTCTime+  }+  deriving stock Generic++instance FromJSON JobDetails where+  parseJSON = genericParseJSON $ aesonPrefix snakeCase++unknownJobDetails :: JobId -> JobDetails+unknownJobDetails jid = JobDetails+  { jdJid = jid+  , jdPercent = Nothing+  , jdDesc = Nothing+  , jdState = JobStateUnknown+  , jdUpdatedAt = Nothing+  }++data SetJobDetails = SetJobDetails+  { sjdJid :: JobId+  , sjdPercent :: Maybe Int+  , sjdDesc :: Maybe Text+  , sjdReserveUntil :: Maybe UTCTime+  }+  deriving stock Generic++instance ToJSON SetJobDetails where+  toJSON = genericToJSON $ aesonPrefix snakeCase+  toEncoding = genericToEncoding $ aesonPrefix snakeCase++trackGet :: Producer -> JobId -> IO (Either String (Maybe JobDetails))+trackGet producer jid =+  commandJSON (producerClient producer) "TRACK GET" [BSL8.pack jid]++-- | 'trackGet' but mask any failures to 'JobStateUnknown'+trackGetHush :: Producer -> JobId -> IO JobDetails+trackGetHush producer jid =+  fromMaybe (unknownJobDetails jid) . join . hush <$> trackGet producer jid++trackSet :: HasCallStack => Producer -> SetJobDetails -> IO ()+trackSet producer details =+  commandOK (producerClient producer) "TRACK SET" [encode details]
+ library/Faktory/JobState.hs view
@@ -0,0 +1,53 @@+module Faktory.JobState+  ( JobState(..)+  ) where++import Faktory.Prelude++import Control.Arrow ((&&&))+import Control.Error.Util (note)+import Data.Aeson+import Data.HashMap.Strict (HashMap)+import qualified Data.HashMap.Strict as HashMap+import qualified Data.Text as T++data JobState+  = JobStateUnknown+  | JobStateEnqueued+  | JobStateWorking+  | JobStateSuccess+  | JobStateFailed+  | JobStateDead+  deriving stock (Eq, Show, Bounded, Enum)++instance ToJSON JobState where+  toJSON = toJSON . jobStateToText+  toEncoding = toEncoding . jobStateToText++instance FromJSON JobState where+  parseJSON = withText "JobState" $ either fail pure . jobStateFromText++jobStateToText :: JobState -> Text+jobStateToText = \case+  JobStateUnknown -> "unknown"+  JobStateEnqueued -> "enqueued"+  JobStateWorking -> "working"+  JobStateSuccess -> "success"+  JobStateFailed -> "failed"+  JobStateDead -> "dead"++jobStateFromText :: Text -> Either String JobState+jobStateFromText x =+  note+      (unpack+      $ "Invalid JobState: "+      <> x+      <> ", must be one of "+      <> T.intercalate ", " (HashMap.keys jobStateMap)+      )+    $ HashMap.lookup x jobStateMap++jobStateMap :: HashMap Text JobState+jobStateMap =+  HashMap.fromList $ map (jobStateToText &&& id) [minBound .. maxBound]+{-# NOINLINE jobStateMap #-}
+ tests/Faktory/Ent/BatchSpec.hs view
@@ -0,0 +1,76 @@+module Faktory.Ent.BatchSpec+  ( spec+  ) where++import Faktory.Test++import Control.Concurrent (threadDelay)+import Faktory.Ent.Batch++spec :: Spec+spec = do+  describe "runBatch" $ do+    it "runs a success job if all in-batch jobs succeed" $ do+      jobs <- workerTestCase $ \producer -> do+        c <- buildJob @Text mempty producer "c"+        void $ runBatch (success c) producer $ do+          void $ batchPerform @Text mempty producer "a"+          void $ batchPerform @Text mempty producer "b"+        -- Give a little time for Faktory to fire the callback+        liftIO $ threadDelay 500000++      jobs `shouldMatchList` ["a", "b", "c", "HALT"]++    it "does not run a success job if all jobs don't succeed" $ do+      jobs <- workerTestCase $ \producer -> do+        c <- buildJob @Text mempty producer "c"+        void $ runBatch (success c) producer $ do+          void $ batchPerform @Text mempty producer "BOOM"+          void $ batchPerform @Text mempty producer "b"+        liftIO $ threadDelay 500000++      jobs `shouldMatchList` ["BOOM", "b", "HALT"]++    it "runs a job on complete" $ do+      jobs <- workerTestCase $ \producer -> do+        c <- buildJob @Text mempty producer "c"+        void $ runBatch (complete c) producer $ do+          void $ batchPerform @Text mempty producer "a"+          void $ batchPerform @Text mempty producer "b"+        liftIO $ threadDelay 500000++      jobs `shouldMatchList` ["a", "b", "c", "HALT"]++    it "runs a job on complete, even if in-batch jobs fail" $ do+      jobs <- workerTestCase $ \producer -> do+        c <- buildJob @Text mempty producer "c"+        void $ runBatch (complete c) producer $ do+          void $ batchPerform @Text mempty producer "BOOM"+          void $ batchPerform @Text mempty producer "b"+        liftIO $ threadDelay 500000++      jobs `shouldMatchList` ["BOOM", "b", "c", "HALT"]++    it "combines duplicate options in last-wins fashion" $ do+      jobs <- workerTestCase $ \producer -> do+        c <- buildJob @Text mempty producer "c"+        d <- buildJob @Text mempty producer "d"+        let options = description "foo" <> success c <> success d+        void $ runBatch options producer $ do+          void $ batchPerform @Text mempty producer "a"+          void $ batchPerform @Text mempty producer "b"+        liftIO $ threadDelay 500000++      jobs `shouldMatchList` ["a", "b", "d", "HALT"]++    it "runs success and complete if all Jobs were successful" $ do+      jobs <- workerTestCase $ \producer -> do+        c <- buildJob @Text mempty producer "c"+        d <- buildJob @Text mempty producer "d"+        let options = description "foo" <> complete c <> success d+        void $ runBatch options producer $ do+          void $ batchPerform @Text mempty producer "a"+          void $ batchPerform @Text mempty producer "b"+        liftIO $ threadDelay 500000++      jobs `shouldMatchList` ["a", "b", "c", "d", "HALT"]
+ tests/Faktory/Ent/TrackingSpec.hs view
@@ -0,0 +1,62 @@+{-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-}++module Faktory.Ent.TrackingSpec+  ( spec+  ) where++import Faktory.Test++import Control.Concurrent.MVar+import Faktory.Ent.Tracking++spec :: Spec+spec = do+  describe "trackPerform" $ do+    it "enqueues with TRACK so we can get details" $ do+      var <- newMVar []+      void $ workerTestCase $ \producer -> do+        a <- trackPerform @Text mempty producer "a"+        modifyMVar_ var $ pure . (<> [a])+        b <- trackPerform @Text mempty producer "BOOM"+        modifyMVar_ var $ pure . (<> [b])++      enqueuedJobIds <- readMVar var+      enqueuedJobIds `shouldSatisfy` (== 2) . length++      let [aJid, bJid] = enqueuedJobIds++      (aDetails, bDetails, cDetails) <-+        bracket newProducerEnv closeProducer $ \producer ->+          (,,)+            <$> trackGetHush producer aJid+            <*> trackGetHush producer bJid+            <*> trackGetHush producer "madeUp"++      jdState aDetails `shouldBe` JobStateSuccess+      jdState bDetails `shouldBe` JobStateFailed+      jdState cDetails `shouldBe` JobStateUnknown++  describe "trackSet" $ do+    it "updates Job Details" $ do+      var <- newMVar []+      void $ workerTestCase $ \producer -> do+        a <- trackPerform @Text mempty producer "a"+        modifyMVar_ var $ pure . (<> [a])++      enqueuedJobIds <- readMVar var+      enqueuedJobIds `shouldSatisfy` (== 1) . length++      let [aJid] = enqueuedJobIds++      aDetails <- bracket newProducerEnv closeProducer $ \producer -> do+        trackSet producer $ SetJobDetails+          { sjdJid = aJid+          , sjdPercent = Just 100+          , sjdDesc = Just "Updated"+          , sjdReserveUntil = Nothing+          }++        trackGetHush producer aJid++      jdPercent aDetails `shouldBe` Just 100+      jdDesc aDetails `shouldBe` Just "Updated"
+ tests/Faktory/Test.hs view
@@ -0,0 +1,46 @@+module Faktory.Test+  ( module X+  , workerTestCase+  , workerTestCaseWith+  )+where++import Faktory.Prelude as X++import Control.Monad.IO.Class as X (MonadIO(..))+import Faktory.Job as X+import Faktory.Producer as X+import Test.Hspec as X++import Control.Concurrent.Async+import Control.Concurrent.MVar+import Faktory.Settings+import Faktory.Worker++workerTestCase :: HasCallStack => (Producer -> IO ()) -> IO [Text]+workerTestCase = workerTestCaseWith id++workerTestCaseWith+  :: HasCallStack+  => (WorkerSettings -> WorkerSettings)+  -> (Producer -> IO ())+  -> IO [Text]+workerTestCaseWith editSettings run = do+  bracket newProducerEnv closeProducer $ \producer -> do+    void $ flush producer++  settings <- envSettings+  workerSettings <- editSettings <$> envWorkerSettings++  processedJobs <- newMVar []+  a <- async $ runWorker settings workerSettings $ \job -> do+    modifyMVar_ processedJobs $ pure . (job :)+    when (job == "BOOM") $ throw $ userError "BOOM"+    when (job == "HALT") $ throw WorkerHalt++  bracket newProducerEnv closeProducer $ \producer -> do+    run producer+    void $ perform @Text mempty producer "HALT"++  void $ wait a+  readMVar processedJobs
tests/FaktorySpec.hs view
@@ -2,19 +2,11 @@   ( spec   ) where -import Faktory.Prelude+import Faktory.Test  import Control.Concurrent (threadDelay)-import Control.Concurrent.Async-import Control.Concurrent.MVar-import Control.Monad.IO.Class (liftIO) import Data.Time (getCurrentTime)-import Faktory.Ent.Batch-import Faktory.Job-import Faktory.Producer import Faktory.Settings-import Faktory.Worker-import Test.Hspec  spec :: Spec spec = describe "Faktory" $ do@@ -59,97 +51,3 @@       threadDelay $ 2 * 1000000 + 250000      jobs `shouldMatchList` ["HALT"]--  context "BATCH" $ do-    it "runs a success job if all in-batch jobs succeed" $ do-      jobs <- workerTestCase $ \producer -> do-        c <- buildJob @Text mempty producer "c"-        void $ runBatch (success c) producer $ do-          void $ batchPerform @Text mempty producer "a"-          void $ batchPerform @Text mempty producer "b"-        -- Give a little time for Faktory to fire the callback-        liftIO $ threadDelay 500000--      jobs `shouldMatchList` ["a", "b", "c", "HALT"]--    it "does not run a success job if all jobs don't succeed" $ do-      jobs <- workerTestCase $ \producer -> do-        c <- buildJob @Text mempty producer "c"-        void $ runBatch (success c) producer $ do-          void $ batchPerform @Text mempty producer "BOOM"-          void $ batchPerform @Text mempty producer "b"-        liftIO $ threadDelay 500000--      jobs `shouldMatchList` ["BOOM", "b", "HALT"]--    it "runs a job on complete" $ do-      jobs <- workerTestCase $ \producer -> do-        c <- buildJob @Text mempty producer "c"-        void $ runBatch (complete c) producer $ do-          void $ batchPerform @Text mempty producer "a"-          void $ batchPerform @Text mempty producer "b"-        liftIO $ threadDelay 500000--      jobs `shouldMatchList` ["a", "b", "c", "HALT"]--    it "runs a job on complete, even if in-batch jobs fail" $ do-      jobs <- workerTestCase $ \producer -> do-        c <- buildJob @Text mempty producer "c"-        void $ runBatch (complete c) producer $ do-          void $ batchPerform @Text mempty producer "BOOM"-          void $ batchPerform @Text mempty producer "b"-        liftIO $ threadDelay 500000--      jobs `shouldMatchList` ["BOOM", "b", "c", "HALT"]--    it "combines duplicate options in last-wins fashion" $ do-      jobs <- workerTestCase $ \producer -> do-        c <- buildJob @Text mempty producer "c"-        d <- buildJob @Text mempty producer "d"-        let options = description "foo" <> success c <> success d-        void $ runBatch options producer $ do-          void $ batchPerform @Text mempty producer "a"-          void $ batchPerform @Text mempty producer "b"-        liftIO $ threadDelay 500000--      jobs `shouldMatchList` ["a", "b", "d", "HALT"]--    it "runs success and complete if all Jobs were successful" $ do-      jobs <- workerTestCase $ \producer -> do-        c <- buildJob @Text mempty producer "c"-        d <- buildJob @Text mempty producer "d"-        let options = description "foo" <> complete c <> success d-        void $ runBatch options producer $ do-          void $ batchPerform @Text mempty producer "a"-          void $ batchPerform @Text mempty producer "b"-        liftIO $ threadDelay 500000--      jobs `shouldMatchList` ["a", "b", "c", "d", "HALT"]--workerTestCase :: HasCallStack => (Producer -> IO ()) -> IO [Text]-workerTestCase = workerTestCaseWith id--workerTestCaseWith-  :: HasCallStack-  => (WorkerSettings -> WorkerSettings)-  -> (Producer -> IO ())-  -> IO [Text]-workerTestCaseWith editSettings run = do-  bracket newProducerEnv closeProducer $ \producer -> do-    void $ flush producer--  settings <- envSettings-  workerSettings <- editSettings <$> envWorkerSettings--  processedJobs <- newMVar []-  a <- async $ runWorker settings workerSettings $ \job -> do-    modifyMVar_ processedJobs $ pure . (job :)-    when (job == "BOOM") $ throw $ userError "BOOM"-    when (job == "HALT") $ throw WorkerHalt--  bracket newProducerEnv closeProducer $ \producer -> do-    run producer-    void $ perform @Text mempty producer "HALT"--  void $ wait a-  readMVar processedJobs