jobqueue (empty) → 0.1.3
raw patch · 20 files changed
+1651/−0 lines, 20 filesdep +HDBCdep +HDBC-sqlite3dep +QuickChecksetup-changed
Dependencies added: HDBC, HDBC-sqlite3, QuickCheck, async, attoparsec, base, bytestring, containers, data-default, directory, fast-logger, hslogger, hspec, hzk, jobqueue, lifted-base, monad-control, monad-logger, mtl, network, split, stm, template-haskell, text, text-format, time, transformers-base
Files
- LICENSE +22/−0
- Setup.hs +2/−0
- jobqueue.cabal +80/−0
- src/Network/JobQueue.hs +178/−0
- src/Network/JobQueue/Action.hs +199/−0
- src/Network/JobQueue/AuxClass.hs +41/−0
- src/Network/JobQueue/Backend.hs +49/−0
- src/Network/JobQueue/Backend/Class.hs +19/−0
- src/Network/JobQueue/Backend/Sqlite3.hs +112/−0
- src/Network/JobQueue/Backend/Types.hs +24/−0
- src/Network/JobQueue/Backend/Zookeeper.hs +58/−0
- src/Network/JobQueue/Backend/Zookeeper/ZookeeperQueue.hs +228/−0
- src/Network/JobQueue/Class.hs +47/−0
- src/Network/JobQueue/Job.hs +36/−0
- src/Network/JobQueue/Job/Internal.hs +67/−0
- src/Network/JobQueue/JobQueue.hs +185/−0
- src/Network/JobQueue/JobQueue/Internal.hs +112/−0
- src/Network/JobQueue/Logger.hs +41/−0
- src/Network/JobQueue/Types.hs +129/−0
- test/Main.hs +22/−0
+ LICENSE view
@@ -0,0 +1,22 @@+Copyright (c) 2013 GREE, Inc.++MIT License++Permission is hereby granted, free of charge, to any person obtaining+a copy of this software and associated documentation files (the+"Software"), to deal in the Software without restriction, including+without limitation the rights to use, copy, modify, merge, publish,+distribute, sublicense, and/or sell copies of the Software, and to+permit persons to whom the Software is furnished to do so, subject to+the following conditions:++The above copyright notice and this permission notice shall be+included in all copies or substantial portions of the Software.++THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ jobqueue.cabal view
@@ -0,0 +1,80 @@+Name: jobqueue+Version: 0.1.3+Synopsis: A job queue library+License: MIT+License-File: LICENSE+Author: Kiyoshi Ikehara+Maintainer: kiyoshi.ikehara at gree.net+Copyright: GREE, Inc.+Build-Type: Simple+Category: Network, Client+Cabal-Version: >=1.8+Description:+ Haskell JobQueue is a library used for building a job scheduler with priority queues.+ The state of jobs is stored in a backend database such as Apache Zookeeper or other + highly reliable message queue systems.++Source-Repository head+ type: git+ location: https://github.com/gree/haskell-jobqueue.git++Library+ Ghc-Options: -Wall+ Build-Depends: base >= 4 && < 5+ , mtl > 2.2+ , network >= 2.3.2+ , hslogger+ , text+ , bytestring+ , containers+ , split+ , time+ , data-default+ , stm >= 2.4+ , hzk >= 2.0.0+ , attoparsec+ , data-default+ , HDBC+ , HDBC-sqlite3+ , fast-logger+ , monad-logger+ , template-haskell+ , text-format+ , monad-control+ , transformers-base+ , lifted-base+ Hs-source-dirs: src+ Exposed-modules: Network.JobQueue+ , Network.JobQueue.Class+ , Network.JobQueue.Types+ , Network.JobQueue.Action+ , Network.JobQueue.AuxClass+ , Network.JobQueue.Job+ , Network.JobQueue.Job.Internal+ , Network.JobQueue.JobQueue+ , Network.JobQueue.JobQueue.Internal+ , Network.JobQueue.Backend+ , Network.JobQueue.Backend.Class+ , Network.JobQueue.Backend.Types+ , Network.JobQueue.Backend.Zookeeper+ , Network.JobQueue.Backend.Sqlite3+ , Network.JobQueue.Logger+ Other-modules: Network.JobQueue.Backend.Zookeeper.ZookeeperQueue+ Extensions: DeriveDataTypeable++Test-Suite test+ Ghc-Options: -threaded -O2+ Build-depends: base+ , bytestring+ , network >= 2.3.2+ , directory+ , QuickCheck == 2.5.1.1+ , hspec+ , async+ , jobqueue+ , data-default+ , stm+ Type: exitcode-stdio-1.0+ Hs-source-dirs: test+ Main-is: Main.hs+
+ src/Network/JobQueue.hs view
@@ -0,0 +1,178 @@+{- |+ Module : Network.JobQueue+ Copyright : (c) Gree, Inc. 2013+ License : MIT-style+ + Maintainer : Kiyoshi Ikehara <kiyoshi.ikehara@gree.net>+ Stability : experimental+ Portability : portable++Haskell JobQueue is a library used for building a job scheduler with a priority queue.+The state of a job is stored in a backend database such as Apache Zookeeper or other +highly reliable mesage queue systems.++[@Unit@]++Unit represents each state in an entire state machine. Units are described as value+constructors in Haskell code.+Unit itself is not executable. To execute using job queue system, extra information such+as job identifier, scheduled time is needed. An instance of a unit is wrapped by a 'job'+and stored into the job queue with those information.++The code shown below describes how to define a Unit.++> data JobUnit = HelloStep | WorldStep deriving (Show, Read)+> +> instance Unit JobUnit where++In this case, you define JobUnit type with 2 states, HelloStep and WorldStep.+This is the entire state machine of your job queue system.+You can define nested or child state machines by defining more complex data types as +long as they are serializable with read and show functions.++For more information, see "Network.JobQueue.Class".++[@Job@]++Each task executed by state machines (such as checking server state or repairing a+cluster) is called a 'job'.++A job is described as a particular state of a state machine. Each state only does one+thing (especially for modifying operations).+This prevents jobs ending in a failure state, which the state machine is unable to handle.++You don't have to know the internal data structure of a job, but need to understand+its when you write action code.++For more information, see "Network.JobQueue.Job".++[@Environment@]++Each unit can contain information used in the action of the state. But in many cases,+there is some information used by almost all states and it is convenient if there is +some kind of global data set that is accessible from all the state's actions.++For this reason, you can define global data structures called environment.+The enviroment can be retrieved using getEnv function in action monad.++> env <- getEnv++For more information, see "Network.JobQueue.Class".++[@Action@]++An action is a function that is called with a unit. You can define actions with the +"process" function.++> let withJobQueue = buildJobQueue loc name $ do+> process $ \WorldStep -> commitIO (putStrLn "world") >> fin+> process $ \HelloStep -> commitIO (putStr "hello, ") >> next WorldStep++In general, an action does the following things:++ * check if the precondition of the state is satisfied or not++ * do the action associated with the state++ * check the postcondition and return the next state.++For more information, see "Network.JobQueue.Action".++-}++{-# LANGUAGE ScopedTypeVariables #-}++module Network.JobQueue (+ buildJobQueue+ , runJobQueue+ , Job(..)+ , JobState(..)+ , Unit(..)+ , ActionM+ , JobM+ , JobActionState+ , process+ , createJob+ , fin+ , none+ , next+ , orNext+ , yield+ , fork+ , forkInTime+ , forkOnTime+ , abort+ , getEnv+ , param+ , commitIO+ , liftIO+ , module Network.JobQueue.Class+ , module Network.JobQueue.AuxClass+ , module Network.JobQueue.JobQueue+ , module Network.JobQueue.Logger+ ) where++import Prelude hiding (log)+import Control.Exception+import Control.Monad++import Network.JobQueue.Types+import Network.JobQueue.Class+import Network.JobQueue.AuxClass+import Network.JobQueue.JobQueue+import Network.JobQueue.Job+import Network.JobQueue.Logger++{- | Build a function that takes a function (('JobQueue' a -> 'IO' ()) -> IO ()) as its first parameter.++The following code executes jobs as long as the queue is not empty.++> main' loc name = do+> let withJobQueue = buildJobQueue loc name $ do+> process $ \WorldStep -> commitIO (putStrLn "world") >> fin+> process $ \HelloStep -> commitIO (putStr "hello, ") >> next WorldStep+> withJobQueue $ loop (initJobEnv loc name [])+> where+> loop env jq = do+> executeJob jq env+> count <- countJobQueue jq+> when (count > 0) $ loop env jq++The following code registers a job with initial state.++> main' loc name = do+> let withJobQueue = buildJobQueue loc name $ do+> process $ \WorldStep -> commitIO (putStrLn "world") >> fin+> process $ \HelloStep -> commitIO (putStr "hello, ") >> next WorldStep+> withJobQueue $ \jq -> scheduleJob jq HelloStep++-}+buildJobQueue :: (Env e, Unit a) => String -- ^ locator (ex.\"zookeeper:\/\/192.168.0.1\/myapp\")+ -> String -- ^ queue name (ex. \"/jobqueue\")+ -> JobM e a () -- ^ job construction function+ -> ((JobQueue e a -> IO ()) -> IO ()) -- ^ job queue executor+buildJobQueue loc name jobm = \action -> do+ bracket (openSession loc) (closeSession) $ \session -> do+ jq <- openJobQueue session name jobm+ action jq+ closeJobQueue jq++{- | Run a job queue while there is at least one job in the queue.+-}+runJobQueue :: (Aux e, Env e, Unit a)+ => e+ -> String -- ^ locator (ex.\"zookeeper:\/\/192.168.0.1\/myapp\")+ -> String -- ^ queue name (ex. \"/jobqueue\")+ -> JobM e a () -- ^ job construction function+ -> IO ()+runJobQueue env loc name jobm = buildJobQueue loc name jobm loop+ where+ loop jq = do+ executeJob jq env+ count <- countJobQueue jq+ when (count > 0) $ loop jq++----------------------------------------------------------------------+-- Docs+----------------------------------------------------------------------+
+ src/Network/JobQueue/Action.hs view
@@ -0,0 +1,199 @@+-- Copyright (c) Gree, Inc. 2013+-- License: MIT-style++{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TemplateHaskell #-}++module Network.JobQueue.Action (+ JobActionState+ , runActionState+ , runAction+ , getEnv+ , param+ , next+ , orNext+ , fin+ , none+ , fork+ , forkInTime+ , forkOnTime+ , abort+ , commitIO+ , liftIO+ , yield+ ) where++import Control.Applicative+import Control.Monad.Except+import Control.Monad.Reader+import Control.Monad.State+import Control.Exception (SomeException(..), toException)+import Control.Exception.Base (PatternMatchFail(..))+import Control.Monad.Logger (runLoggingT)+import Control.Exception.Lifted (catch)+import Control.Monad.Base ()++import Data.Maybe+import Data.Time.Clock+import Data.Default (Default, def)++import Network.JobQueue.Class+import Network.JobQueue.AuxClass+import Network.JobQueue.Types+import Network.JobQueue.Logger++runActionState :: (Env e, Unit a) => JobActionState e a -> ActionFn e a+runActionState (JobActionState { jobActions = actions } ) env ju = runActionState' actions+ where+ runActionState' actions' = case actions' of+ [] -> return $ Right Nothing+ (act:acts) -> do+ r <- act env ju+ case r of+ Right Nothing -> runActionState' acts+ _ -> return r++runAction :: (Aux e, Env e, Unit a) => + e -> a -> ActionT e a IO () -> IO (Either Break (Maybe (RuntimeState a)))+runAction env ju action = do+ (e,r) <- flip runLoggingT (auxLogger env)+ $ flip runStateT Nothing+ $ flip runReaderT (ActionEnv env ju)+ $ runExceptT+ $ runAM $ do+ when (toBeLogged ju) $ $(logWarn) "{}" [desc ju]+ action `catch` handlePatternMatchFail `catch` handleSome+ return $ either Left (const $ Right r) e++handlePatternMatchFail :: (Aux e, Env e, Unit a) => PatternMatchFail -> ActionT e a IO ()+handlePatternMatchFail e = do+ s <- get+ if getCommits (fromMaybe def s) > 0+ then do+ ju <- getJobUnit <$> ask+ $(logError) "pattern match fail: ! ({})" [desc ju]+ throwError $ Unhandled (toException e)+ else none++handleSome :: (Aux e, Env e, Unit a) => SomeException -> ActionT e a IO b+handleSome e = do+ $(logError) "unhandled exception: {}" [show e]+ throwError $ Unhandled e++--------------------------------++{- | Get environment in action.+-}+getEnv :: (Env e, Unit a) => ActionM e a e+getEnv = getJobEnv <$> ask++{- | Get a parameter value with a key from the environment in action.+ This is a special function for ParamEnv.+-}+param :: (ParamEnv e, Unit a, Read b) => (String, String) -> ActionM e a b+param (key, defaultValue) = do+ env <- getEnv+ case maybeRead defaultValue of+ Nothing -> do+ $(logCritical) "internal error. no parse: " [show (key, defaultValue)]+ abort+ Just defaultValue' -> case lookup key (envParameters env) of+ Just value -> return (fromMaybe defaultValue' (maybeRead value))+ Nothing -> return (defaultValue')+ where+ maybeRead = fmap fst . listToMaybe . reads+ +----------------++{- | Do a dirty I/O action with a side effect to the external system.+ If it doesn't change the state of the external system, you should use liftIO instead.+-}+commitIO :: (Env e, Unit a) => IO b -> ActionM e a b+commitIO action = do+ do s <- get+ when (getCommits (fromMaybe def s) > 0) $ do+ ju <- getJobUnit <$> ask+ $(logWarn) "commitIO called twice! ({})" [desc ju]+ modify $ \s -> Just $ incrementCommits $ fromMaybe def s+ liftIO action++----------------++{- | Yield execution+-}+yield :: (Env e, Unit a) => ActionM e a ()+yield = do+ ju <- getJobUnit <$> ask+ forkWith ju Nothing++{- | Create a job with a unit and schedule it.+-}+fork :: (Env e, Unit a)+ => a -- ^ a unit+ -> ActionM e a ()+fork ju = forkWith ju Nothing++{- | Create a job with a unit and schedule it at a specific time.+-}+forkOnTime :: (Env e, Unit a)+ => UTCTime -- ^ absolute time in UTC+ -> a -- ^ a unit+ -> ActionM e a ()+forkOnTime t ju = forkWith ju (Just t)++{- | Create a job with a unit and schedule it after a few micro seconds.+-}+forkInTime :: (Env e, Unit a) => NominalDiffTime -> a -> ActionM e a ()+forkInTime tDiff ju = do+ currentTime <- liftIO $ getCurrentTime+ forkWith ju (Just (addUTCTime tDiff currentTime))++{- | Move to the next state immediately.+ After the execution of the action the job being processed will be+ moved to the given state. The next action will be invoked immediately+ and can continue to work without being interrupted by another job.+ NOTE: This overrides the next state if it is already set.+-}+next :: (Env e, Unit a)+ => a -- ^ the next state+ -> ActionM e a ()+next ju = modify $ \s -> Just $ setNextJob ju $ fromMaybe def s++{- | Move to the next state immediately.+ This is different from "next" function because this doesn't override+ if the next job is already set.+-}+orNext :: (Env e, Unit a)+ => a -- ^ the next state+ -> ActionM e a ()+orNext ju = modify $ \s -> Just $ setNextJobIfEmpty ju $ fromMaybe def s++{- | Finish a job.+-}+fin :: (Env e, Unit a) => ActionM e a ()+fin = modify $ \s -> Just $ emptyNextJob $ fromMaybe def s++{- | If the unit passed by the job queue system cannot be processed by the+ action function, the function should call this.+-}+none :: (Env e, Unit a) => ActionM e a ()+none = result Nothing++{- | Abort the execution of a state machine.+ If a critical problem is found and there is a need to switch to the failure state,+ call this function.+-}+abort :: (Env e, Unit a) => ActionM e a b+abort = do+ ju <- getJobUnit <$> ask+ throwError $ Failure ("aborted on " ++ desc ju)++---------------------------------------------------------------- PRIVATE++{- | Set the result of the action. (for internal use)+-}+result :: (Env e, Unit a) => Maybe (RuntimeState a) -> ActionM e a ()+result = modify . setResult++forkWith :: (Env e, Unit a) => a -> Maybe UTCTime -> ActionM e a ()+forkWith ju mt = modify $ \s -> Just $ addForkJob (ju, mt) $ fromMaybe def s
+ src/Network/JobQueue/AuxClass.hs view
@@ -0,0 +1,41 @@+-- Copyright (c) Gree, Inc. 2014+-- License: MIT-style++{-# LANGUAGE OverloadedStrings #-}++module Network.JobQueue.AuxClass where++import Control.Monad.Logger+import Control.Applicative+import System.Log.FastLogger+import System.Log.Logger+import System.Environment (getProgName)++import Network.JobQueue.Types+import Network.JobQueue.Job.Internal++import qualified Data.ByteString.Char8 as S8++class Aux a where+ auxLogger :: a -> Loc -> LogSource -> LogLevel -> LogStr -> IO ()+ auxLogger _ loc logsrc loglevel msg = do+ progName <- getProgName+ logFunc loglevel progName $ S8.unpack $ fromLogStr $ defaultLogStr loc logsrc loglevel msg+ where+ logFunc level = case level of+ LevelDebug -> debugM+ LevelInfo -> infoM+ LevelWarn -> warningM+ LevelError -> errorM+ LevelOther "notice" -> noticeM+ LevelOther "critical" -> criticalM+ LevelOther _ -> warningM++ auxHandleFailure :: (Unit b) => a -> Maybe (Job b) -> IO (Maybe (Job b))+ auxHandleFailure _ mjob = do+ case mjob of+ Just job -> Just <$> createJob Runnable (getRecovery (jobUnit job))+ Nothing -> return (Nothing)++ auxHandleAfterExecute :: (Unit b) => a -> Job b -> IO ()+ auxHandleAfterExecute _ _job = return ()
+ src/Network/JobQueue/Backend.hs view
@@ -0,0 +1,49 @@+-- Copyright (c) Gree, Inc. 2013+-- License: MIT-style++{-# LANGUAGE OverloadedStrings #-}++module Network.JobQueue.Backend (openBackend) where++import Prelude hiding (takeWhile)+import Control.Exception (throwIO)+import qualified Data.ByteString.Char8 as BS+import Network.JobQueue.Backend.Class ()+import Network.JobQueue.Backend.Types+import Network.JobQueue.Backend.Zookeeper+import Network.JobQueue.Backend.Sqlite3++import Data.Attoparsec.ByteString+import Control.Applicative++data Locator = Zookeeper String | Sqlite3 String+++{- | Open a backend database.+-}+openBackend :: String -- ^ locator (eg. \"zookeeper://localhost:2181/myapp\", \"sqlite3://myapp.sqlite3\")+ -> IO Backend -- ^ backend+openBackend locator = case parseLocator locator of+ Just (Zookeeper connString) -> openZookeeperBackend connString+ Just (Sqlite3 localPath) -> openSqlite3Backend localPath+ _ -> throwIO $ userError "invalid locator"+++---------------------------------------------------------------- PRIVATE++parseLocator :: String -> Maybe Locator+parseLocator v = case parse locatorParser $ BS.pack v of+ Done _ locator -> Just locator+ Partial parse' -> case parse' "" of+ Done _ locator -> Just locator+ _ -> Nothing+ _ -> Nothing+ where+ locatorParser :: Parser (Locator)+ locatorParser = do+ scheme <- takeWhile (/= 58) <* string "://"+ case scheme of+ "zookeeper" -> Zookeeper <$> fmap BS.unpack (takeWhile1 (\_ -> True))+ "sqlite3" -> Sqlite3 <$> fmap BS.unpack (takeWhile1 (\_ -> True))+ _ -> fail "unknown scheme"+
+ src/Network/JobQueue/Backend/Class.hs view
@@ -0,0 +1,19 @@+-- Copyright (c) Gree, Inc. 2013+-- License: MIT-style++module Network.JobQueue.Backend.Class (BackendQueue(..)) where++import qualified Data.ByteString.Char8 as BS++class BackendQueue q where+ readQueue :: q -> IO (Maybe (BS.ByteString, String))+ peekQueue :: q -> IO (Maybe (BS.ByteString, String, String, Int))+ updateQueue :: q -> String -> BS.ByteString -> Int -> IO (Bool)+ deleteQueue :: q -> String -> IO (Bool)+ writeQueue :: q -> BS.ByteString -> Int -> IO (String)+ listQueue :: q -> IO ([BS.ByteString])+ itemsQueue :: q -> IO ([String])+ countQueue :: q -> IO (Int)+ closeQueue :: q -> IO ()+ closeQueue _ = return ()+
+ src/Network/JobQueue/Backend/Sqlite3.hs view
@@ -0,0 +1,112 @@+-- Copyright (c) Gree, Inc. 2013+-- License: MIT-style+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE ScopedTypeVariables #-}++module Network.JobQueue.Backend.Sqlite3 (openSqlite3Backend, newSqlite3Backend) where++import qualified Data.ByteString.Char8 as BS+import Database.HDBC+import Database.HDBC.Sqlite3+import Network.JobQueue.Backend.Types+import Network.JobQueue.Backend.Class+import Control.Concurrent.MVar+import Control.Exception++data Sqlite3Queue = Sqlite3Queue+ { conn :: Connection+ , queueName :: String+ , mlock :: MVar ()+ }++instance BackendQueue Sqlite3Queue where+ readQueue = readDBQueue+ peekQueue = peekDBQueue+ updateQueue = updateDBQueue+ deleteQueue = deleteDBQueue+ writeQueue = writeDBQueue+ listQueue = listDBQueue+ itemsQueue = itemsDBQueue+ countQueue = countDBQueue+ closeQueue = const $ return ()++openSqlite3Backend :: String -> IO Backend+openSqlite3Backend filePath = do+ c <- connectSqlite3 filePath+ m <- newMVar ()+ return $ Backend {+ bOpenQueue = \qn -> do+ _ <- withLock m $ withTransaction c $ \c' -> do+ run c' ("CREATE TABLE IF NOT EXISTS '" ++ qn ++ "' (key INTEGER PRIMARY KEY AUTOINCREMENT, prio INTEGER, value TEXT, version INTEGER)") []+ return (Sqlite3Queue c qn m)+ , bClose = disconnect c+ }++newSqlite3Backend :: Connection -> IO Backend+newSqlite3Backend c = do+ m <- newMVar ()+ return $ Backend {+ bOpenQueue = \qn -> do+ return (Sqlite3Queue c qn m)+ , bClose = return ()+ }++readDBQueue :: Sqlite3Queue -> IO (Maybe (BS.ByteString, String))+readDBQueue Sqlite3Queue {..} = withLock mlock $ withTransaction conn $ \conn' -> do+ sqlvalues <- quickQuery' conn' ("SELECT key, value FROM '" ++ queueName ++ "' ORDER BY prio, key LIMIT 1") []+ case sqlvalues of+ ((key:value:_):_) -> do+ _ <- run conn' ("DELETE FROM '" ++ queueName ++ "' WHERE key = ?") [toSql key]+ return (Just (fromSql value, fromSql key))+ _ -> return (Nothing)++peekDBQueue :: Sqlite3Queue -> IO (Maybe (BS.ByteString, String, String, Int))+peekDBQueue Sqlite3Queue {..} = withLock mlock $ withTransaction conn $ \conn' -> do+ sqlvalues <- quickQuery' conn' ("SELECT key, value, version FROM '" ++ queueName ++ "' ORDER BY prio, key LIMIT 1") []+ case sqlvalues of+ ((key:value:version:_):_) -> return (Just (fromSql value, fromSql key, fromSql key, fromSql version))+ _ -> return (Nothing)++writeDBQueue :: Sqlite3Queue -> BS.ByteString -> Int -> IO (String)+writeDBQueue Sqlite3Queue {..} value prio = do+ withLock mlock $ withTransaction conn $ \conn' -> do+ _ <- run conn' ("INSERT INTO '" ++ queueName ++ "'(prio, value, version) VALUES (?,?,0)") [toSql prio, toSql value]+ sqlvalues <- quickQuery' conn' ("SELECT seq FROM sqlite_sequence where name = '" ++ queueName ++ "'") []+ case sqlvalues of+ ((key:_):_) -> do+ return (fromSql key)+ _ -> return ("")++deleteDBQueue :: Sqlite3Queue -> String -> IO (Bool)+deleteDBQueue Sqlite3Queue {..} key = withLock mlock $ withTransaction conn $ \conn' -> do+ _ <- run conn' ("DELETE FROM '" ++ queueName ++ "' WHERE key = ?") [toSql key]+ return (True)++updateDBQueue :: Sqlite3Queue -> String -> BS.ByteString -> Int -> IO (Bool)+updateDBQueue Sqlite3Queue {..} key value version = do+ withLock mlock $ withTransaction conn $ \conn' -> do+ nrows <- run conn' ("UPDATE '" ++ queueName ++ "' SET value = ?, version = ? WHERE key = ? AND version = ?") [toSql value, toSql (version+1), toSql key, toSql version]+ return $ if nrows > 0 then True else False++countDBQueue :: Sqlite3Queue -> IO (Int)+countDBQueue Sqlite3Queue {..} = withLock mlock $ withTransaction conn $ \conn' -> do+ sqlvalues <- quickQuery' conn' ("SELECT COUNT (*) FROM '" ++ queueName ++ "' ORDER BY prio, key LIMIT 1") []+ case sqlvalues of+ ((count:_):_) -> return (fromSql count)+ _ -> return (0)++itemsDBQueue :: Sqlite3Queue -> IO ([String])+itemsDBQueue Sqlite3Queue {..} = withLock mlock $ withTransaction conn $ \conn' -> do+ sqlvalues <- quickQuery' conn' ("SELECT key FROM '" ++ queueName ++ "' ORDER BY prio, key") []+ case sqlvalues of+ keys -> return (map (fromSql . head) keys)++listDBQueue :: Sqlite3Queue -> IO ([BS.ByteString])+listDBQueue Sqlite3Queue {..} = withLock mlock $ withTransaction conn $ \conn' -> do+ sqlvalues <- quickQuery' conn' ("SELECT value FROM '" ++ queueName ++ "' ORDER BY prio, key") []+ case sqlvalues of+ keys -> return (map (fromSql . head) keys)++withLock :: MVar () -> IO a -> IO a+withLock m act = handleSql (\err -> throwIO $ SessionError (show err)) $ do+ bracket (takeMVar m) (putMVar m) $ const act
+ src/Network/JobQueue/Backend/Types.hs view
@@ -0,0 +1,24 @@+-- Copyright (c) Gree, Inc. 2013+-- License: MIT-style++{-# LANGUAGE GADTs #-}+{-# LANGUAGE DeriveDataTypeable #-}++module Network.JobQueue.Backend.Types (Backend(..), BackendError(..)) where++import Control.Exception+import Network.JobQueue.Backend.Class+import Data.Typeable++data Backend where+ Backend :: (BackendQueue q) => {+ bOpenQueue :: String -> IO q+ , bClose :: IO ()+ } -> Backend++data BackendError = + NotFound String+ | SessionError String+ deriving (Show, Typeable)++instance Exception BackendError
+ src/Network/JobQueue/Backend/Zookeeper.hs view
@@ -0,0 +1,58 @@+-- Copyright (c) Gree, Inc. 2013+-- License: MIT-style++module Network.JobQueue.Backend.Zookeeper (+ openZookeeperBackend+ , newZookeeperBackend+ ) where++import qualified Database.Zookeeper as Z+import Control.Concurrent+import Control.Concurrent.STM+import Network.JobQueue.Backend.Types+import Network.JobQueue.Backend.Zookeeper.ZookeeperQueue++openZookeeperBackend :: String -> IO Backend+openZookeeperBackend endpoint = do+ Z.setDebugLevel Z.ZLogError+ zvar <- newTVarIO Nothing+ stateVar <- newTVarIO Z.ConnectingState+ _ <- forkIO $ Z.withZookeeper endpoint 100000 (Just $ watcher stateVar) Nothing $ \z -> do+ atomically $ do+ state <- readTVar stateVar+ case state of+ Z.ConnectingState -> retry+ _ -> return ()+ atomically $ writeTVar zvar (Just z)+ atomically $ do+ mz <- readTVar zvar+ case mz of+ Just _ -> retry+ Nothing -> return ()+ return $ Backend {+ bOpenQueue = openQueue zvar+ , bClose = atomically $ writeTVar zvar Nothing+ }+ where+ openQueue :: TVar (Maybe Z.Zookeeper) -> String -> IO (ZookeeperQueue)+ openQueue zvar queueName = do+ z <- atomically $ readTVar zvar >>= maybe retry return+ zq <- initZQueue z (basePath queueName) Z.OpenAclUnsafe+ return zq++ watcher :: TVar Z.State -> Z.Watcher+ watcher stateVar _z event state _mZnode = do+ case event of+ Z.SessionEvent -> atomically $ writeTVar stateVar state+ _ -> return ()++newZookeeperBackend :: Z.Zookeeper -> Backend+newZookeeperBackend zh = Backend {+ bOpenQueue = \queueName -> initZQueue zh (basePath queueName) Z.OpenAclUnsafe+ , bClose = return ()+ }++basePath :: String -> String+basePath queueName = case queueName of+ '/':_ -> queueName+ _ -> '/':queueName
+ src/Network/JobQueue/Backend/Zookeeper/ZookeeperQueue.hs view
@@ -0,0 +1,228 @@+-- Copyright (c) Gree, Inc. 2013+-- License: MIT-style++module Network.JobQueue.Backend.Zookeeper.ZookeeperQueue (+ ZookeeperQueue+ , initZQueue+ , readZQueue+ , peekZQueue+ , updateZQueue+ , deleteZQueue+ , writeZQueue+ , destroyZQueue+ , listZQueue+ , itemsZQueue+ , countZQueue+ ) where++import qualified Database.Zookeeper as Z+import qualified Data.ByteString.Char8 as C+import Control.Exception hiding (handle)+import Data.List+import Control.Monad+import Data.Maybe+import Data.List.Split++import Network.JobQueue.Backend.Class+import Network.JobQueue.Backend.Types++data ZookeeperQueue = ZookeeperQueue {+ zqHandle :: Z.Zookeeper+ , zqBasePath :: String+ , zqNodeName :: String+ , zqAcls :: Z.AclList+ }++instance BackendQueue ZookeeperQueue where+ readQueue = readZQueue+ peekQueue = peekZQueue+ updateQueue = updateZQueue+ deleteQueue = deleteZQueue+ writeQueue = writeZQueue+ listQueue = listZQueue+ itemsQueue = itemsZQueue+ countQueue = countZQueue+ ++maxPrio :: Int+maxPrio = 999++minPrio :: Int+minPrio = -999++qnPrefix :: String+qnPrefix = "qn-"++---- init++initZQueue :: Z.Zookeeper -> String -> Z.AclList -> IO (ZookeeperQueue)+initZQueue z path acls = do+ e <- createZnodeRecursively z path Nothing acls []+ case e of+ Right _ -> return ()+ Left zkerr -> throwZKError "initZQueue" zkerr+ return (ZookeeperQueue z path qnPrefix acls)++-- take+readZQueue :: ZookeeperQueue -> IO (Maybe (C.ByteString, String))+readZQueue zkQueue = do+ children <- getChildren zkQueue+ case children of+ [] -> return (Nothing)+ _ -> takeHead (sortChildren children)+ where+ takeHead [] = return (Nothing)+ takeHead (nodeName:xs) = do+ let path = zqBasePath zkQueue ++ "/" ++ nodeName+ e <- Z.get (zqHandle zkQueue) path Nothing+ case e of+ Right (Just value, _stat) -> do+ e' <- Z.delete (zqHandle zkQueue) path Nothing+ case e' of+ Right () -> return (Just (value, nodeName))+ Left _zkerr -> do+ r <- Z.exists (zqHandle zkQueue) path Nothing+ case r of+ Right _stat -> takeHead (nodeName:xs)+ Left Z.NoNodeError -> takeHead xs+ Left zkerr -> throwZKError "readZQueue" zkerr+ Right (Nothing, _stat) -> takeHead xs -- ignore if the content is empty+ Left Z.NoNodeError -> return (Nothing)+ Left zkerr -> throwZKError "readZQueue" zkerr++-- peek+peekZQueue :: ZookeeperQueue -> IO (Maybe (C.ByteString, String, String, Int))+peekZQueue zkQueue = do+ children <- getChildren zkQueue+ case children of+ [] -> return Nothing+ _ -> getHead (sortChildren children)+ where+ idSuffixLen :: Int+ idSuffixLen = 10+ + getHead :: [String] -> IO (Maybe (C.ByteString, String, String, Int))+ getHead [] = return Nothing+ getHead (x:xs) = do+ e <- Z.get (zqHandle zkQueue) (fullPath zkQueue x) Nothing+ case e of+ Right (mValue, stat) -> do+ case mValue of+ Just v -> return $ Just (v, x, drop (length x - idSuffixLen) x, fromIntegral $ Z.statVersion stat)+ Nothing -> getHead xs+ Left Z.NoNodeError -> peekZQueue zkQueue+ Left zkerr -> throwZKError "peekZQueue" zkerr+ +-- update+updateZQueue :: ZookeeperQueue -> String -> C.ByteString -> Int -> IO (Bool)+updateZQueue zkQueue znodeName value version = do+ e <- Z.set (zqHandle zkQueue) (fullPath zkQueue znodeName) (Just value) (Just (fromIntegral version))+ case e of+ Right _stat -> return (True)+ Left Z.BadVersionError -> return (False)+ Left Z.NoNodeError -> return (False)+ Left zkerr -> throwZKError "updateZQueue" zkerr++-- delete+deleteZQueue :: ZookeeperQueue -> String -> IO (Bool)+deleteZQueue zkQueue nodeName = do+ let nodeName' = zqBasePath zkQueue ++ "/" ++ nodeName+ e <- Z.delete (zqHandle zkQueue) nodeName' Nothing+ case e of+ Right () -> return (True)+ Left Z.NoNodeError -> throwIO $ NotFound nodeName+ Left zkerr -> throwZKError ("deleteZQueue(nodeName=" ++ nodeName' ++ ")") zkerr++-- offer+writeZQueue :: ZookeeperQueue -> C.ByteString -> Int -> IO (String)+writeZQueue zkQueue value prio = do+ r <- Z.create (zqHandle zkQueue)+ (zqBasePath zkQueue ++ "/" ++ (nodePrefix (zqNodeName zkQueue) prio))+ (Just value)+ (zqAcls zkQueue)+ [Z.Sequence]+ case r of+ Right znode -> return $ head $ reverse (splitOn "/" znode)+ Left zkerr -> throwZKError "writeZQueue" zkerr++-- destroy+destroyZQueue :: ZookeeperQueue -> IO ()+destroyZQueue _zkQueue = return ()++-- elems+listZQueue :: ZookeeperQueue -> IO ([C.ByteString])+listZQueue zkQueue = do+ results <- getChildren zkQueue+ values <- forM (sortChildren results) getItem+ return (catMaybes values)+ where+ getItem x = do+ e <- Z.get (zqHandle zkQueue) (zqBasePath zkQueue ++ "/" ++ x) Nothing + case e of+ Right (mValue, _stat) -> return (mValue)+ Left Z.NoNodeError -> return (Nothing)+ Left zkerr -> throwZKError "listZQueue" zkerr++-- items+itemsZQueue :: ZookeeperQueue -> IO ([String])+itemsZQueue zkQueue = do+ items <- getChildren zkQueue+ return (sortChildren items)++-- count+countZQueue :: ZookeeperQueue -> IO (Int)+countZQueue zkQueue = do+ items <- getChildren zkQueue+ return (length items)++----++getChildren :: ZookeeperQueue -> IO ([String])+getChildren zkQueue = do+ e <- Z.getChildren (zqHandle zkQueue) (zqBasePath zkQueue) Nothing+ case e of+ Right results -> return (results)+ Left zkerr -> throwZKError "getChildren" zkerr++sortChildren :: [String] -> [String]+sortChildren = sort . filter (isPrefixOf qnPrefix)++fullPath :: ZookeeperQueue -> String -> String+fullPath zkQueue x = (zqBasePath zkQueue ++ "/" ++ x)++nodePrefix :: String -> Int -> String+nodePrefix base prio = base ++ priorityPart' ++ "-"+ where+ priority = if prio > maxPrio then maxPrio else (if prio < minPrio then minPrio else prio)+ plus = priority >= 0+ priorityPart = show $ if plus then abs priority else maxPrio + 1 + priority+ priorityPart' = (if plus then "0" else "-")+ ++ (take (3 - length priorityPart) $ repeat '0')+ ++ priorityPart++throwZKError :: String -> Z.ZKError -> IO a+throwZKError func zkerr = throwIO $ SessionError (func ++ ": " ++ show zkerr)++createZnodeRecursively :: Z.Zookeeper -> String -> Maybe C.ByteString -> Z.AclList -> [Z.CreateFlag] -> IO (Either Z.ZKError String)+createZnodeRecursively z path mData acls flags = do+ createZnodeRecursively' z (reverse $ splitOn "/" path) mData acls flags++createZnodeRecursively' :: Z.Zookeeper -> [String] -> Maybe C.ByteString -> Z.AclList -> [Z.CreateFlag] -> IO (Either Z.ZKError String)+createZnodeRecursively' _ [] _ _ _ = return $ Right "/"+createZnodeRecursively' _ ("":[]) _ _ _ = return $ Right "/"+createZnodeRecursively' z revZnodes value acls cflags = do+ let path = intercalate "/" (reverse revZnodes)+ eStats <- Z.exists z path Nothing+ case eStats of+ Right _stat -> return $ Right path+ Left Z.NoNodeError -> do+ e <- createZnodeRecursively' z (tail revZnodes) Nothing acls cflags+ case e of+ Right _ -> do+ r <- Z.create z path value acls cflags+ return $ case r of+ Right newPath -> Right newPath+ Left zkerr -> Left zkerr+ Left zkerr -> return (Left zkerr)+ Left zkerr -> return (Left zkerr)+
+ src/Network/JobQueue/Class.hs view
@@ -0,0 +1,47 @@+-- Copyright (c) Gree, Inc. 2013+-- License: MIT-style++{- | Class definitions+-}+module Network.JobQueue.Class where++{- | Environment class+-}+class Env a where++{- | Environment with a parameter set+-}+class (Env a) => ParamEnv a where+ envParameters :: a -> [(String, String)]+ envParameters _env = []++{- | Description class+-}+class (Show a) => Desc a where+ {- | Define the description of a unit.+ -}+ desc :: a -> String+ desc x = show x+ + {- | Define the short description of a unit.+ -}+ shortDesc :: a -> String+ shortDesc x = takeWhile (/= ' ') $ show x++{- | Unit class+-}+class (Read a, Show a, Desc a, Eq a) => Unit a where+ {- | Define the priority of a unit.+ -}+ getPriority :: a -> Int+ getPriority _ju = 1+ + {- | Define the recovery state of a unit.+ -}+ getRecovery :: a -> a+ getRecovery ju = ju++ {- | Define the logging necessity of a unit.+ -}+ toBeLogged :: a -> Bool+ toBeLogged _ju = False
+ src/Network/JobQueue/Job.hs view
@@ -0,0 +1,36 @@+-- Copyright (c) Gree, Inc. 2013+-- License: MIT-style++module Network.JobQueue.Job (+ Job(jobState, jobUnit, jobCTime, jobOnTime, jobId, jobGroup, jobPriority, StopTheWorld)+ , JobState(..)+ , buildActionState+ , process+ , createJob+ , createOnTimeJob+ , printJob+ , module Network.JobQueue.Types+ , module Network.JobQueue.Action+ ) where++import Control.Monad.State hiding (state)++import Network.JobQueue.Class+import Network.JobQueue.AuxClass+import Network.JobQueue.Types+import Network.JobQueue.Action+import Network.JobQueue.Job.Internal++--------------------------------++buildActionState :: (Env e, Unit a) => JobM e a () -> IO (JobActionState e a)+buildActionState jobs = execStateT (runS jobs) (JobActionState [])++{- | Declare a function which accepts a unit and execute the action of it if possible.+-}+process :: (Aux e, Env e, Unit a) => (a -> ActionM e a ()) -> JobM e a ()+process action = modify $ addAction $ eval action++eval :: (Aux e, Env e, Unit a) => (a -> ActionM e a ()) -> ActionFn e a+eval action env ju = runAction env ju (action ju)+
+ src/Network/JobQueue/Job/Internal.hs view
@@ -0,0 +1,67 @@++module Network.JobQueue.Job.Internal where++import Data.Time.Clock+import System.Log.Logger+import System.IO++import Network.JobQueue.Class++data JobState = Initialized | Runnable | Running | Aborted | Finished+ deriving (Show, Read, Eq)++{- | Job control block+Job consists of /State/, /Unit/, /CTime/, /OnTime/, /Id/, /Group/, and /Priority/.++- State - takes one of 5 states (initialized, runnable, running, aborted and finished)++- Unit - an instance of Unit class, which is specified by type parameter of Job data type++- CTime - creation time++- OnTime - the time at which this job starts++- Id - Identifier of this job++- Group - Group ID of this job++- Priority - the priority of this job++-}+data Job a =+ Job {+ jobState :: JobState+ , jobUnit :: a+ , jobCTime :: UTCTime+ , jobOnTime :: UTCTime+ , jobId :: Int+ , jobGroup :: Int+ , jobPriority :: Int }+ | StopTheWorld+ deriving (Show, Read, Eq)++createJob :: (Unit a) => JobState -> a -> IO (Job a)+createJob state unit = do+ ctime <- getCurrentTime+ return (Job state unit ctime ctime (defaultId) (defaultGroup) (getPriority unit))++createOnTimeJob :: (Unit a) => JobState -> UTCTime -> a -> IO (Job a)+createOnTimeJob state ontime unit = do+ ctime <- getCurrentTime+ return (Job state unit ctime ontime (defaultId) (defaultGroup) (getPriority unit))++printJob :: (Unit a) => Job a -> IO ()+printJob job = case job of+ Job {} -> do+ noticeM "job" $ show (jobUnit job)+ hPutStrLn stdout $ desc (jobUnit job)+ hFlush stdout+ StopTheWorld -> return ()++---------------------------------------------------------------- PRIVATE++defaultId :: Int+defaultId = -1++defaultGroup :: Int+defaultGroup = -1
+ src/Network/JobQueue/JobQueue.hs view
@@ -0,0 +1,185 @@+-- Copyright (c) Gree, Inc. 2013+-- License: MIT-style++{-# LANGUAGE ScopedTypeVariables #-}++module Network.JobQueue.JobQueue (+ JobQueue+ , Session+ , openSession+ , newSession+ , closeSession+ , openJobQueue+ , closeJobQueue+ , countJobQueue+ , resumeJobQueue+ , suspendJobQueue+ , executeJob+ , scheduleJob+ , deleteJob+ , clearJobs+ , peekJob+ ) where++import Control.Applicative+import Control.Concurrent+import Control.Monad+import Control.Exception+import qualified Data.ByteString.Char8 as BS+import Data.Maybe++import Network.JobQueue.Class+import Network.JobQueue.AuxClass+import Network.JobQueue.Types+import Network.JobQueue.Job+import Network.JobQueue.Backend+import Network.JobQueue.Backend.Class+import Network.JobQueue.Backend.Types++import Network.JobQueue.JobQueue.Internal+++{- | A session handler++ A session usually represents a database session to access job queues stored in the backend+ database.+-}+data Session = Session Bool String Backend++{- | Open a queue session with a resource locator+-}+openSession :: String -- ^ a resource locator+ -> IO (Session) -- ^ a session handler+openSession locator = Session True locator <$> openBackend locator++{- | Create a queue session with a backend handler+-}+newSession :: String -- ^ a resource locator (dummy)+ -> Backend -> Session -- ^ a session handler+newSession dummyLocator backend = Session False dummyLocator backend++{- | Close a queue session if needed+-}+closeSession :: Session -> IO ()+closeSession (Session isOwner _locator backend) = when isOwner $ bClose backend++{- | Open a job queue with a session.+-}+openJobQueue :: (Env e, Unit a)+ => Session -- ^ a session handler+ -> String -- ^ a queue name+ -> JobM e a () -- ^ a state machine definition+ -> IO (JobQueue e a)+openJobQueue (Session _isOwner _locator _backend@(Backend { bOpenQueue = oq }))+ name+ jobm = do+ JobQueue <$> oq name <*> buildActionState jobm++{- | Close a job queue.+-}+closeJobQueue :: (Env e, Unit a) => JobQueue e a -> IO ()+closeJobQueue JobQueue { jqBackendQueue = bq } = closeQueue bq++{- | Count the number of jobs queued in a job queue.+-}+countJobQueue :: (Env e, Unit a) => JobQueue e a -> IO (Int)+countJobQueue JobQueue { jqBackendQueue = bq } = countQueue bq++{- | Resume a job queue+-}+resumeJobQueue :: (Env e, Unit a) => JobQueue e a -> IO (Bool)+resumeJobQueue jobqueue = do+ r <- peekJob' jobqueue+ case r of+ Just (job, nodeName, idName, _version) -> case actionForJob job idName of+ Execute StopTheWorld -> resume jobqueue nodeName+ _ -> return True+ _ -> return True+ where+ resume JobQueue { jqBackendQueue = bq } key = deleteQueue bq key++{- | Suspend a job queue+-}+suspendJobQueue :: forall e. forall a. (Env e, Unit a) => JobQueue e a -> IO (Bool)+suspendJobQueue jobqueue = do+ r <- peekJob' jobqueue+ case r of+ Just (job, _nodeName, idName, _version) -> case actionForJob job idName of+ Execute StopTheWorld -> return False+ _ -> suspend jobqueue >> return True+ _ -> suspend jobqueue >> return True+ where+ suspend JobQueue { jqBackendQueue = bq } = writeQueue bq (pack (StopTheWorld :: Job a)) (-1)++{- | Execute an action of the head job in a job queue.+-}+executeJob :: (Aux e, Env e, Unit a) => JobQueue e a -> e -> IO ()+executeJob jobqueue env = do+ r <- peekJob' jobqueue+ case r of+ Just (job, nodeName, idName, version) -> case actionForJob job idName of+ Execute StopTheWorld -> do+ threadDelay 1000000+ return ()+ Execute job' -> do+ isUpdated <- updateJob jobqueue nodeName job' version+ when (isUpdated && jobState job == Runnable && jobState job' == Running) $ do+ executeJob' jobqueue env nodeName job' version >>= afterExecuteJob jobqueue env nodeName job' version+ auxHandleAfterExecute env job'+ Delete -> do+ void $ deleteJob jobqueue nodeName+ executeJob jobqueue env+ Skip -> return ()+ Nothing -> return ()++{- | Schedule a job.+-}+scheduleJob :: (Unit a)+ => JobQueue e a -- ^ a job queue+ -> a -- ^ a unit+ -> IO ()+scheduleJob JobQueue { jqBackendQueue = bq } ju = do+ job <- createJob Initialized ju+ void $ writeQueue bq (pack job) (getPriority ju)++{- | Delete a job from a job queue.+-}+deleteJob :: (Unit a)+ => JobQueue e a -- ^ a job queue+ -> String -- ^ a job identifier+ -> IO Bool+deleteJob JobQueue { jqBackendQueue = bq } nodeName = do+ deleteQueue bq nodeName `catch` \e -> case e of+ NotFound _ -> return True+ _ -> throwIO e++{- | Clear all jobs from a job queue.+-}+clearJobs :: (Unit a)+ => JobQueue e a -- ^ a job queue+ -> IO [(String, Job a)]+clearJobs JobQueue { jqBackendQueue = bq } = loop []+ where+ loop dequeued = do+ obj <- readQueue bq+ case obj of+ Nothing -> return dequeued+ Just (bs, nodeName) -> case (fmap fst . listToMaybe . reads) $ BS.unpack bs of+ Nothing -> return dequeued+ Just job -> loop ((nodeName, job):dequeued)++{- | Peek a job form a job queue.+-}+peekJob :: (Unit a)+ => JobQueue e a -- ^ a job queue+ -> IO (Maybe (Job a))+peekJob jobqueue = do+ mjob <- peekJob' jobqueue+ return $ case mjob of+ Just (job, _nodeName, _idName, _version) -> Just job+ Nothing -> Nothing++---------------------------------------------------------------- PRIVATE++ +
+ src/Network/JobQueue/JobQueue/Internal.hs view
@@ -0,0 +1,112 @@+-- Copyright (c) Gree, Inc. 2013+-- License: MIT-style++{-# LANGUAGE GADTs #-}++module Network.JobQueue.JobQueue.Internal where++import qualified Data.ByteString.Char8 as BS+import Control.Exception+import Data.Time.Clock+import Control.Monad+import Data.Maybe++import Network.JobQueue.Class+import Network.JobQueue.AuxClass+import Network.JobQueue.Types+import Network.JobQueue.Action+import Network.JobQueue.Job+import Network.JobQueue.Backend.Class+import Network.JobQueue.Backend.Types+++data JobQueue e a where+ JobQueue :: (BackendQueue q) => {+ jqBackendQueue :: q+ , jqActionState :: JobActionState e a+ } -> JobQueue e a++data ActionForJob a = (Unit a) => Execute (Job a) | Delete | Skip+++actionForJob :: Unit a => Job a -> String -> ActionForJob a+actionForJob job idName = do+ case job of+ StopTheWorld -> Execute job+ _ -> case jobState job of+ Initialized -> case (fmap fst . listToMaybe . reads) idName of+ Just ident -> Execute $ job { jobState = Runnable, jobId = ident }+ Nothing -> Execute $ job { jobState = Runnable, jobId = (-1) }+ Runnable -> Execute $ job { jobState = Running }+ Running -> Skip+ Aborted -> Skip+ Finished -> Delete++peekJob' :: (Unit a) => JobQueue e a -> IO (Maybe (Job a, String, String, Int))+peekJob' JobQueue { jqBackendQueue = bq } = do+ obj <- peekQueue bq+ case obj of+ Nothing -> return (Nothing)+ Just (value, nodeName, idName, version) -> do+ case (fmap fst . listToMaybe . reads) $ BS.unpack value of+ Nothing -> return (Nothing)+ Just job -> return (Just (job, nodeName, idName, version))++executeJob' :: (Aux e, Env e, Unit a) => JobQueue e a -> e -> String -> Job a -> Int -> IO (Either Break (Maybe (RuntimeState a)))+executeJob' jqueue@JobQueue { jqBackendQueue = bq, jqActionState = actionState } env nodeName currentJob version = do+ currentTime <- getCurrentTime+ if jobOnTime currentJob < currentTime+ then do+ runActionState actionState env (jobUnit currentJob)+ else do+ r <- updateJob jqueue nodeName currentJob { jobState = Finished } (version+1)+ when r $ void $ writeQueue bq (pack $ currentJob { jobState = Runnable } ) (jobPriority currentJob)+ return $ Right Nothing++afterExecuteJob :: (Aux e, Env e, Unit a) => JobQueue e a -> e -> String -> Job a -> Int -> Either Break (Maybe (RuntimeState a)) -> IO ()+afterExecuteJob jqueue env nodeName currentJob version mResult = case mResult of+ Right (Just (RS mNextJu forks _)) -> do+ case mNextJu of+ Just nextJu -> do+ _r <- updateJob jqueue nodeName currentJob { jobState = Runnable, jobUnit = nextJu } (version+1)+ return ()+ Nothing -> do+ _r <- updateJob jqueue nodeName currentJob { jobState = Finished } (version+1)+ return ()+ forM_ (reverse forks) $ \(forked, ontime) -> rescheduleJob jqueue ontime forked+ Right (Nothing) -> recover Nothing -- nothing to do anymore+ Left (Failure _msg) -> do+ n <- auxHandleFailure env (Just currentJob)+ recover n+ Left Retriable -> do+ _r <- updateJob jqueue nodeName currentJob { jobState = Runnable } (version+1)+ return ()+ Left (Unhandled _someException) -> do+ _r <- updateJob jqueue nodeName currentJob { jobState = Finished } (version+1)+ return ()+ where+ recover n = case n of+ Just nextJu -> do+ _r <- updateJob jqueue nodeName nextJu (version+1)+ return ()+ Nothing -> do+ _r <- updateJob jqueue nodeName currentJob { jobState = Finished } (version+1)+ return ()++rescheduleJob :: (Unit a) => JobQueue e a -> Maybe UTCTime -> a -> IO ()+rescheduleJob JobQueue { jqBackendQueue = bq } mOntime ju = do+ job <- case mOntime of+ Just ontime -> createOnTimeJob Initialized ontime ju+ Nothing -> createJob Initialized ju+ void $ writeQueue bq (pack $ job) (getPriority ju)++updateJob :: (Unit a) => JobQueue e a -> String -> Job a -> Int -> IO (Bool)+updateJob JobQueue { jqBackendQueue = bq } nodeName job version = do+ updateQueue bq nodeName (pack job) version `catch` handleError+ where+ handleError :: BackendError -> IO (Bool)+ handleError _ = return (False)++pack :: (Unit a) => Job a -> BS.ByteString+pack = BS.pack . show+
+ src/Network/JobQueue/Logger.hs view
@@ -0,0 +1,41 @@++{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE OverloadedStrings #-}++module Network.JobQueue.Logger+ ( logDebug+ , logInfo+ , logWarn+ , logError+ , logNotice+ , logCritical+ , Only+ ) where++import qualified Data.Text as T+import qualified Data.Text.Lazy as LT+import qualified Control.Monad.Logger as ML+import Language.Haskell.TH.Syntax (Q, Exp, qLocation)+import Data.Text.Format++logTH :: ML.LogLevel -> Q Exp+logTH level = [|\a b -> ML.monadLoggerLog $(qLocation >>= ML.liftLoc) (T.pack "") level (LT.toStrict $ format (a :: Format) b)|]++logDebug :: Q Exp+logDebug = logTH ML.LevelDebug++logInfo :: Q Exp+logInfo = logTH ML.LevelInfo++logWarn :: Q Exp+logWarn = logTH ML.LevelWarn++logError :: Q Exp+logError = logTH ML.LevelError++logNotice :: Q Exp+logNotice = logTH (ML.LevelOther "notice")++logCritical :: Q Exp+logCritical = logTH (ML.LevelOther "critical")+
+ src/Network/JobQueue/Types.hs view
@@ -0,0 +1,129 @@+-- Copyright (c) Gree, Inc. 2013+-- License: MIT-style++{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE TypeSynonymInstances #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE TypeFamilies, MultiParamTypeClasses, UndecidableInstances #-}+++module Network.JobQueue.Types+ ( JobActionState(..)+ , JobM+ , ActionM+ , ActionT+ , ActionFn+ , ActionEnv(..)+ , Unit(..)+ , RuntimeState(..)+ , Break(..)+ , LogLevel(..)+ , setNextJob+ , setNextJobIfEmpty+ , emptyNextJob+ , addForkJob+ , incrementCommits+ , getCommits+ , runS+ , runAM+ , addAction+ , setResult+ ) where++import Data.Time.Clock++import Control.Applicative+import Control.Monad.Base+import Control.Monad.Trans.Control+import Control.Monad.Except+import Control.Monad.Reader+import Control.Monad.State+import Control.Monad.Logger+import Control.Exception+import Data.Default (Default, def)++import Network.JobQueue.Class++-------------------------------- Types++data Break = Unhandled SomeException | Failure String | Retriable deriving (Show)++-------------------------------- State in Action++data RuntimeState a = RS+ { rsNextJob :: (Maybe a)+ , rsNextForks :: [(a, Maybe UTCTime)]+ , rsCommits :: Int+ } deriving (Show)++instance (Unit a) => Default (RuntimeState a) where+ def = RS Nothing [] 0++setNextJob :: (Unit a) => a -> (RuntimeState a) -> (RuntimeState a)+setNextJob x next@(RS _ _ _) = next { rsNextJob = Just x }++setNextJobIfEmpty :: (Unit a) => a -> (RuntimeState a) -> (RuntimeState a)+setNextJobIfEmpty x next@(RS mju _ _) = maybe (next { rsNextJob = Just x }) (const next) mju++emptyNextJob :: (Unit a) => (RuntimeState a) -> (RuntimeState a)+emptyNextJob next@(RS _ _ _) = next { rsNextJob = Nothing }++addForkJob :: (Unit a) => (a, Maybe UTCTime) -> (RuntimeState a) -> (RuntimeState a)+addForkJob (x, mt) next@(RS _ xs _) = next { rsNextForks = ((x, mt):xs) }++incrementCommits :: (Unit a) => (RuntimeState a) -> (RuntimeState a)+incrementCommits next@(RS _ _ cnt) = next { rsCommits = cnt + 1 }++getCommits :: (Unit a) => (RuntimeState a) -> Int+getCommits (RS _ _ cnt) = cnt++-------------------------------- JobActionState++type ActionFn e a = e -> a -> IO (Either Break (Maybe (RuntimeState a)))++data JobActionState e a = JobActionState { jobActions :: [ActionFn e a] }++addAction :: (Env e, Unit a) => ActionFn e a -> JobActionState e a -> JobActionState e a+addAction action s@(JobActionState { jobActions = actions }) = s { jobActions = action:actions }++instance Default (JobActionState e a) where+ def = JobActionState []++-------------------------------- ActionM++newtype (Env e, Unit a) => JobM e a b = JobM { runS :: StateT (JobActionState e a) IO b }+ deriving (Monad, MonadIO, Functor, MonadState (JobActionState e a))++data ActionEnv e a = ActionEnv+ { getJobEnv :: e+ , getJobUnit :: a+ }++newtype ActionT e a m b = ActionT+ { runAM :: ExceptT Break (ReaderT (ActionEnv e a) (StateT (Maybe (RuntimeState a)) (LoggingT m))) b+ } deriving ( Applicative, Functor, Monad, MonadIO, MonadLogger, MonadError Break+ , MonadReader (ActionEnv e a), MonadState (Maybe (RuntimeState a)), MonadBase base)++type ActionM e a b = ActionT e a IO b++instance MonadTrans (ActionT e a) where+ lift = ActionT . lift . lift . lift . lift++instance MonadTransControl (ActionT e a) where+ newtype StT (ActionT e a) b = StAction { unStAction :: (Either Break b, Maybe (RuntimeState a)) }+ restoreT = ActionT . ExceptT . ReaderT . const . StateT . const+ . LoggingT . const . liftM unStAction+ liftWith f = ActionT . ExceptT . ReaderT $ \r -> StateT $ \s -> LoggingT $ \l ->+ liftM (\x -> (Right x, s))+ (f $ \t -> liftM StAction (runLoggingT (runStateT (runReaderT (runExceptT (runAM t)) r) s) l))++instance MonadBaseControl base m => MonadBaseControl base (ActionT e a m) where+ newtype StM (ActionT e a m) b = StMActionT { unStMActionT :: ComposeSt (ActionT e a) m b }+ liftBaseWith = defaultLiftBaseWith StMActionT+ restoreM = defaultRestoreM unStMActionT++setResult :: (Unit a) => Maybe (RuntimeState a) -> Maybe (RuntimeState a) -> Maybe (RuntimeState a)+setResult result _ = result+++--------------------------------
+ test/Main.hs view
@@ -0,0 +1,22 @@+-- Copyright (c) Gree, Inc. 2013+-- License: MIT-style++module Main where++import Test.Hspec+import System.Environment (lookupEnv)+import Data.Maybe+import System.IO++import Action+import BackendQueue+import JobQueue++main :: IO ()+main = do+ hSetBuffering stderr LineBuffering+ backend <- fmap (fromMaybe "sqlite3://test.sqlite3") $ lookupEnv "JOBQUEUE_TEST_BACKEND"+ hspec $ do+ testAction backend+ testJobQueueBackend backend+ testJobQueue backend