Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce event-sourcing style in the HeadLogic #999

Merged
merged 32 commits into from
Jul 28, 2023

Conversation

pgrange
Copy link
Contributor

@pgrange pgrange commented Jul 26, 2023

To prepare for event-sourced persistency we first revamp the HeadlLogic to make it event-sourced.


  • CHANGELOG updated or not needed
  • Documentation updated or not needed
  • Haddocks updated or not needed
  • No new TODOs introduced or explained herafter

@github-actions
Copy link

github-actions bot commented Jul 26, 2023

Transactions Costs

Sizes and execution budgets for Hydra protocol transactions. Note that unlisted parameters are currently using arbitrary values and results are not fully deterministic and comparable to previous runs.

Metadata
Generated at 2023-07-28 09:54:44.710997377 UTC
Max. memory units 14000000
Max. CPU units 10000000000
Max. tx size (kB) 16384

Script summary

Name Hash Size (Bytes)
νInitial 7ceb53f05e444cfdabfd0a37a0590090066da457a1f1db30d613b8bd 4289
νCommit 70e70fc13217bfde96932956656c1d540a743b1588c845ca09dc3723 2124
νHead cda51d313c1c8285b6925ce2413def012db27f544e2bbd79b8173000 9185
μHead 1c0b665fc49bc2e9e2ce4e8252c8f37fe84dd75bd8e086abfdb92685* 4149
  • The minting policy hash is only usable for comparison. As the script is parameterized, the actual script is unique per Head.

Cost of Init Transaction

Parties Tx size % max Mem % max CPU Min fee ₳
1 4742 15.18 5.94 0.52
2 4947 15.67 6.08 0.54
3 5157 16.90 6.52 0.56
5 5562 23.73 9.14 0.65
10 6592 33.97 12.96 0.81
36 11920 96.02 36.32 1.71

Cost of Commit Transaction

This is using ada-only outputs for better comparability.

UTxO Tx size % max Mem % max CPU Min fee ₳
1 596 14.97 5.73 0.34
2 787 19.66 7.73 0.40
3 974 24.85 9.91 0.47
5 1348 36.19 14.60 0.61
10 2282 72.00 28.94 1.04
13 2851 98.33 39.25 1.35

Cost of CollectCom Transaction

Parties UTxO (bytes) Tx size % max Mem % max CPU Min fee ₳
1 57 815 28.09 10.90 0.49
2 114 1135 43.81 17.13 0.68
3 171 1456 62.29 24.50 0.89
4 227 1776 82.95 32.77 1.13

Cost of Close Transaction

Parties Tx size % max Mem % max CPU Min fee ₳
1 639 18.68 8.34 0.39
2 804 20.38 9.71 0.42
3 969 21.48 10.85 0.44
5 968 20.85 8.87 0.43
10 2132 31.27 19.66 0.64
50 5424 65.40 29.58 1.13

Cost of Contest Transaction

Parties Tx size % max Mem % max CPU Min fee ₳
1 700 25.44 11.06 0.47
2 840 26.59 12.04 0.49
3 998 28.31 13.41 0.52
5 1329 31.32 15.99 0.58
10 2161 39.92 22.84 0.73
44 7773 98.80 69.64 1.79

Cost of Abort Transaction

Some variation because of random mixture of still initial and already committed outputs.

Parties Tx size % max Mem % max CPU Min fee ₳
1 4857 22.60 9.44 0.61
2 5108 34.13 14.28 0.75
3 5496 54.06 23.07 1.00
4 5535 64.40 27.15 1.11
5 6143 97.15 41.81 1.51

Cost of FanOut Transaction

Involves spending head output and burning head tokens. Uses ada-only UTxO for better comparability.

Parties UTxO UTxO (bytes) Tx size % max Mem % max CPU Min fee ₳
5 0 0 4762 8.72 3.59 0.46
5 1 56 4798 10.12 4.41 0.48
5 5 285 4946 15.71 7.72 0.55
5 10 570 5126 22.69 11.85 0.64
5 20 1137 5483 36.67 20.11 0.83
5 30 1704 5842 50.65 28.38 1.02
5 40 2278 6201 64.63 36.65 1.21
5 50 2848 6561 78.62 44.92 1.40
5 65 3699 7101 99.62 57.34 1.68

End-To-End Benchmark Results

This page is intended to collect the latest end-to-end benchmarks results produced by Hydra's Continuous Integration system from the latest master code.

Please take those results with a grain of salt as they are currently produced from very limited cloud VMs and not controlled hardware. Instead of focusing on the absolute results, the emphasis should be on relative results, eg. how the timings for a scenario evolve as the code changes.

Generated at 2023-07-28 09:48:44.126724528 UTC

3-nodes Scenario

A rather typical setup, with 3 nodes forming a Hydra head.

Number of nodes 3
Number of txs 900
Avg. Confirmation Time (ms) 148.460465613
P99 275.94785713999994ms
P95 229.06335479999996ms
P50 139.7133455ms
Number of Invalid txs 0

Baseline Scenario

This scenario represents a minimal case and as such is a good baseline against which to assess the overhead introduced by more complex setups. There is a single hydra-node d with a single client submitting single input and single output transactions with a constant UTxO set of 1.

Number of nodes 1
Number of txs 300
Avg. Confirmation Time (ms) 10.169359150
P99 25.32321700999998ms
P95 17.12590800000002ms
P50 9.0939015ms
Number of Invalid txs 0

@github-actions
Copy link

github-actions bot commented Jul 26, 2023

Test Results

348 tests  ±0   343 ✔️ ±0   18m 55s ⏱️ - 3m 20s
116 suites ±0       5 💤 ±0 
    6 files   ±0       0 ±0 

Results for commit 8e597d0. ± Comparison against base commit 08d15d6.

♻️ This comment has been updated with latest results.

@pgrange pgrange force-pushed the ensemble/event-sourced-persistence branch from 331d761 to 07dd9cf Compare July 27, 2023 08:08
@pgrange pgrange changed the title Ensemble/event sourced persistence Introduce event-sourcing style in the HeadLogic Jul 27, 2023
@pgrange pgrange marked this pull request as ready for review July 27, 2023 09:02
Copy link
Contributor

@ffakenz ffakenz left a comment

Choose a reason for hiding this comment

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

Besides missing TODOs to tackle, this LGTM.

Just as a suggestion, I would keep the Spec state changes comments on the aggregate.

hydra-node/src/Hydra/HeadLogic/Error.hs Outdated Show resolved Hide resolved
StateChanged sc -> do
-- TODO: We should not need to query the head state here
s <- atomically queryHeadState
save $ aggregate ledger s sc
Copy link
Contributor

@ffakenz ffakenz Jul 27, 2023

Choose a reason for hiding this comment

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

idk if here or in processNextEvent, but I think we should log a warn message in case, after processing an event, the state remains the same.

basically I want to know how many times we fall under the _otherState case during state aggregate, as imo those events are invalid.

Copy link
Member

Choose a reason for hiding this comment

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

Interesting idea.

If your assumption - given an event sequence is valid, we will never not update the state after a call to aggregate - is true, then we could write a validateEvents function which does just that and call it on the events whenever we want!

So not only when processing the events one by one, also after the fact or fail startup if that is the case (would be quite restrictive).

@ch1bo ch1bo self-requested a review July 28, 2023 06:02
Copy link

@ghost ghost left a comment

Choose a reason for hiding this comment

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

I would like to remove the Ledger tx parameter from the aggregate function which introduces complexity and dependency in the interpretation of StateChanged events which seems undesirable.

hydra-node/src/Hydra/HeadLogic.hs Outdated Show resolved Hide resolved
Copy link
Member

@ch1bo ch1bo left a comment

Choose a reason for hiding this comment

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

Some clean up on comments, imports, and maybe the tick observed event should be emitted in all states.

StateChanged sc -> do
-- TODO: We should not need to query the head state here
s <- atomically queryHeadState
save $ aggregate ledger s sc
Effects _ -> pure ()
Copy link
Member

Choose a reason for hiding this comment

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

Maybe not related to this PR, but I would have expected the effect processing next to the state change persisting, next to the re-enqueuing. i.e. processEffects to be here and not done in a separate mapM (on line 110)

StateChanged sc -> do
-- TODO: We should not need to query the head state here
s <- atomically queryHeadState
save $ aggregate ledger s sc
Copy link
Member

Choose a reason for hiding this comment

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

Interesting idea.

If your assumption - given an event sequence is valid, we will never not update the state after a call to aggregate - is true, then we could write a validateEvents function which does just that and call it on the events whenever we want!

So not only when processing the events one by one, also after the fact or fail startup if that is the case (would be quite restrictive).

@@ -190,6 +185,7 @@ data NodeState tx m = NodeState
}

-- | Initialize a new 'NodeState'.
-- TODO: We do not need a TVar to keep the HeadState. REMOVE.
Copy link
Member

Choose a reason for hiding this comment

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

Do we want to do anything about this now?

I see two alternatives:

  • Explicitly handing the HeadState tx around between invocations of stepHydraNode, changing it's signature to stepHydraNode :: HydraNode tx m -> HydraState tx -> m (HydraState tx)
  • Making the monad we run in stateful. In mtl-style that would be stepHydraNode :: MonadState (HydraState tx) m => HydraNode tx m -> m (), which makes us to use StateT (HeadState tx) IO or a ReaderT (TVar (HeadState tx)) IO (or similar).

Alternative 1 is more explicit, a bit more verbose, and strictly less powerful (maybe good) as it's obviously not concurrently useful.
Alternative 2 would have very similar capabilities and complexity (instead of creating the handle you would need to run the monad transformer stack), but a departure from the handle pattern which we use throughout, and I would prefer consistency.

Copy link
Member

Choose a reason for hiding this comment

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

CC @pgrange for later reference maybe

hydra-node/src/Hydra/HeadLogic.hs Outdated Show resolved Hide resolved
hydra-node/src/Hydra/HeadLogic.hs Outdated Show resolved Hide resolved
hydra-node/src/Hydra/HeadLogic.hs Outdated Show resolved Hide resolved
hydra-node/src/Hydra/HeadLogic.hs Show resolved Hide resolved
hydra-node/src/Hydra/HeadLogic.hs Outdated Show resolved Hide resolved
hydra-node/src/Hydra/HeadLogic.hs Outdated Show resolved Hide resolved
hydra-node/test/Hydra/HeadLogicSpec.hs Show resolved Hide resolved
Copy link
Contributor

@v0d1ch v0d1ch left a comment

Choose a reason for hiding this comment

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

Nice chunk of work guys! The main concern I have is keeping the ledger out of aggregate function which would simplify things.

hydra-node/src/Hydra/HeadLogic.hs Outdated Show resolved Hide resolved
hydra-node/src/Hydra/HeadLogic.hs Outdated Show resolved Hide resolved
hydra-node/src/Hydra/HeadLogic.hs Outdated Show resolved Hide resolved
hydra-node/src/Hydra/HeadLogic.hs Outdated Show resolved Hide resolved
hydra-node/test/Hydra/HeadLogicSpec.hs Show resolved Hide resolved
@ch1bo ch1bo force-pushed the ensemble/event-sourced-persistence branch from 5dcc44e to 5281614 Compare July 28, 2023 09:27
Copy link
Member

@ch1bo ch1bo left a comment

Choose a reason for hiding this comment

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

With the changes we did in ensemble, I think this is a good step forward now.

ch1bo and others added 18 commits July 28, 2023 11:39
We use a dummy StateReplaced to have a backward compatible way to inject
a full state update through the new interfaces.
This also does not return a state and instead introduces another
getState function to access the state for assertions.
Refactoring the assertNewState into step + getState makes use of
aggregate internally and moves the test code a bit away of using
'collectState'.
We don't call the event SnaphotConfirmed because of a name collision
with Server. Should probably qualify Server at some point.

In AckSn:
* we use SnapshotHasBeenConfirmedEvent to notify that a snapshot has been confirmed
* we use SnapshotRequestDecided to notify we requested a snapshot
* we use SnapshotHasBeenConfirmed when the receiving party is not the leader or its
localTxs are null.
…ell.

The goal is to keep the ordering constraints of Combined only inside the HeadLogic.
pgrange and others added 14 commits July 28, 2023 11:39
The pruning of the transactions can be done in the
events aggregation function instead of in the update
function so we do not need to carry transactions and
utxo in the event anymore
This really hilight the symetry between events and server outputs.
Make it more clear what the event is about. Move the agregation to the
aggregate and not in the update anymore.
…event

We want to avoid requiring a ledger to interpret the StateChanged events
to allow for simpler consumption downstream (maybe even in non-haskell
contexts).
This way it is more clear that this event will always be the outcome of
a ReqTx being processed.
This way, any downstream code of the events can decide whether they want
to consume that event or not.
The NoOutcome identity is not needed and now all update outcomes must be
any of the other data constructors.

Also make Combine the mappend of Outcome to make the code a bit easier
to read.
@ch1bo ch1bo force-pushed the ensemble/event-sourced-persistence branch from 5281614 to 8e597d0 Compare July 28, 2023 09:39
@ch1bo ch1bo merged commit 3bb2e91 into master Jul 28, 2023
@ch1bo ch1bo deleted the ensemble/event-sourced-persistence branch July 28, 2023 10:48
@ch1bo ch1bo mentioned this pull request Jul 31, 2023
13 tasks
@ch1bo ch1bo linked an issue Jul 31, 2023 that may be closed by this pull request
13 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Event sourced persistence
5 participants