Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<!--
A new scriv changelog fragment.
Uncomment the section that is right (remove the HTML comment wrapper).
For top level release notes, leave all the headers commented out.
-->

<!--
### Breaking
-->

### Non-Breaking

- fixed false positive in `prop_diffusion_target_active_below` testnet test
- improved approach in general to target-chasing tests in diffusion testnet
and PeerSelection mock environment tests.
540 changes: 254 additions & 286 deletions cardano-diffusion/tests/lib/Test/Cardano/Network/Diffusion/Testnet.hs

Large diffs are not rendered by default.

421 changes: 263 additions & 158 deletions cardano-diffusion/tests/lib/Test/Cardano/Network/PeerSelection.hs

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<!--
A new scriv changelog fragment.

Uncomment the section that is right (remove the HTML comment wrapper).
For top level release notes, leave all the headers commented out.
-->

### Breaking

- `linger` function's arm callback now returns a `Maybe Bool`
- `keyedLinger'`s arm callback now returns a `Maybe (Set b)`
- `keyedLinger'`'s arm callback now returns a `Maybe (Set b, DiffTime))`
- The above changes allow those functions to reset signal state on `Nothing`

### Non-Breaking

- Added latch function to `Signal`
- bugfix missed promotion/demotion opportunities in:
- `ActivePeers.aboveTargetBigLedgerPeers`
- `ActivePeers.aboveTargetOther`
- `EstablishedPeers.aboveTargetOther`
- `EstablishedPeers.aboveTargetBigLedgerPeers`
- `EstablishedPeers.belowTargetLocal`
- `EstablishedPeers.belowTargetOther`
- `ActivePeers.belowTargetLocal`
Original file line number Diff line number Diff line change
Expand Up @@ -259,11 +259,12 @@ belowTargetLocal actions@PeerSelectionActions {
Set.\\ inProgressPromoteWarm
Set.\\ inProgressDemoteWarm
Set.\\ inProgressDemoteToCold
numPromoteInProgress = Set.size inProgressPromoteWarm
, not (Set.null availableToPromote)
, (HotValency hotTarget, members, membersActive) <- groupsBelowTarget
, let membersAvailableToPromote = Set.intersection
members availableToPromote
numPromoteInProgress = Set.size (Set.intersection
inProgressPromoteWarm members)
numMembersToPromote = hotTarget
- Set.size membersActive
- numPromoteInProgress
Expand Down Expand Up @@ -686,20 +687,25 @@ aboveTargetBigLedgerPeers actions@PeerSelectionActions {
}
-- Are we above the general target for number of active peers?
| numActiveBigLedgerPeers > targetNumberOfActiveBigLedgerPeers

-- Would we demote any if we could?
, let numPeersToDemote = numActiveBigLedgerPeers
, let activeBigLedger = activePeers
`Set.intersection` bigLedgerPeersSet
-- Would we demote any if we could?
numPeersToDemote = numActiveBigLedgerPeers
- targetNumberOfActiveBigLedgerPeers
- numDemoteInProgressBigLedgerPeers
-- don't drop too many and don't fail to take an opportunity
-- if there are warm peers which are async demoted
- Set.size (Set.intersection
inProgressDemoteToCold
activeBigLedger)
Comment on lines +696 to +700
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you considered fixing this in peerSelectionStateToVie, so that numberOfActiveBigLedgerPeersDemotions` already reflects to warm and to cold demotions?

, numPeersToDemote > 0

-- Are there any hot peers we actually can pick to demote?
-- For the moment we say we cannot demote local root peers.
-- TODO: review this decision. If we want to be able to demote local root
-- peers, e.g. for churn and improved selection, then we'll need an extra
-- mechanism to avoid promotion/demotion loops for local peers.
, let availableToDemote = activePeers
`Set.intersection` bigLedgerPeersSet
, let availableToDemote = activeBigLedger
Set.\\ inProgressDemoteHot
Set.\\ inProgressDemoteToCold
Set.\\ LocalRootPeers.keysSet localRootPeers
Expand Down Expand Up @@ -890,23 +896,26 @@ aboveTargetOther actions@PeerSelectionActions {
}
-- Are we above the general target for number of active peers?
| numActivePeers > targetNumberOfActivePeers

-- Would we demote any if we could?
, let numPeersToDemote = numActivePeers
, let activeNonBig = activePeers Set.\\ bigLedgerPeersSet
-- Would we demote any if we could?
numPeersToDemote = numActivePeers
- targetNumberOfActivePeers
- numDemoteInProgress
- (Set.size inProgressDemoteToCold)
-- don't drop too many and don't fail to take an opportunity
-- if there are warm peers which are async demoted
- Set.size (Set.intersection
inProgressDemoteToCold
activeNonBig)
, numPeersToDemote > 0

-- Are there any hot peers we actually can pick to demote?
-- For the moment we say we cannot demote local root peers.
-- TODO: review this decision. If we want to be able to demote local root
-- peers, e.g. for churn and improved selection, then we'll need an extra
-- mechanism to avoid promotion/demotion loops for local peers.
, let availableToDemote = activePeers
, let availableToDemote = activeNonBig
Set.\\ inProgressDemoteHot
Set.\\ LocalRootPeers.keysSet localRootPeers
Set.\\ bigLedgerPeersSet
Set.\\ inProgressDemoteToCold
, not (Set.null availableToDemote)
= Guarded Nothing $ do
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,8 @@ belowTargetLocal actions@PeerSelectionActions {
, let membersAvailableToPromote = Set.intersection members availableToPromote
numMembersToPromote = warmTarget
- Set.size membersEstablished
- numLocalConnectInProgress
- Set.size (Set.intersection
localConnectInProgress members)
Comment on lines +162 to +163
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good catch, we need to do the calculation per local group peer group.

, not (Set.null membersAvailableToPromote)
, numMembersToPromote > 0
]
Expand Down Expand Up @@ -216,7 +217,7 @@ belowTargetLocal actions@PeerSelectionActions {
viewKnownLocalRootPeers = (localRootPeersSet, _),
viewEstablishedLocalRootPeers = (localEstablishedPeers, _),
viewAvailableToConnectLocalRootPeers = (localAvailableToConnect, _),
viewColdLocalRootPeersPromotions = (localConnectInProgress, numLocalConnectInProgress)
viewColdLocalRootPeersPromotions = (localConnectInProgress, _)
} = peerSelectionStateToView extraPeersToSet extraStateToExtraCounters st


Expand Down Expand Up @@ -256,6 +257,7 @@ belowTargetOther actions@PeerSelectionActions {
policyPickColdPeersToPromote
}
st@PeerSelectionState {
localRootPeers,
knownPeers,
establishedPeers,
inProgressPromoteCold,
Expand Down Expand Up @@ -285,6 +287,7 @@ belowTargetOther actions@PeerSelectionActions {
availableToPromote = availableToConnect
Set.\\ EstablishedPeers.toSet establishedPeers
Set.\\ inProgressPromoteCold
Set.\\ LocalRootPeers.keysSet localRootPeers
numPeersToPromote = targetNumberOfEstablishedPeers
- numEstablishedPeers
- numConnectInProgress
Expand Down Expand Up @@ -507,6 +510,7 @@ jobPromoteColdPeer PeerSelectionActions {
(fuzz, stdGen') = randomR (-2, 2 :: Double) stdGen

-- exponential backoff: 5s, 10s, 20s, 40s, 80s, 160s.
-- Don't forget to change in diffusion tests if changed here
delay :: DiffTime
delay = realToFrac fuzz
+ fromIntegral
Expand Down Expand Up @@ -698,20 +702,20 @@ aboveTargetOther actions@PeerSelectionActions {
-- Or more precisely, how many established peers could we demote?
-- We only want to pick established peers that are not active, since for
-- active one we need to demote them first.
| let peerSelectionView = peerSelectionStateToView extraPeersToSet extraStateToExtraCounters st
PeerSelectionView {
| let peerSelectionView@PeerSelectionView {
viewKnownBigLedgerPeers = (bigLedgerPeersSet, _),
viewEstablishedPeers = (_, numEstablishedPeers),
viewEstablishedPeers = (establishedPeersSet, numEstablishedPeers),
viewActivePeers = (_, numActivePeers)
}
=
peerSelectionView
= peerSelectionStateToView extraPeersToSet extraStateToExtraCounters st
PeerSelectionCountersHWC {
numberOfWarmLocalRootPeers = numLocalWarmPeers
}
=
snd <$> peerSelectionView

warmPeers =
establishedPeersSet
Set.\\ Set.unions [activePeers, LocalRootPeers.keysSet localRootPeers, bigLedgerPeersSet]
-- One constraint on how many to demote is the difference in the
-- number we have now vs the target. The other constraint is that
-- we pick established peers that are not also active. These
Expand All @@ -725,17 +729,15 @@ aboveTargetOther actions@PeerSelectionActions {
- numActivePeers)
- Set.size (inProgressDemoteWarm Set.\\ bigLedgerPeersSet)
- Set.size (inProgressPromoteWarm Set.\\ bigLedgerPeersSet)
- Set.size (Set.intersection warmPeers inProgressDemoteToCold)
, numPeersToDemote > 0

availableToDemote :: Set peeraddr
availableToDemote = EstablishedPeers.toSet establishedPeers
Set.\\ activePeers
Set.\\ LocalRootPeers.keysSet localRootPeers
Set.\\ bigLedgerPeersSet
, let availableToDemote :: Set peeraddr
availableToDemote = warmPeers
Set.\\ inProgressDemoteWarm
Set.\\ inProgressPromoteWarm
Set.\\ inProgressDemoteToCold
, numPeersToDemote > 0
, not (Set.null availableToDemote)
, assert (not . Set.null $ availableToDemote) not (Set.null availableToDemote)
= Guarded Nothing $ do
selectedToDemote <- pickPeers memberExtraPeers st
policyPickWarmPeersToDemote
Expand Down Expand Up @@ -814,6 +816,9 @@ aboveTargetBigLedgerPeers actions@PeerSelectionActions {
-- We only want to pick established peers that are not active, since for
-- active one we need to demote them first.
| let bigLedgerPeersSet = PublicRootPeers.getBigLedgerPeers publicRootPeers
warmBigLedgerPeers = EstablishedPeers.toSet establishedPeers
`Set.intersection` bigLedgerPeersSet
Set.\\ activePeers
PeerSelectionCounters {
numberOfEstablishedBigLedgerPeers = numEstablishedBigLedgerPeers,
numberOfActiveBigLedgerPeers = numActiveBigLedgerPeers
Expand All @@ -825,25 +830,22 @@ aboveTargetBigLedgerPeers actions@PeerSelectionActions {
-- pick active peer. The `min` is taken so that `pickPeers` is given
-- consistent number of peers with the set of peers available to demote,
-- i.e. `availableToDemote`.
numBigLedgerPeersToDemote = min ( numEstablishedBigLedgerPeers
- targetNumberOfEstablishedBigLedgerPeers)
( numEstablishedBigLedgerPeers
- numActiveBigLedgerPeers)
- Set.size inProgressDemoteWarm
- Set.size inProgressPromoteWarm
numBigLedgerPeersToDemote =
min (numEstablishedBigLedgerPeers - targetNumberOfEstablishedBigLedgerPeers)
(numEstablishedBigLedgerPeers - numActiveBigLedgerPeers)
- Set.size inProgressDemoteWarm
- Set.size inProgressPromoteWarm
- Set.size (Set.intersection warmBigLedgerPeers inProgressDemoteToCold)
Comment on lines +836 to +838
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I first thought that we're subtracting inProgressDemoteWarm twice, but it isn't, because async demotions are only in inProgressDemoteToCold, isn't it?

Yes, that's right: I checked that assertPeerSelectionState states that inProgressDemoteWarm, inProgressPromoteWarm and inProgressDemoteToCold are all disjoint.

It would be good to extend the comment.


availableToDemote :: Set peeraddr
availableToDemote = EstablishedPeers.toSet establishedPeers
`Set.intersection` bigLedgerPeersSet
Set.\\ activePeers
availableToDemote = warmBigLedgerPeers
Set.\\ inProgressDemoteWarm
Set.\\ inProgressPromoteWarm
Set.\\ inProgressDemoteToCold

, numBigLedgerPeersToDemote > 0
, not (Set.null availableToDemote)
, assert (not $ Set.null availableToDemote) not (Set.null availableToDemote)
= Guarded Nothing $ do

selectedToDemote <- pickPeers memberExtraPeers st
policyPickWarmPeersToDemote
availableToDemote
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ module Test.Ouroboros.Network.Data.Signal
, scanl
, always
, eventually
, latch
-- * Set-based temporal operations
, keyedTimeout
, keyedLinger
Expand All @@ -53,7 +54,7 @@ import Prelude hiding (scanl, until)
import Data.Bool (bool)
import Data.Foldable qualified as Deque (toList)
import Data.List (groupBy)
import Data.Maybe (maybeToList)
import Data.Maybe (fromMaybe, maybeToList)
import Data.OrdPSQ (OrdPSQ)
import Data.OrdPSQ qualified as PSQ
import Data.Set (Set)
Expand Down Expand Up @@ -262,12 +263,13 @@ nubBy eq (Signal x0 xs0) =
-- signal is @True@.
--
linger :: DiffTime
-> (a -> Bool)
-> (a -> Maybe Bool)
-- ^ Nothing to stop tracking
-> Signal a
-> Signal Bool
linger d arm =
fmap (not . Set.null)
. keyedLinger d (bool Set.empty (Set.singleton ()) . arm)
. keyedLinger d (fmap (bool Set.empty (Set.singleton ())) . arm)


-- | Make a timeout signal, based on observing an underlying signal.
Expand Down Expand Up @@ -303,6 +305,17 @@ until start stop =
(bool Set.empty (Set.singleton ()) . stop)
(const False)

-- | The signal is scrutinised with a function and if it returns Nothing,
-- then the previous Signal output is maintained, otherwise the new
-- signal value is provided.
--
latch :: (a -> Maybe b)
-> b
-> Signal a
-> Signal b
latch f = scanl f'
where
f' z' e = fromMaybe z' (f e)

-- | Make a signal that keeps track of recent activity, based on observing an
-- underlying signal.
Expand All @@ -311,10 +324,11 @@ until start stop =
--
keyedLinger :: forall a b. Ord b
=> DiffTime
-> (a -> Set b) -- ^ The activity set signal
-> (a -> Maybe (Set b))
-- ^ The activity set signal, Nothing to stop
-> Signal a
-> Signal (Set b)
keyedLinger d arm = keyedLinger' (fmap (\x -> (x, d)) arm)
keyedLinger d arm = keyedLinger' (fmap (\x -> (x, d)) <$> arm)

-- | Make a signal that keeps track of recent activity, based on observing an
-- underlying signal.
Expand All @@ -336,7 +350,8 @@ keyedLinger d arm = keyedLinger' (fmap (\x -> (x, d)) arm)
-- those. This allow us to correctly identify valid promotion opportunities.
--
keyedLinger' :: forall a b. Ord b
=> (a -> (Set b, DiffTime)) -- ^ The activity set signal
=> (a -> Maybe (Set b, DiffTime))
-- ^ The activity set signal, resets on Nothing
-> Signal a
-> Signal (Set b)
keyedLinger' arm =
Expand All @@ -347,19 +362,20 @@ keyedLinger' arm =
where
go :: Set b
-> OrdPSQ b Time ()
-> [E (Set b, DiffTime)]
-> [E (Maybe (Set b, DiffTime))]
-> [E (Set b)]
go !_ !_ [] = []

go !lingerSet !lingerPSQ (E ts@(TS t _) xs : txs)
go !_ !_ (E _ts Nothing : txs) = go Set.empty PSQ.empty txs
go !lingerSet !lingerPSQ (E ts@(TS t _) xs@Just{} : txs)
| Just (y, t', _, lingerPSQ') <- PSQ.minView lingerPSQ
, t' < t
, (ys, lingerPSQ'') <- PSQ.atMostView t' lingerPSQ'
, let armed = Set.fromList $ y : map (\(a, _, _) -> a) ys
lingerSet' = Set.difference lingerSet armed
= E (TS t' 0) lingerSet' : go lingerSet' lingerPSQ'' (E ts xs : txs)

go !lingerSet !lingerPSQ (E ts@(TS t _) x : txs) =
go !lingerSet !lingerPSQ (E ts@(TS t _) (Just x) : txs) =
let lingerSet' = lingerSet <> fst x
t' = addTime (snd x) t
lingerPSQ' = Set.foldl' (\s y -> PSQ.insert y t' () s) lingerPSQ (fst x)
Expand Down Expand Up @@ -552,5 +568,3 @@ mergeBy cmp = merge

data MergeResult a b = OnlyInLeft a | InBoth a b | OnlyInRight b
deriving (Eq, Show)