@@ -71,6 +71,7 @@ import Data.Maybe
7171import Data.Ord (Down (.. ))
7272import qualified Data.Set as Set
7373import qualified Data.Text as T
74+ import qualified Data.Time.Clock as DTC
7475import Data.Type.Equality
7576import Data.Word (Word64 )
7677import GHC.Exts (IsList (.. ))
@@ -194,19 +195,27 @@ retryUntilJustM esv timeout act = withFrozenCallStack $ do
194195 WaitForSlots n -> n
195196 WaitForBlocks n -> n
196197
198+ -- | Status of the 'EpochStateView' background thread when epoch state is not yet available
199+ data EpochStateStatus
200+ = EpochStateNotInitialised
201+ -- ^ The background thread has not yet received any epoch state from the node
202+ | EpochStateFoldError ! FoldBlocksError
203+ -- ^ The background thread encountered an error while folding blocks
204+
197205-- | A read-only mutable pointer to an epoch state, updated automatically
198206data EpochStateView = EpochStateView
199207 { nodeConfigPath :: ! (NodeConfigFile In )
200208 -- ^ node configuration file path
201209 , socketPath :: ! SocketPath
202210 -- ^ node socket path, to which foldEpochState is connected to
203- , epochStateView :: ! (IORef (Maybe (AnyNewEpochState , SlotNo , BlockNo )))
204- -- ^ Automatically updated current NewEpochState. Use 'getEpochState', 'getBlockNumber', 'getSlotNumber'
205- -- to access the values.
211+ , epochStateView :: ! (IORef (Either EpochStateStatus (AnyNewEpochState , SlotNo , BlockNo )))
212+ -- ^ Automatically updated current NewEpochState. 'Left' indicates the state is not yet available
213+ -- (either not initialised or an error occurred). 'Right' contains the latest epoch state.
214+ -- Use 'getEpochState', 'getBlockNumber', 'getSlotNumber' to access the values.
206215 }
207216
208- -- | Get epoch state from the view. If the state isn't available, retry waiting up to 15 seconds. Fails when
209- -- the state is not available after 15 seconds.
217+ -- | Get epoch state from the view. If the state isn't available, retry waiting up to 25 seconds. Fails
218+ -- immediately if the background thread encountered an error, or after 25 seconds if not yet initialised .
210219getEpochState
211220 :: HasCallStack
212221 => MonadTest m
@@ -215,7 +224,7 @@ getEpochState
215224 => EpochStateView
216225 -> m AnyNewEpochState
217226getEpochState epochStateView =
218- withFrozenCallStack $ getEpochStateDetails epochStateView $ \ (nes, _, _) -> pure nes
227+ withFrozenCallStack $ ( \ (nes, _, _) -> nes) <$> getEpochStateDetails epochStateView
219228
220229getBlockNumber
221230 :: HasCallStack
@@ -225,7 +234,7 @@ getBlockNumber
225234 => EpochStateView
226235 -> m BlockNo -- ^ The number of last produced block
227236getBlockNumber epochStateView =
228- withFrozenCallStack $ getEpochStateDetails epochStateView $ \ (_, _, blockNumber) -> pure blockNumber
237+ withFrozenCallStack $ ( \ (_, _, blockNumber) -> blockNumber) <$> getEpochStateDetails epochStateView
229238
230239getSlotNumber
231240 :: HasCallStack
@@ -235,24 +244,45 @@ getSlotNumber
235244 => EpochStateView
236245 -> m SlotNo -- ^ The current slot number
237246getSlotNumber epochStateView =
238- withFrozenCallStack $ getEpochStateDetails epochStateView $ \ (_, slotNumber, _) -> pure slotNumber
247+ withFrozenCallStack $ ( \ (_, slotNumber, _) -> slotNumber) <$> getEpochStateDetails epochStateView
239248
240- -- | Utility function for accessing epoch state in `IORef`
249+ -- | Utility function for accessing epoch state in 'IORef'.
250+ -- Retries every 0.5s for up to 25 seconds while not initialised.
251+ -- Fails immediately if the background fold thread encountered an error.
241252getEpochStateDetails
242253 :: HasCallStack
243254 => MonadAssertion m
244255 => MonadTest m
245256 => MonadIO m
246257 => EpochStateView
247- -> ((AnyNewEpochState , SlotNo , BlockNo ) -> m a )
248- -> m a
249- getEpochStateDetails EpochStateView {epochStateView} f =
250- withFrozenCallStack $
251- H. byDurationM 0.5 15 " EpochStateView has not been initialized within 15 seconds" $
252- H. evalIO (readIORef epochStateView) >>= maybe H. failure f
258+ -> m (AnyNewEpochState , SlotNo , BlockNo )
259+ getEpochStateDetails EpochStateView {epochStateView} =
260+ withFrozenCallStack $ do
261+ deadline <- liftIO $ DTC. addUTCTime 25 <$> DTC. getCurrentTime
262+ go deadline
263+ where
264+ go deadline = do
265+ result <- H. evalIO $ readIORef epochStateView
266+ case result of
267+ Left (EpochStateFoldError err) -> do
268+ H. note_ $ " EpochStateView background thread failed: " <> docToString (prettyError err)
269+ H. failure
270+ Left EpochStateNotInitialised -> do
271+ currentTime <- liftIO DTC. getCurrentTime
272+ if currentTime < deadline
273+ then do
274+ H. threadDelay 500_000
275+ go deadline
276+ else do
277+ H. note_ " EpochStateView has not been initialised within 25 seconds"
278+ H. failure
279+ Right details -> pure details
253280
254281-- | Create a background thread listening for new epoch states. New epoch states are available to access
255282-- through 'EpochStateView', using query functions.
283+ -- The background thread captures any 'FoldBlocksError' into the shared state, so that consumers
284+ -- (e.g. 'getEpochStateDetails') can fail immediately with a meaningful error message instead of
285+ -- waiting for the full timeout.
256286getEpochStateView
257287 :: HasCallStack
258288 => MonadResource m
@@ -261,11 +291,15 @@ getEpochStateView
261291 -> SocketPath -- ^ node socket path
262292 -> m EpochStateView
263293getEpochStateView nodeConfigFile socketPath = withFrozenCallStack $ do
264- epochStateView <- H. evalIO $ newIORef Nothing
265- void . asyncRegister_ . runExceptT . foldEpochState nodeConfigFile socketPath QuickValidation (EpochNo maxBound ) Nothing
266- $ \ epochState slotNumber blockNumber -> do
267- liftIOAnnotated . writeIORef epochStateView $ Just (epochState, slotNumber, blockNumber)
268- pure ConditionNotMet
294+ epochStateView <- H. evalIO $ newIORef $ Left EpochStateNotInitialised
295+ void . asyncRegister_ $ do
296+ result <- runExceptT $ foldEpochState nodeConfigFile socketPath QuickValidation (EpochNo maxBound ) ()
297+ $ \ epochState slotNumber blockNumber -> do
298+ liftIOAnnotated . writeIORef epochStateView $ Right (epochState, slotNumber, blockNumber)
299+ pure ConditionNotMet
300+ case result of
301+ Left err -> writeIORef epochStateView $ Left $ EpochStateFoldError err
302+ Right _ -> pure ()
269303 pure $ EpochStateView nodeConfigFile socketPath epochStateView
270304
271305-- | Watch the epoch state view until the guard function returns 'Just' or the timeout epoch is reached.
@@ -285,7 +319,7 @@ watchEpochStateUpdate epochStateView (EpochInterval maxWait) f = withFrozenCall
285319 where
286320 go :: Word64 -> m (Maybe a )
287321 go timeout = do
288- newEpochStateDetails@ (AnyNewEpochState _ newEpochState' _, _, _) <- getEpochStateDetails epochStateView pure
322+ newEpochStateDetails@ (AnyNewEpochState _ newEpochState' _, _, _) <- getEpochStateDetails epochStateView
289323 let EpochNo currentEpoch = L. nesEL newEpochState'
290324 f newEpochStateDetails >>= \ case
291325 Just result -> pure (Just result)
@@ -573,10 +607,10 @@ assertNewEpochState epochStateView sbe maxWait lens expected = withFrozenCallSta
573607 getFromEpochStateForEra
574608 :: HasCallStack
575609 => m value
576- getFromEpochStateForEra = withFrozenCallStack $ getEpochStateDetails epochStateView $
577- \ (AnyNewEpochState actualEra newEpochState _, _, _) -> do
578- Refl <- H. leftFail $ assertErasEqual sbe actualEra
579- pure $ newEpochState ^. lens
610+ getFromEpochStateForEra = withFrozenCallStack $ do
611+ (AnyNewEpochState actualEra newEpochState _, _, _) <- getEpochStateDetails epochStateView
612+ Refl <- H. leftFail $ assertErasEqual sbe actualEra
613+ pure $ newEpochState ^. lens
580614
581615-- | Return current protocol parameters from the governance state
582616getProtocolParams :: (H. MonadAssertion m , MonadTest m , MonadIO m )
0 commit comments