-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathStreaming.hs
More file actions
349 lines (319 loc) · 14.1 KB
/
Copy pathStreaming.hs
File metadata and controls
349 lines (319 loc) · 14.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
module PSR.Streaming (
streamChainSyncEvents,
unshiftFst,
streamBlocks,
streamTransactionContext,
mainLoop,
) where
--------------------------------------------------------------------------------
-- Imports
--------------------------------------------------------------------------------
import PSR.Events.Interface (EvalError (..), Events (..), ExecutionContext (..), ExecutionEventPayload (..), TraceLogs (..))
import Cardano.Api qualified as C
import Cardano.Api.Internal.Pretty (Pretty (pretty), docToText)
import Cardano.Ledger.Binary (getVersion64)
import Cardano.Ledger.Plutus (
PlutusArgs,
PlutusWithContext (..),
SLanguage (..),
isLanguage,
)
import Control.Concurrent (forkIO)
import Control.Exception (throw)
import Control.Monad (void, when)
import Data.Foldable (forM_)
import Data.Function ((&))
import Data.Map qualified as Map
import Ouroboros.Network.Protocol.ChainSync.Client (
ClientStIdle (..),
ClientStIntersect (..),
ClientStNext (..),
)
import PSR.Chain
import PSR.ConfigMap qualified as CM
import PSR.ContextBuilder
import PSR.Metrics (Counter, Gauge, Summary, getGauge, incCounter, incCounterBy, observeDuration, regCounter, regGauge, regSummary, setGauge)
import PSR.Types
import PlutusLedgerApi.Common (
MajorProtocolVersion (MajorProtocolVersion),
PlutusLedgerLanguage (..),
)
import Streamly.Data.Fold.Prelude qualified as Fold
import Streamly.Data.Scanl (Scanl)
import Streamly.Data.Scanl.Prelude qualified as Scanl
import Streamly.Data.Stream.Prelude (Stream)
import Streamly.Data.Stream.Prelude qualified as Stream
--------------------------------------------------------------------------------
-- Notes
--------------------------------------------------------------------------------
{- FAQ:
Q: Why does "subscribeToChainSyncEvents" take [C.ChainPoint] and not
C.ChainPoint?
From my understanding the node walks its chain backwards internally and returns
the first one it finds. The client and the node are not always in sync and the
client might not know the exact point the node is on. So the client may give
something like: [p10, p5, p2, genesis]
NOTE: Stream Nesting
There are 2 types of streams in streamly: Stream and StreamK. StreamK should
ideally be used instead of Stream when there are nested operations. Stream does
not perform well with deep nesting as fusion breaks. Alternatively, we can use
unfoldEach for nesting streams while supporting fusion.
-}
--------------------------------------------------------------------------------
-- Streaming Utils
--------------------------------------------------------------------------------
-- Pairs the current set of transactions with the previous chainpoint
unshiftFst :: (Monad m) => Scanl m (a, b) (a, b)
unshiftFst =
snd <$> Scanl.mkScanl step initial
where
step (prev, _) (new, txs) = (new, (prev, txs))
initial =
( error "unshiftFst: Use postscanl"
, error "unshiftFst: Use postscanl"
)
--------------------------------------------------------------------------------
-- Main
--------------------------------------------------------------------------------
{- | "subscribeToChainSyncEvents" uses the chain-sync mini-protocol to
connect to a locally running node and fetch blocks from the given
starting point.
-}
subscribeToChainSyncEvents ::
-- | Connection Info
C.LocalNodeConnectInfo ->
-- | The points on the chain to start streaming from
[C.ChainPoint] ->
(ChainSyncEvent -> IO ()) ->
IO ()
subscribeToChainSyncEvents conn points callback =
C.connectToLocalNode
conn
C.LocalNodeClientProtocols
{ C.localChainSyncClient =
C.LocalChainSyncClient $ C.ChainSyncClient chainSyncClient
, C.localStateQueryClient = Nothing
, C.localTxMonitoringClient = Nothing
, C.localTxSubmissionClient = Nothing
}
where
-- TODO: When points == [] ensure we start streaming from the latest block.
-- To start streaming from genesis, use [ChainPointAtGenesis].
chainSyncClient =
case points of
[] -> sendRequestNext
_ -> sendOnIntersect
-- This is required if we want PSR to start indexing events from a specific
-- checkpoint. Essentially set the pointer to somewhere in the past.
sendOnIntersect =
pure $
SendMsgFindIntersect points $
ClientStIntersect
{ recvMsgIntersectFound = \chainPoint tip ->
C.ChainSyncClient $ do
callback (RollBackward chainPoint tip)
sendRequestNext
, recvMsgIntersectNotFound = throw NoIntersectionFound
}
-- NOTE: We should handle this properly via callback if required.
actionOnAwait = pure ()
-- This is required to go to the next block. Essentially forward the
-- pointer.
sendRequestNext =
pure $ SendMsgRequestNext actionOnAwait do
ClientStNext
{ recvMsgRollForward = \blockInMode tip ->
C.ChainSyncClient $ do
callback (RollForward blockInMode tip)
sendRequestNext
, recvMsgRollBackward = \chainPoint tip ->
C.ChainSyncClient $ do
callback (RollBackward chainPoint tip)
sendRequestNext
}
--------------------------------------------------------------------------------
-- Streams
--------------------------------------------------------------------------------
traceChainSyncEvent :: Events -> ChainSyncEvent -> IO ()
traceChainSyncEvent events = \case
RollForward (C.BlockInMode _ blk) _ -> do
let header = C.getBlockHeader blk
events.addSelectionEvent header
_ -> pure ()
traceTransactionExecutionResult :: Events -> TransactionContext era -> IO ()
traceTransactionExecutionResult events tc =
-- TODO: we need to use the script purpose to tell the difference between executions
-- TODO: Nesting Eithers made this more difficult to read. We need to clean
-- this up
forM_ (Map.elems tc.ctxTransactionExecutionResult) $ \case
Right elems ->
forM_ elems $ \case
(sname, val) -> case val of
-- This is a script evaluation error.
Left (C.ScriptErrorEvaluationFailed (C.DebugPlutusFailure evalErr pwc exUnits logs)) ->
addEvent sname pwc logs exUnits (Just evalErr)
Right (pwc, logs, exUnits) ->
addEvent sname pwc logs exUnits Nothing
-- TODO: we might need to cover more errors, ex budget
_ -> pure ()
-- NOTE: This is not a script evaluation error but a script
-- selection error.
-- TODO: We should also report script missing error.
Left _ -> pure ()
where
addEvent
scriptName
PlutusWithContext
{ pwcArgs = args :: PlutusArgs l
, pwcCostModel
, pwcScriptHash
, pwcProtocolVersion
, pwcExUnits
}
logs
exUnits
evalError' = do
let
scriptHash = C.ScriptHash pwcScriptHash
ledgerLanguage =
case isLanguage @l of
SPlutusV1 -> PlutusV1
SPlutusV2 -> PlutusV2
SPlutusV3 -> PlutusV3
(scriptContext, datum, redeemer) = extractContextDatumRedeemer args
context =
ExecutionContext
{ transactionHash = C.getTxId $ C.getTxBody tc.ctxTransaction
, scriptName = scriptName
, scriptHash
, ledgerLanguage
, majorProtocolVersion = MajorProtocolVersion (fromIntegral (getVersion64 pwcProtocolVersion))
, datum
, redeemer
, scriptContext
, costModel = pwcCostModel
, exMaxBudget = pwcExUnits
}
void $ do
eci <- events.addExecutionContext tc.ctxBlockHeader context
events.addExecutionEvent tc.ctxBlockHeader eci $
ExecutionEventPayload
{ traceLogs = TraceLogs logs
, exUnits
, evalError = EvalError . docToText . pretty <$> evalError'
, context
}
streamChainSyncEvents ::
-- | Connection Info
C.LocalNodeConnectInfo ->
-- | The points on the chain to start streaming from
[C.ChainPoint] ->
Stream IO ChainSyncEvent
streamChainSyncEvents conn points =
Stream.fromCallback (void . forkIO . subscribeToChainSyncEvents conn points)
countTransactions :: Maybe Block -> Int
countTransactions Nothing = 0
countTransactions (Just (Block _ _ txs)) = length txs
streamBlocks :: StreamingMetrics -> Events -> CM.ConfigMap -> [C.ChainPoint] -> Stream IO (C.ChainPoint, Block)
streamBlocks metrics events CM.ConfigMap{..} points =
streamChainSyncEvents cmLocalNodeConn points
& Stream.trace (const (incCounter metrics.blocks_since_start))
& Stream.trace (traceChainSyncEvent events)
& fmap getEventBlock
& Stream.postscanl unshiftFst
-- TODO: Can we filter here to remove any block that doesn't reference
-- any scripts we care about?
& Stream.trace (\(_, txs) -> incCounterBy metrics.tx_since_start (countTransactions txs))
& Stream.mapMaybe (\(a, b) -> (a,) <$> b)
streamTransactionContext ::
ContextBuilderMetrics -> CM.ConfigMap -> BlockContext era -> Stream IO (TransactionContext era)
streamTransactionContext cbMetrics cm ctx1@BlockContext{..} =
Stream.fromList ctxTransactions
& Stream.mapM (mkTransactionContext cbMetrics cm ctx1)
--------------------------------------------------------------------------------
-- Main
--------------------------------------------------------------------------------
-- TODO: Save the auxiliary state to disk when the application is exiting. When
-- restarting, use the saved auxillary state instead of querying the state from
-- the cardano-node.
--
-- NOTE: Think more about how we can reduce the bandwidth of the initial costly
-- query.
--
mainLoop :: Events -> CM.ConfigMap -> [C.ChainPoint] -> IO ()
mainLoop events cm@CM.ConfigMap{..} points = do
metrics <- initialiseMetrics
cbMetrics <- initialiseContextBuilderMetrics
streamBlocks metrics events cm points
& Stream.fold (Fold.foldlM' (consumeBlock metrics cbMetrics) (pure Nothing))
& void
where
confHashes = Map.keysSet cmScripts
consumeBlock metrics cbMetrics mUtxoMap (previousChainPt, Block bh sbe txList) = do
let getUtxoMap =
case mUtxoMap of
Nothing ->
getSpendProjectedUtxoMap cmLocalNodeConn cmLeashId previousChainPt sbe confHashes
Just utxoMap -> pure utxoMap
-- NOTE: We only consume a specific set of transactions and not all
-- the transactions in a block. We use the internal UTxO map to
-- decide which transaction meet the criteria.
consumeTransactions era selectedTxs = do
ctx1 <- mkBlockContext cbMetrics bh cmLocalNodeConn cmLeashId previousChainPt era selectedTxs
streamTransactionContext cbMetrics cm ctx1
& Stream.trace (traceTransactionExecutionResult events)
& Stream.fold Fold.drain
withAlonzoEra era = do
prevUtxoMap <- getUtxoMap
let (newUtxoMap, selectedTxs) =
selectScriptTriggeredTxs confHashes prevUtxoMap txList
-- TODO: It is possible to maintain this count at all times. We
-- should do that instead of going through our map each and
-- every time.
observeUtxoMapValue metrics (Map.size newUtxoMap)
-- NOTE: In most cases the list of selected transactions is
-- going to be empty. It is non-empty if and only if,
-- 1. The block has transactions that involve script executions
-- 2. These scripts have a non-empty intersection with the
-- configured scripts
when (not (null selectedTxs)) $ consumeTransactions era selectedTxs
pure $ Just newUtxoMap
observeDuration metrics.mainLoop_consumeBlock_runtime $
case proveAlonzoEraOnwards sbe of
Nothing -> pure mUtxoMap
Just era -> withAlonzoEra era
observeUtxoMapValue :: StreamingMetrics -> Int -> IO ()
observeUtxoMapValue metrics currVal0 = do
let currVal = fromIntegral currVal0
setGauge metrics.internal_utxo_map_size_cur currVal
prevVal <- getGauge metrics.internal_utxo_map_size_max
when (currVal > prevVal) $
setGauge metrics.internal_utxo_map_size_max currVal
--------------------------------------------------------------------------------
-- Module metrics
--------------------------------------------------------------------------------
data StreamingMetrics = StreamingMetrics
{ mainLoop_consumeBlock_runtime :: Summary
, blocks_since_start :: Counter
, tx_since_start :: Counter
, -- TODO: Make the name consice.
internal_utxo_map_size_cur :: Gauge
, internal_utxo_map_size_max :: Gauge
}
initialiseMetrics :: IO StreamingMetrics
initialiseMetrics = do
mainLoop_consumeBlock_runtime <-
regSummary
"mainLoop_consumeBlock_runtime"
"Runtime summary for consumeBlock"
blocks_since_start <-
regCounter
"blocks_since_start"
"Number of transactions seen since application start"
tx_since_start <-
regCounter
"tx_since_start"
"The number of transactions seen since application start"
internal_utxo_map_size_cur <- regGauge "internal_utxo_map_size_cur" ""
internal_utxo_map_size_max <- regGauge "internal_utxo_map_size_max" ""
pure StreamingMetrics{..}