packages feed

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 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` -![build status](https://travis-ci.org/jfischoff/postgresql-libpq-notify.svg?branch=master)+[![build status](https://travis-ci.org/jfischoff/postgresql-libpq-notify.svg?branch=master)](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