Skip to content

Commit 75aece7

Browse files
committed
Remove watchEpochStateUpdate. Simplify retryUntilJustM.
1 parent 51b6046 commit 75aece7

15 files changed

Lines changed: 208 additions & 269 deletions

File tree

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
### Maintenance
2+
3+
- Unified retry/wait functions in `Testnet.Components.Query` by factoring out a common `retryUntilRightM` core.
4+
`retryUntilJustM` and `retryUntilM` are now thin wrappers over this shared primitive, eliminating duplicated timeout/polling logic.
5+
- Removed `watchEpochStateUpdate` and migrated all call sites to `retryUntilJustM`/`retryUntilM`.
6+
- Simplified `waitForBlocks` (dropped `MonadCatch` constraint, eliminated `EpochInterval maxBound` hack).
7+
Now mirrors `waitForEpochs`: relies solely on the shared retry loop's timeout instead of an outer block-count predicate, avoiding the drift between two independent snapshots of the starting block number.
8+
- Simplified `checkDRepState` by replacing direct `foldEpochState` usage with `EpochStateView` polling.
9+
- Simplified `assertNewEpochState` by replacing `watchEpochStateUpdate` with `retryUntilRightM`.
10+
- Changed `EpochStateView` from a record with three fields to a newtype wrapping the `IORef`, removing unused `nodeConfigPath` and `socketPath` fields.
11+
- Added `maybeExtractGovernanceActionExpiry` in `Testnet.EpochStateProcessing`, which reads a proposal's `gasExpiresAfter` epoch from the gov state.
12+
- Rewrote the `Gov Action Timeout` integration test to derive its wait target from the proposal's actual expiry epoch, removing the race window caused by not knowing which epoch the proposal was recorded in.
13+
The check now waits one full epoch past the removal boundary so the RATIFY-produced state is @k@-deep stable and cannot be invalidated by a chain rollback.

cardano-testnet/src/Cardano/Testnet.hs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ module Cardano.Testnet (
2929

3030
-- * EpochState processsing helper functions
3131
maybeExtractGovernanceActionIndex,
32+
maybeExtractGovernanceActionExpiry,
3233

3334
-- * Processes
3435
procChairman,

cardano-testnet/src/Testnet/Components/Query.hs

Lines changed: 70 additions & 154 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ module Testnet.Components.Query
1414
, getEpochState
1515
, getSlotNumber
1616
, getBlockNumber
17-
, watchEpochStateUpdate
17+
, getEpochStateDetails
1818

1919
, getMinDRepDeposit
2020
, getMinGovActionDeposit
@@ -60,10 +60,8 @@ import qualified Cardano.Ledger.State as L
6060

6161
import Prelude
6262

63-
import Control.Exception.Safe (MonadCatch)
6463
import Control.Monad
6564
import Control.Monad.Trans.Resource
66-
import Control.Monad.Trans.State.Strict (put)
6765
import Data.IORef
6866
import Data.List (sortOn)
6967
import qualified Data.Map as Map
@@ -123,7 +121,7 @@ waitForEpochs
123121
-> EpochInterval -- ^ Number of epochs to wait
124122
-> m EpochNo -- ^ The epoch number reached
125123
waitForEpochs epochStateView interval = withFrozenCallStack $ do
126-
void $ watchEpochStateUpdate epochStateView interval $ \_ -> pure Nothing
124+
void . retryUntilRightM epochStateView (WaitForEpochs interval) . pure $ Left ()
127125
getCurrentEpochNo epochStateView
128126

129127
-- | Wait for the requested number of blocks
@@ -132,20 +130,15 @@ waitForBlocks
132130
=> MonadIO m
133131
=> MonadTest m
134132
=> MonadAssertion m
135-
=> MonadCatch m
136133
=> EpochStateView
137134
-> Word64 -- ^ Number of blocks to wait
138135
-> m BlockNo -- ^ The block number reached
139136
waitForBlocks epochStateView numberOfBlocks = withFrozenCallStack $ do
140137
BlockNo startingBlockNumber <- getBlockNumber epochStateView
141138
H.note_ $ "Current block number: " <> show startingBlockNumber <> ". "
142139
<> "Waiting for " <> show numberOfBlocks <> " blocks"
143-
H.noteShowM . H.nothingFailM . fmap (fmap BlockNo) $
144-
watchEpochStateUpdate epochStateView (EpochInterval maxBound) $ \(_, _, BlockNo blockNumber) ->
145-
pure $
146-
if blockNumber >= startingBlockNumber + numberOfBlocks
147-
then Just blockNumber
148-
else Nothing
140+
void . retryUntilRightM epochStateView (WaitForBlocks numberOfBlocks) . pure $ Left ()
141+
getBlockNumber epochStateView
149142

150143
data TestnetWaitPeriod
151144
= WaitForEpochs EpochInterval
@@ -159,6 +152,34 @@ instance Show TestnetWaitPeriod where
159152
WaitForBlocks n -> "WaitForBlocks " <> show n
160153
WaitForSlots n -> "WaitForSlots " <> show n
161154

155+
-- | Core retry loop. Repeats the action every 300ms until it returns 'Right'
156+
-- or the timeout is reached, in which case the last 'Left' is returned.
157+
retryUntilRightM
158+
:: HasCallStack
159+
=> MonadIO m
160+
=> MonadTest m
161+
=> MonadAssertion m
162+
=> EpochStateView
163+
-> TestnetWaitPeriod
164+
-> m (Either e a)
165+
-> m (Either e a)
166+
retryUntilRightM esv timeout act = withFrozenCallStack $ do
167+
startingValue <- getCurrentValue
168+
go $ startingValue + timeoutW64
169+
where
170+
go deadline = act >>= \case
171+
r@(Right _) -> pure r
172+
l@(Left _) -> do
173+
cv <- getCurrentValue
174+
if cv > deadline
175+
then pure l
176+
else H.threadDelay 300_000 >> go deadline
177+
178+
(getCurrentValue, timeoutW64) = case timeout of
179+
WaitForEpochs (EpochInterval n) -> (unEpochNo <$> getCurrentEpochNo esv, fromIntegral n)
180+
WaitForSlots n -> (unSlotNo <$> getSlotNumber esv, n)
181+
WaitForBlocks n -> (unBlockNo <$> getBlockNumber esv, n)
182+
162183
-- | Retries the action until it returns 'Just' or the timeout is reached
163184
retryUntilJustM
164185
:: HasCallStack
@@ -169,32 +190,12 @@ retryUntilJustM
169190
-> TestnetWaitPeriod -- ^ timeout for an operation
170191
-> m (Maybe a)
171192
-> m a
172-
retryUntilJustM esv timeout act = withFrozenCallStack $ do
173-
startingValue <- getCurrentValue
174-
go startingValue
175-
where
176-
go startingValue = withFrozenCallStack $ do
177-
cv <- getCurrentValue
178-
when (timeoutW64 + startingValue < cv) $ do
179-
H.note_ $ "Action did not result in 'Just' - waited for: " <> show timeout
180-
H.failure
181-
act >>= \case
182-
Just a -> pure a
183-
Nothing -> do
184-
H.threadDelay 300_000
185-
go startingValue
186-
187-
getCurrentValue = withFrozenCallStack $
188-
case timeout of
189-
WaitForEpochs _ -> unEpochNo <$> getCurrentEpochNo esv
190-
WaitForSlots _ -> unSlotNo <$> getSlotNumber esv
191-
WaitForBlocks _ -> unBlockNo <$> getBlockNumber esv
192-
193-
timeoutW64 =
194-
case timeout of
195-
WaitForEpochs (EpochInterval n) -> fromIntegral n
196-
WaitForSlots n -> n
197-
WaitForBlocks n -> n
193+
retryUntilJustM esv timeout act = withFrozenCallStack $
194+
retryUntilRightM esv timeout (maybe (Left ()) Right <$> act) >>= \case
195+
Right a -> pure a
196+
Left () -> do
197+
H.note_ $ "Action did not result in 'Just' - waited for: " <> show timeout
198+
H.failure
198199

199200
-- | Like 'retryUntilJustM' but takes a plain action and a predicate instead of
200201
-- an action returning 'Maybe'. On timeout, annotates the last value that failed
@@ -210,34 +211,13 @@ retryUntilM
210211
-> m a -- ^ action to retry
211212
-> (a -> Bool) -- ^ predicate that must hold
212213
-> m a
213-
retryUntilM esv timeout act predicate = withFrozenCallStack $ do
214-
startingValue <- getCurrentValue
215-
go startingValue
216-
where
217-
go startingValue = withFrozenCallStack $ do
218-
result <- act
219-
if predicate result
220-
then pure result
221-
else do
222-
cv <- getCurrentValue
223-
if timeoutW64 + startingValue < cv
224-
then do
225-
H.noteShow_ result
226-
H.note_ $ "Predicate not satisfied after: " <> show timeout
227-
H.failure
228-
else H.threadDelay 300_000 >> go startingValue
229-
230-
getCurrentValue = withFrozenCallStack $
231-
case timeout of
232-
WaitForEpochs _ -> unEpochNo <$> getCurrentEpochNo esv
233-
WaitForSlots _ -> unSlotNo <$> getSlotNumber esv
234-
WaitForBlocks _ -> unBlockNo <$> getBlockNumber esv
235-
236-
timeoutW64 =
237-
case timeout of
238-
WaitForEpochs (EpochInterval n) -> fromIntegral n
239-
WaitForSlots n -> n
240-
WaitForBlocks n -> n
214+
retryUntilM esv timeout act predicate = withFrozenCallStack $
215+
retryUntilRightM esv timeout ((\r -> if predicate r then Right r else Left r) <$> act) >>= \case
216+
Right a -> pure a
217+
Left r -> do
218+
H.noteShow_ r
219+
H.note_ $ "Predicate not satisfied after: " <> show timeout
220+
H.failure
241221

242222
-- | Status of the 'EpochStateView' background thread when epoch state is not yet available
243223
data EpochStateStatus
@@ -247,12 +227,8 @@ data EpochStateStatus
247227
-- ^ The background thread encountered an error while folding blocks
248228

249229
-- | A read-only mutable pointer to an epoch state, updated automatically
250-
data EpochStateView = EpochStateView
251-
{ nodeConfigPath :: !(NodeConfigFile In)
252-
-- ^ node configuration file path
253-
, socketPath :: !SocketPath
254-
-- ^ node socket path, to which foldEpochState is connected to
255-
, epochStateView :: !(IORef (Either EpochStateStatus (AnyNewEpochState, SlotNo, BlockNo)))
230+
newtype EpochStateView = EpochStateView
231+
{ epochStateView :: IORef (Either EpochStateStatus (AnyNewEpochState, SlotNo, BlockNo))
256232
-- ^ Automatically updated current NewEpochState. 'Left' indicates the state is not yet available
257233
-- (either not initialised or an error occurred). 'Right' contains the latest epoch state.
258234
-- Use 'getEpochState', 'getBlockNumber', 'getSlotNumber' to access the values.
@@ -344,34 +320,7 @@ getEpochStateView nodeConfigFile socketPath = withFrozenCallStack $ do
344320
case result of
345321
Left err -> writeIORef epochStateView $ Left $ EpochStateFoldError err
346322
Right _ -> pure ()
347-
pure $ EpochStateView nodeConfigFile socketPath epochStateView
348-
349-
-- | Watch the epoch state view until the guard function returns 'Just' or the timeout epoch is reached.
350-
-- Executes the guard function every 300ms. Waits for at most @maxWait@ epochs.
351-
-- The function will return the result of the guard function if it is met within the number of epochs,
352-
-- otherwise it will return @Nothing@.
353-
watchEpochStateUpdate
354-
:: forall m a. (HasCallStack, MonadIO m, MonadTest m, MonadAssertion m)
355-
=> EpochStateView -- ^ The info to access the epoch state
356-
-> EpochInterval -- ^ The maximum number of epochs to wait
357-
-> ((AnyNewEpochState, SlotNo, BlockNo) -> m (Maybe a)) -- ^ The guard function (@Just@ if the condition is met, @Nothing@ otherwise)
358-
-> m (Maybe a)
359-
watchEpochStateUpdate epochStateView (EpochInterval maxWait) f = withFrozenCallStack $ do
360-
AnyNewEpochState _ newEpochState _ <- getEpochState epochStateView
361-
let EpochNo currentEpoch = L.nesEL newEpochState
362-
go $ currentEpoch + fromIntegral maxWait
363-
where
364-
go :: Word64 -> m (Maybe a)
365-
go timeout = do
366-
newEpochStateDetails@(AnyNewEpochState _ newEpochState' _, _, _) <- getEpochStateDetails epochStateView
367-
let EpochNo currentEpoch = L.nesEL newEpochState'
368-
f newEpochStateDetails >>= \case
369-
Just result -> pure (Just result)
370-
Nothing
371-
| currentEpoch > timeout -> pure Nothing
372-
| otherwise -> do
373-
H.threadDelay 300_000
374-
go timeout
323+
pure $ EpochStateView epochStateView
375324

376325
-- | Retrieve all UTxOs map from the epoch state view.
377326
findAllUtxos
@@ -505,42 +454,18 @@ checkDRepState
505454
-> Maybe a) -- ^ A function that checks whether the DRep state is correct or up to date
506455
-- and potentially inspects it.
507456
-> m a
508-
checkDRepState epochStateView@EpochStateView{nodeConfigPath, socketPath} sbe f = withFrozenCallStack $ do
509-
currentEpoch <- getCurrentEpochNo epochStateView
510-
let terminationEpoch = succ . succ $ currentEpoch
511-
result <- H.evalIO . runExceptT $ foldEpochState nodeConfigPath socketPath QuickValidation terminationEpoch Nothing
512-
$ \(AnyNewEpochState actualEra newEpochState _) _slotNumber _blockNumber -> do
513-
Refl <- either error pure $ assertErasEqual sbe actualEra
514-
let dreps =
515-
shelleyBasedEraConstraints sbe
516-
$ SQ.queryDRepState newEpochState Set.empty
517-
case f dreps of
518-
Nothing -> pure ConditionNotMet
519-
Just a -> do put $ Just a
520-
pure ConditionMet
521-
case result of
522-
Left (FoldBlocksApplyBlockError (TerminationEpochReached epochNo)) -> do
523-
H.note_ $ unlines
524-
[ "checkDRepState: condition not met before termination epoch: " <> show epochNo
525-
, "This is likely an error of this test." ]
526-
H.failure
527-
Left err -> do
528-
H.note_ $ unlines
529-
[ "checkDRepState: could not reach termination epoch: " <> docToString (prettyError err)
530-
, "This is probably an error unrelated to this test." ]
457+
checkDRepState epochStateView sbe f = withFrozenCallStack $
458+
retryUntilRightM epochStateView (WaitForEpochs $ EpochInterval 2) action >>= \case
459+
Right a -> pure a
460+
Left () -> do
461+
H.note_ "checkDRepState: condition not met within 2 epochs. This is likely a test error."
531462
H.failure
532-
Right (_, Nothing) -> do
533-
H.note_ $ unlines
534-
[ "checkDRepState: foldEpochState returned Nothing: "
535-
, "This is probably an error related to foldEpochState." ]
536-
H.failure
537-
Right (ConditionNotMet, Just _) -> do
538-
H.note_ $ unlines
539-
[ "checkDRepState: foldEpochState returned Just and ConditionNotMet: "
540-
, "This is probably an error related to foldEpochState." ]
541-
H.failure
542-
Right (ConditionMet, Just val) ->
543-
return val
463+
where
464+
action = do
465+
AnyNewEpochState actualEra newEpochState _ <- getEpochState epochStateView
466+
Refl <- H.leftFail $ assertErasEqual sbe actualEra
467+
pure . maybe (Left ()) Right . f $ shelleyBasedEraConstraints sbe
468+
$ SQ.queryDRepState newEpochState Set.empty
544469

545470
-- | Obtain governance state from node (CLI query)
546471
getGovState
@@ -627,30 +552,21 @@ assertNewEpochState
627552
-- ^ The lens to access the specific value in the epoch state.
628553
-> value -- ^ The expected value to check in the epoch state.
629554
-> m ()
630-
assertNewEpochState epochStateView sbe maxWait lens expected = withFrozenCallStack $ do
631-
mStateView <- watchEpochStateUpdate epochStateView maxWait (const checkEpochState)
632-
when (isNothing mStateView) $ do
633-
val <- getFromEpochStateForEra
634-
-- there's a tiny tiny chance that the value has changed since 'watchEpochStateUpdate'
635-
-- so check it again
636-
if val == expected
637-
then pure ()
638-
else H.failMessage callStack $ unlines
639-
[ "assertNewEpochState: expected value not reached within the time frame."
640-
, "Expected value: " <> show expected
641-
, "Actual value: " <> show val
642-
]
555+
assertNewEpochState epochStateView sbe maxWait lens expected = withFrozenCallStack $
556+
retryUntilRightM epochStateView (WaitForEpochs maxWait) checkEpochState >>= \case
557+
Right () -> pure ()
558+
Left actual -> do
559+
H.note_ $ unlines
560+
[ "assertNewEpochState: expected value not reached within " <> show maxWait
561+
, "Expected: " <> show expected
562+
, "Actual: " <> show actual
563+
]
564+
H.failure
643565
where
644-
checkEpochState
645-
:: HasCallStack
646-
=> m (Maybe ())
647566
checkEpochState = withFrozenCallStack $ do
648567
val <- getFromEpochStateForEra
649-
pure $ if val == expected then Just () else Nothing
568+
pure $ if val == expected then Right () else Left val
650569

651-
getFromEpochStateForEra
652-
:: HasCallStack
653-
=> m value
654570
getFromEpochStateForEra = withFrozenCallStack $ do
655571
(AnyNewEpochState actualEra newEpochState _, _, _) <- getEpochStateDetails epochStateView
656572
Refl <- H.leftFail $ assertErasEqual sbe actualEra

0 commit comments

Comments
 (0)