postgresql-libpq-notify 0.1.0.0 → 0.2.0.0
raw patch · 5 files changed
+47/−96 lines, 5 filesPVP ok
version bump matches the API change (PVP)
API changes (from Hackage documentation)
- Database.PostgreSQL.LibPQ.Notify: [interrupt] :: Config -> Maybe (IO ())
- Database.PostgreSQL.LibPQ.Notify: [interrupted] :: Config -> IO ()
- Database.PostgreSQL.LibPQ.Notify: [threadWaitReadReturned] :: Config -> IO ()
- Database.PostgreSQL.LibPQ.Notify: Config :: Maybe (IO ()) -> IO () -> IO () -> IO () -> IO () -> Config
+ Database.PostgreSQL.LibPQ.Notify: Config :: IO () -> IO () -> Config
- Database.PostgreSQL.LibPQ.Notify: getNotification :: (forall a. c -> (Connection -> IO a) -> IO a) -> c -> IO (Either IOError Notify)
+ Database.PostgreSQL.LibPQ.Notify: getNotification :: Connection -> IO (Either IOError Notify)
- Database.PostgreSQL.LibPQ.Notify: getNotificationWithConfig :: Config -> (forall a. c -> (Connection -> IO a) -> IO a) -> c -> IO (Either IOError Notify)
+ Database.PostgreSQL.LibPQ.Notify: getNotificationWithConfig :: Config -> Connection -> IO (Either IOError Notify)
Files
- CHANGELOG.md +4/−0
- README.md +1/−1
- postgresql-libpq-notify.cabal +1/−2
- src/Database/PostgreSQL/LibPQ/Notify.hs +27/−78
- test/Main.hs +14/−15
CHANGELOG.md view
@@ -1,1 +1,5 @@+- 0.2.0.0+ - #6 Simplify interface because the race is impossible+ to prevent unless the connection is held until+ getNotification returns. - 0.1.0.0 First version
README.md view
@@ -1,6 +1,6 @@ # `postgresql-libpq-notify` -+[](http://travis-ci.org/jfischoff/postgresql-libpq-notify) `postgresql-libpq-notify` is a low level and minimal dependency PostgreSQL notification library.
postgresql-libpq-notify.cabal view
@@ -1,5 +1,5 @@ name: postgresql-libpq-notify-version: 0.1.0.0+version: 0.2.0.0 synopsis: Minimal dependency PostgreSQL notifications library description: Minimal dependency PostgreSQL notifications library. Please see README.md homepage: https://github.com/jfischoff/postgresql-libpq-notify#readme@@ -18,7 +18,6 @@ hs-source-dirs: src exposed-modules: Database.PostgreSQL.LibPQ.Notify build-depends: base < 5- , async , postgresql-libpq , stm ghc-options: -Wall
src/Database/PostgreSQL/LibPQ/Notify.hs view
@@ -25,21 +25,13 @@ <http://hackage.haskell.org/trac/ghc/ticket/7353> PostgreSQL notifications support using the same connection for sending and-receiving notifications. This pattern is particularly useful in tests and-probably not that useful otherwise.--This library relies on receiving OS notifications that new data is available to-read from the connection's socket. However both epoll_wait and kqueue do-not return if recvfrom is able to clear the socket receive buffer-before they verify there is data available.+receiving notifications. -This race between recvfrom and the event processing in kqueue/epoll_wait is common-and largely unavoidable if one is using the same connection for sending and-receiving notifications on different threads simultaneously.+However this implementation cannot support this usage pattern. -To support this pattern the library provides an advanced API which allows custom-interrupt to be used in addition to the socket read ready notification.-See 'getNotificationWithConfig' for more details.+This implementation favors low latency by utilizing socket read notifications.+However a consequence of this implementation choice is the connection used+to wait for the notification cannot be used for anything else. -} @@ -62,23 +54,13 @@ #endif import Data.Function(fix) import Data.Bifunctor(first)-import Control.Concurrent.Async (race) -- | Options for controlling and instrumenting the behavior of 'getNotificationWithConfig' data Config = Config- { interrupt :: Maybe (IO ())- -- ^ Custom interrupt- , interrupted :: IO ()- -- ^ Event called if the 'interrupt' is set and returns- -- before the 'threadWaitReadSTM' action returns.- , threadWaitReadReturned :: IO ()- -- ^ Event called if 'threadWaitReadSTM' action returns before the- -- 'interrupt' action returns.- , startLoop :: IO ()+ { startLoop :: IO () -- ^ Called each time 'getNotificationWithConfig' loops to look for another notification , beforeWait :: IO ()- -- ^ Event called before the thread will- -- wait on 'threadWaitReadSTM' action or 'interrupt' returning.+ -- ^ Event called before the thread will wait on 'threadWaitReadSTM' action. #if defined(mingw32_HOST_OS) , retryDelay :: Int -- ^ How long to wait in microseconds before retrying on Windows.@@ -88,18 +70,13 @@ -- | Default configuration defaultConfig :: Config defaultConfig = Config- { interrupt = Nothing- , interrupted = pure ()- , threadWaitReadReturned = pure ()- , startLoop = pure ()+ { startLoop = pure () , beforeWait = pure () #if defined(mingw32_HOST_OS) , retryDelay = 100000 #endif } -data RetryOrReturn = Retry (IO (Either () ())) | Return PQ.Notify- funcName :: String funcName = "Hasql.Notification.getNotification" @@ -121,42 +98,27 @@ Returns a single notification. If no notifications are available, 'getNotificationWithConfig' blocks until one arrives. Unlike 'getNotification', 'getNotificationWithConfig' takes in an-additional 'Config' parameter which provides custom 'interrupt' and-various event hooks for operational insight.+additional 'Config' parameter provides event hooks for operational insight. -Using a custom 'interrupt' is necessary if one would like to call-'getNotificationWithConfig' on one thread and @NOTIFY@ on another-thread using the same connection.+The connection passed in cannot be used for anything else+while waiting on the notification or this call might never return. -To support this behavior one must cause 'interrupt' to return after the-call to @NOTIFY@ checks it's result from the server. -See the test file of this package for an example of how to use a custom-'interrupt'.- Note that PostgreSQL does not deliver notifications while a connection is inside a transaction. -} getNotificationWithConfig :: Config- -- ^- -> (forall a. c -> (PQ.Connection -> IO a) -> IO a)- -- ^ This is a way to get a connection from a 'c'.- -- A concrete example would be if 'c' is 'MVar PQ.Connection'- -- and then this function would be 'withMVar'- -> c- -- ^ A type that can used to provide a connection when- -- used with the former argument. Typically a concurrency- -- primitive like 'MVar PQ.Connection'+ -- ^ 'Config' to instrument and configure the retry period on Windows.+ -> PQ.Connection+ -- ^ The connection. The connection cannot be used for anything else+ -- while waiting on the notification or this call might never return. -> IO (Either IOError PQ.Notify)-getNotificationWithConfig Config {..} withConnection conn = fmap (first setLoc) $ try $ fix $ \next -> do- -- We try to get the notification or register a file descriptor callback- -- while holding the lock. We then give up the lock to wait.- -- That is why code is broken up into these two sections.+getNotificationWithConfig Config {..} c = fmap (first setLoc) $ try $ fix $ \next -> do startLoop- e <- withConnection conn $ \c -> PQ.consumeInput c >> PQ.notifies c >>= \case+ PQ.notifies c >>= \case -- We found a notification just return it- Just x -> pure $ Return x+ Just x -> pure x -- There wasn't a notification so we need to register to wait on the file handle Nothing -> PQ.socket c >>= \case -- This is an odd error@@ -173,37 +135,24 @@ -- with the custom interrupt event if one is provided let fileNotification = atomically action #endif- pure $ Retry $ maybe (pure <$> fileNotification) (fileNotification `race`) interrupt-- case e of- Retry raceResult -> do- beforeWait- -- Wait on either threadWaitReadSTM action or the custom interrupt- either (const interrupted) (const threadWaitReadReturned) =<< raceResult- next- Return x -> pure x+ beforeWait+ fileNotification+ _ <- PQ.consumeInput c+ next {-| Returns a single notification. If no notifications are available, 'getNotification' blocks until one arrives. -If 'getNotification' is called and afterwards on a different thread-@NOTIFY@ is called using the same connection, 'getNotification' can-block even if a notification is sent.--To support this behavior one must use 'getNotificationWithConfig' instead.+The connection passed in cannot be used for anything else+while waiting on the notification or this call might never return. Note that PostgreSQL does not deliver notifications while a connection is inside a transaction. -} getNotification- :: (forall a. c -> (PQ.Connection -> IO a) -> IO a)- -- ^ This is a way to get a connection from a 'c'.- -- A concrete example would be if 'c' is 'MVar PQ.Connection'- -- and then this function would be 'withMVar'- -> c- -- ^ A type that can used to provide a connection when- -- used with the former argument. Typically a concurrency- -- primitive like 'MVar PQ.Connection'+ :: PQ.Connection+ -- ^ The connection. The connection cannot be used for anything else+ -- while waiting on the notification or this call might never return. -> IO (Either IOError PQ.Notify) getNotification = getNotificationWithConfig defaultConfig
test/Main.hs view
@@ -38,46 +38,45 @@ beforeAll theStart $ afterAll theStop $ specWith -withSetup :: (PQ.Connection -> IO ()) -> IO ()+withSetup :: ((PQ.Connection, PQ.Connection) -> IO ()) -> IO () withSetup f = either throwIO pure <=< Temp.withDbCache $ \dbCache -> Temp.withConfig (Temp.defaultConfig <> Temp.cacheConfig dbCache) $ \db -> do let localhostOpts = Temp.toConnectionOptions db bracket (PQ.connectdb (Options.toConnectionString localhostOpts)) PQ.finish- f+ $ \c -> bracket (PQ.connectdb (Options.toConnectionString localhostOpts))+ PQ.finish+ $ \c1 -> f (c, c1) spec :: Spec spec = aroundAll withSetup $ do describe "getNotificationWithConfig" $ describe "returns a notification" $ do- it "before" $ \conn -> do- connVar <- newMVar conn+ it "before" $ \(conn1, conn2) -> do let initialChannel = "channel" initialData = "hi!"- _ <- withMVar connVar $ \c -> do- _ <- PQ.exec c $ "LISTEN " <> initialChannel- PQ.exec c ("NOTIFY " <> initialChannel <> ", '" <> initialData <>"';") - Right PQ.Notify {..} <- getNotification withMVar connVar+ _ <- PQ.exec conn2 $ "LISTEN " <> initialChannel+ _ <- PQ.exec conn1 $ "NOTIFY " <> initialChannel <> ", '" <> initialData <>"';"++ Right PQ.Notify {..} <- getNotification conn2 notifyRelname `shouldBe` initialChannel notifyExtra `shouldBe` initialData - it "after file notifications are registered" $ \conn -> do- connVar <- newMVar conn+ it "after file notifications are registered" $ \(conn1, conn2) -> do ender <- newEmptyMVar beforeWaiter <- newEmptyMVar let initialChannel = "channel" initialData = "hi!"- _ <- withMVar connVar $ \c -> PQ.exec c $ "LISTEN " <> initialChannel+ _ <- PQ.exec conn2 $ "LISTEN " <> initialChannel _ <- forkIO $ do takeMVar beforeWaiter- _ <- withMVar connVar $ \c -> PQ.exec c ("NOTIFY " <> initialChannel <> ", '" <> initialData <>"';")+ _ <- PQ.exec conn1 ("NOTIFY " <> initialChannel <> ", '" <> initialData <>"';") putMVar ender () let config = defaultConfig- { interrupt = Just $ takeMVar ender- , beforeWait = putMVar beforeWaiter ()+ { beforeWait = putMVar beforeWaiter () } - Right PQ.Notify {..} <- getNotificationWithConfig config withMVar connVar+ Right PQ.Notify {..} <- getNotificationWithConfig config conn2 notifyRelname `shouldBe` initialChannel notifyExtra `shouldBe` initialData