mirror of
https://github.com/logos-blockchain/logos-blockchain-testing.git
synced 2026-01-03 05:43:09 +00:00
577 lines
31 KiB
HTML
577 lines
31 KiB
HTML
|
|
<!DOCTYPE HTML>
|
||
|
|
<html lang="en" class="light" dir="ltr">
|
||
|
|
<head>
|
||
|
|
<!-- Book generated using mdBook -->
|
||
|
|
<meta charset="UTF-8">
|
||
|
|
<title>RunContext: BlockFeed & Node Control - Logos Blockchain Testing Framework Book</title>
|
||
|
|
|
||
|
|
|
||
|
|
<!-- Custom HTML head -->
|
||
|
|
|
||
|
|
<meta name="description" content="">
|
||
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
|
|
<meta name="theme-color" content="#ffffff">
|
||
|
|
|
||
|
|
<link rel="icon" href="favicon.svg">
|
||
|
|
<link rel="shortcut icon" href="favicon.png">
|
||
|
|
<link rel="stylesheet" href="css/variables.css">
|
||
|
|
<link rel="stylesheet" href="css/general.css">
|
||
|
|
<link rel="stylesheet" href="css/chrome.css">
|
||
|
|
<link rel="stylesheet" href="css/print.css" media="print">
|
||
|
|
|
||
|
|
<!-- Fonts -->
|
||
|
|
<link rel="stylesheet" href="FontAwesome/css/font-awesome.css">
|
||
|
|
<link rel="stylesheet" href="fonts/fonts.css">
|
||
|
|
|
||
|
|
<!-- Highlight.js Stylesheets -->
|
||
|
|
<link rel="stylesheet" href="highlight.css">
|
||
|
|
<link rel="stylesheet" href="tomorrow-night.css">
|
||
|
|
<link rel="stylesheet" href="ayu-highlight.css">
|
||
|
|
|
||
|
|
<!-- Custom theme stylesheets -->
|
||
|
|
|
||
|
|
</head>
|
||
|
|
<body class="sidebar-visible no-js">
|
||
|
|
<div id="body-container">
|
||
|
|
<!-- Provide site root to javascript -->
|
||
|
|
<script>
|
||
|
|
var path_to_root = "";
|
||
|
|
var default_theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "navy" : "light";
|
||
|
|
</script>
|
||
|
|
|
||
|
|
<!-- Work around some values being stored in localStorage wrapped in quotes -->
|
||
|
|
<script>
|
||
|
|
try {
|
||
|
|
var theme = localStorage.getItem('mdbook-theme');
|
||
|
|
var sidebar = localStorage.getItem('mdbook-sidebar');
|
||
|
|
|
||
|
|
if (theme.startsWith('"') && theme.endsWith('"')) {
|
||
|
|
localStorage.setItem('mdbook-theme', theme.slice(1, theme.length - 1));
|
||
|
|
}
|
||
|
|
|
||
|
|
if (sidebar.startsWith('"') && sidebar.endsWith('"')) {
|
||
|
|
localStorage.setItem('mdbook-sidebar', sidebar.slice(1, sidebar.length - 1));
|
||
|
|
}
|
||
|
|
} catch (e) { }
|
||
|
|
</script>
|
||
|
|
|
||
|
|
<!-- Set the theme before any content is loaded, prevents flash -->
|
||
|
|
<script>
|
||
|
|
var theme;
|
||
|
|
try { theme = localStorage.getItem('mdbook-theme'); } catch(e) { }
|
||
|
|
if (theme === null || theme === undefined) { theme = default_theme; }
|
||
|
|
var html = document.querySelector('html');
|
||
|
|
html.classList.remove('light')
|
||
|
|
html.classList.add(theme);
|
||
|
|
var body = document.querySelector('body');
|
||
|
|
body.classList.remove('no-js')
|
||
|
|
body.classList.add('js');
|
||
|
|
</script>
|
||
|
|
|
||
|
|
<input type="checkbox" id="sidebar-toggle-anchor" class="hidden">
|
||
|
|
|
||
|
|
<!-- Hide / unhide sidebar before it is displayed -->
|
||
|
|
<script>
|
||
|
|
var body = document.querySelector('body');
|
||
|
|
var sidebar = null;
|
||
|
|
var sidebar_toggle = document.getElementById("sidebar-toggle-anchor");
|
||
|
|
if (document.body.clientWidth >= 1080) {
|
||
|
|
try { sidebar = localStorage.getItem('mdbook-sidebar'); } catch(e) { }
|
||
|
|
sidebar = sidebar || 'visible';
|
||
|
|
} else {
|
||
|
|
sidebar = 'hidden';
|
||
|
|
}
|
||
|
|
sidebar_toggle.checked = sidebar === 'visible';
|
||
|
|
body.classList.remove('sidebar-visible');
|
||
|
|
body.classList.add("sidebar-" + sidebar);
|
||
|
|
</script>
|
||
|
|
|
||
|
|
<nav id="sidebar" class="sidebar" aria-label="Table of contents">
|
||
|
|
<div class="sidebar-scrollbox">
|
||
|
|
<ol class="chapter"><li class="chapter-item expanded "><a href="project-context-primer.html"><strong aria-hidden="true">1.</strong> Project Context Primer</a></li><li class="chapter-item expanded "><a href="what-you-will-learn.html"><strong aria-hidden="true">2.</strong> What You Will Learn</a></li><li class="chapter-item expanded "><a href="quickstart.html"><strong aria-hidden="true">3.</strong> Quickstart</a></li><li class="chapter-item expanded "><a href="part-i.html"><strong aria-hidden="true">4.</strong> Part I — Foundations</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="introduction.html"><strong aria-hidden="true">4.1.</strong> Introduction</a></li><li class="chapter-item expanded "><a href="architecture-overview.html"><strong aria-hidden="true">4.2.</strong> Architecture Overview</a></li><li class="chapter-item expanded "><a href="testing-philosophy.html"><strong aria-hidden="true">4.3.</strong> Testing Philosophy</a></li><li class="chapter-item expanded "><a href="scenario-lifecycle.html"><strong aria-hidden="true">4.4.</strong> Scenario Lifecycle</a></li><li class="chapter-item expanded "><a href="design-rationale.html"><strong aria-hidden="true">4.5.</strong> Design Rationale</a></li></ol></li><li class="chapter-item expanded "><a href="part-ii.html"><strong aria-hidden="true">5.</strong> Part II — User Guide</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="workspace-layout.html"><strong aria-hidden="true">5.1.</strong> Workspace Layout</a></li><li class="chapter-item expanded "><a href="annotated-tree.html"><strong aria-hidden="true">5.2.</strong> Annotated Tree</a></li><li class="chapter-item expanded "><a href="authoring-scenarios.html"><strong aria-hidden="true">5.3.</strong> Authoring Scenarios</a></li><li class="chapter-item expanded "><a href="workloads.html"><strong aria-hidden="true">5.4.</strong> Core Content: Workloads & Expectations</a></li><li class="chapter-item expanded "><a href="scenario-builder-ext-patterns.html"><strong aria-hidden="true">5.5.</strong> Core Content: ScenarioBuilderExt Patterns</a></li><li class="chapter-item expanded "><a href="best-practices.html"><strong aria-hidden="true">5.6.</strong> Best Practices</a></li><li class="chapter-item expanded "><a href="usage-patterns.html"><strong aria-hidden="true">5.7.</strong> Usage Patterns</a></li><li class="chapter-item expanded "><a href="examples.html"><strong aria-hidden="true">5.8.</strong> Examples</a></li><li class="chapter-item expanded "><a href="examples-advanced.html"><strong aria-hidden="true">5.9.</strong> Advanced & Artificial Examples</a></li><li class="chapter-item expanded "><a href="cucumber-bdd.html"><strong aria-hidden="true">5.10.</strong> Cucumber/BDD Interface</a></li><li class="chapter-item expanded "><a href="running-scenarios.html"><strong aria-hidden="true">5.11.</strong> Running Scenarios</a></li><li class="chapter-item expanded "><a href="runners.html"><strong aria-hidden="true">5.12.</strong> Runners</a></li><li class="chapter-item expanded "><a href="node-control.html" class="active"><strong aria-hidden="true">5.13.</strong> RunContext: BlockFeed & Node Control</a></li><li class="chapter-item expanded "><a href="chaos.html"><strong aria-hidden="true">5.14.</strong> Chaos Workloads</a></li><li class="chapter-item expanded "><a href="topology-chaos.html"><strong aria-hidden="true">5.15.</strong> Topology & Chaos Patterns</a></li></ol></li><li class="chapter-item expanded "><a href="part-iii.html"><strong aria-hidden="true">6.</strong> Part III — Developer Reference</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="scenario-model.html"><strong aria-hidden="true">6.1.</strong> Scenario Model (Developer Level)</a></li><li class="chapter-item expanded "><a href="api-levels.html"><strong aria-hidden="true">6.2.</strong> API Levels: Builder DSL vs. Direct</a></li><li class="chapter-item expanded "><a href="extending.html"><strong aria-hidden="true">6.3.</strong> Extending the Framework</a></li><li class="chap
|
||
|
|
</div>
|
||
|
|
<div id="sidebar-resize-handle" class="sidebar-resize-handle">
|
||
|
|
<div class="sidebar-resize-indicator"></div>
|
||
|
|
</div>
|
||
|
|
</nav>
|
||
|
|
|
||
|
|
<!-- Track and set sidebar scroll position -->
|
||
|
|
<script>
|
||
|
|
var sidebarScrollbox = document.querySelector('#sidebar .sidebar-scrollbox');
|
||
|
|
sidebarScrollbox.addEventListener('click', function(e) {
|
||
|
|
if (e.target.tagName === 'A') {
|
||
|
|
sessionStorage.setItem('sidebar-scroll', sidebarScrollbox.scrollTop);
|
||
|
|
}
|
||
|
|
}, { passive: true });
|
||
|
|
var sidebarScrollTop = sessionStorage.getItem('sidebar-scroll');
|
||
|
|
sessionStorage.removeItem('sidebar-scroll');
|
||
|
|
if (sidebarScrollTop) {
|
||
|
|
// preserve sidebar scroll position when navigating via links within sidebar
|
||
|
|
sidebarScrollbox.scrollTop = sidebarScrollTop;
|
||
|
|
} else {
|
||
|
|
// scroll sidebar to current active section when navigating via "next/previous chapter" buttons
|
||
|
|
var activeSection = document.querySelector('#sidebar .active');
|
||
|
|
if (activeSection) {
|
||
|
|
activeSection.scrollIntoView({ block: 'center' });
|
||
|
|
}
|
||
|
|
}
|
||
|
|
</script>
|
||
|
|
|
||
|
|
<div id="page-wrapper" class="page-wrapper">
|
||
|
|
|
||
|
|
<div class="page">
|
||
|
|
<div id="menu-bar-hover-placeholder"></div>
|
||
|
|
<div id="menu-bar" class="menu-bar sticky">
|
||
|
|
<div class="left-buttons">
|
||
|
|
<label id="sidebar-toggle" class="icon-button" for="sidebar-toggle-anchor" title="Toggle Table of Contents" aria-label="Toggle Table of Contents" aria-controls="sidebar">
|
||
|
|
<i class="fa fa-bars"></i>
|
||
|
|
</label>
|
||
|
|
<button id="theme-toggle" class="icon-button" type="button" title="Change theme" aria-label="Change theme" aria-haspopup="true" aria-expanded="false" aria-controls="theme-list">
|
||
|
|
<i class="fa fa-paint-brush"></i>
|
||
|
|
</button>
|
||
|
|
<ul id="theme-list" class="theme-popup" aria-label="Themes" role="menu">
|
||
|
|
<li role="none"><button role="menuitem" class="theme" id="light">Light</button></li>
|
||
|
|
<li role="none"><button role="menuitem" class="theme" id="rust">Rust</button></li>
|
||
|
|
<li role="none"><button role="menuitem" class="theme" id="coal">Coal</button></li>
|
||
|
|
<li role="none"><button role="menuitem" class="theme" id="navy">Navy</button></li>
|
||
|
|
<li role="none"><button role="menuitem" class="theme" id="ayu">Ayu</button></li>
|
||
|
|
</ul>
|
||
|
|
<button id="search-toggle" class="icon-button" type="button" title="Search. (Shortkey: s)" aria-label="Toggle Searchbar" aria-expanded="false" aria-keyshortcuts="S" aria-controls="searchbar">
|
||
|
|
<i class="fa fa-search"></i>
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<h1 class="menu-title">Logos Blockchain Testing Framework Book</h1>
|
||
|
|
|
||
|
|
<div class="right-buttons">
|
||
|
|
<a href="print.html" title="Print this book" aria-label="Print this book">
|
||
|
|
<i id="print-button" class="fa fa-print"></i>
|
||
|
|
</a>
|
||
|
|
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div id="search-wrapper" class="hidden">
|
||
|
|
<form id="searchbar-outer" class="searchbar-outer">
|
||
|
|
<input type="search" id="searchbar" name="searchbar" placeholder="Search this book ..." aria-controls="searchresults-outer" aria-describedby="searchresults-header">
|
||
|
|
</form>
|
||
|
|
<div id="searchresults-outer" class="searchresults-outer hidden">
|
||
|
|
<div id="searchresults-header" class="searchresults-header"></div>
|
||
|
|
<ul id="searchresults">
|
||
|
|
</ul>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Apply ARIA attributes after the sidebar and the sidebar toggle button are added to the DOM -->
|
||
|
|
<script>
|
||
|
|
document.getElementById('sidebar-toggle').setAttribute('aria-expanded', sidebar === 'visible');
|
||
|
|
document.getElementById('sidebar').setAttribute('aria-hidden', sidebar !== 'visible');
|
||
|
|
Array.from(document.querySelectorAll('#sidebar a')).forEach(function(link) {
|
||
|
|
link.setAttribute('tabIndex', sidebar === 'visible' ? 0 : -1);
|
||
|
|
});
|
||
|
|
</script>
|
||
|
|
|
||
|
|
<div id="content" class="content">
|
||
|
|
<main>
|
||
|
|
<h1 id="runcontext-blockfeed--node-control"><a class="header" href="#runcontext-blockfeed--node-control">RunContext: BlockFeed & Node Control</a></h1>
|
||
|
|
<p>The deployer supplies a <code>RunContext</code> that workloads and expectations share. It
|
||
|
|
provides:</p>
|
||
|
|
<ul>
|
||
|
|
<li>Topology descriptors (<code>GeneratedTopology</code>)</li>
|
||
|
|
<li>Client handles (<code>NodeClients</code> / <code>ClusterClient</code>) for HTTP/RPC calls</li>
|
||
|
|
<li>Metrics (<code>RunMetrics</code>, <code>Metrics</code>) and block feed</li>
|
||
|
|
<li>Optional <code>NodeControlHandle</code> for managing nodes</li>
|
||
|
|
</ul>
|
||
|
|
<h2 id="blockfeed-observing-block-production"><a class="header" href="#blockfeed-observing-block-production">BlockFeed: Observing Block Production</a></h2>
|
||
|
|
<p>The <code>BlockFeed</code> is a broadcast stream of block observations that allows workloads and expectations to monitor blockchain progress in real-time. It polls a validator node continuously and broadcasts new blocks to all subscribers.</p>
|
||
|
|
<h3 id="what-blockfeed-provides"><a class="header" href="#what-blockfeed-provides">What BlockFeed Provides</a></h3>
|
||
|
|
<p><strong>Real-time block stream:</strong></p>
|
||
|
|
<ul>
|
||
|
|
<li>Subscribe to receive <code>BlockRecord</code> notifications as blocks are produced</li>
|
||
|
|
<li>Each record includes the block header (<code>HeaderId</code>) and full block payload</li>
|
||
|
|
<li>Backed by a background task that polls node storage every second</li>
|
||
|
|
</ul>
|
||
|
|
<p><strong>Block statistics:</strong></p>
|
||
|
|
<ul>
|
||
|
|
<li>Track total transactions across all observed blocks</li>
|
||
|
|
<li>Access via <code>block_feed.stats().total_transactions()</code></li>
|
||
|
|
</ul>
|
||
|
|
<p><strong>Broadcast semantics:</strong></p>
|
||
|
|
<ul>
|
||
|
|
<li>Multiple subscribers can receive the same blocks independently</li>
|
||
|
|
<li>Late subscribers start receiving from current block (no history replay)</li>
|
||
|
|
<li>Lagged subscribers skip missed blocks automatically</li>
|
||
|
|
</ul>
|
||
|
|
<h3 id="accessing-blockfeed"><a class="header" href="#accessing-blockfeed">Accessing BlockFeed</a></h3>
|
||
|
|
<p>BlockFeed is available through <code>RunContext</code>:</p>
|
||
|
|
<pre><code class="language-rust ignore">let block_feed = ctx.block_feed();</code></pre>
|
||
|
|
<h3 id="usage-in-expectations"><a class="header" href="#usage-in-expectations">Usage in Expectations</a></h3>
|
||
|
|
<p>Expectations typically use BlockFeed to verify block production and inclusion of transactions/data.</p>
|
||
|
|
<p><strong>Example: Counting blocks during a run</strong></p>
|
||
|
|
<pre><code class="language-rust ignore">use std::sync::{
|
||
|
|
Arc,
|
||
|
|
atomic::{AtomicU64, Ordering},
|
||
|
|
};
|
||
|
|
|
||
|
|
use async_trait::async_trait;
|
||
|
|
use testing_framework_core::scenario::{DynError, Expectation, RunContext};
|
||
|
|
|
||
|
|
struct MinimumBlocksExpectation {
|
||
|
|
min_blocks: u64,
|
||
|
|
captured_blocks: Option<Arc<AtomicU64>>,
|
||
|
|
}
|
||
|
|
|
||
|
|
#[async_trait]
|
||
|
|
impl Expectation for MinimumBlocksExpectation {
|
||
|
|
fn name(&self) -> &'static str {
|
||
|
|
"minimum_blocks"
|
||
|
|
}
|
||
|
|
|
||
|
|
async fn start_capture(&mut self, ctx: &RunContext) -> Result<(), DynError> {
|
||
|
|
let block_count = Arc::new(AtomicU64::new(0));
|
||
|
|
let block_count_task = Arc::clone(&block_count);
|
||
|
|
|
||
|
|
// Subscribe to block feed
|
||
|
|
let mut receiver = ctx.block_feed().subscribe();
|
||
|
|
|
||
|
|
// Spawn a task to count blocks
|
||
|
|
tokio::spawn(async move {
|
||
|
|
loop {
|
||
|
|
match receiver.recv().await {
|
||
|
|
Ok(_record) => {
|
||
|
|
block_count_task.fetch_add(1, Ordering::Relaxed);
|
||
|
|
}
|
||
|
|
Err(tokio::sync::broadcast::error::RecvError::Lagged(skipped)) => {
|
||
|
|
tracing::debug!(skipped, "receiver lagged, skipping blocks");
|
||
|
|
}
|
||
|
|
Err(tokio::sync::broadcast::error::RecvError::Closed) => {
|
||
|
|
tracing::debug!("block feed closed");
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
self.captured_blocks = Some(block_count);
|
||
|
|
Ok(())
|
||
|
|
}
|
||
|
|
|
||
|
|
async fn evaluate(&mut self, ctx: &RunContext) -> Result<(), DynError> {
|
||
|
|
let blocks = self.captured_blocks
|
||
|
|
.as_ref()
|
||
|
|
.expect("start_capture must be called first")
|
||
|
|
.load(Ordering::Relaxed);
|
||
|
|
|
||
|
|
if blocks < self.min_blocks {
|
||
|
|
return Err(format!(
|
||
|
|
"expected at least {} blocks, observed {}",
|
||
|
|
self.min_blocks, blocks
|
||
|
|
).into());
|
||
|
|
}
|
||
|
|
|
||
|
|
tracing::info!(blocks, min = self.min_blocks, "minimum blocks expectation passed");
|
||
|
|
Ok(())
|
||
|
|
}
|
||
|
|
}</code></pre>
|
||
|
|
<p><strong>Example: Inspecting block contents</strong></p>
|
||
|
|
<pre><code class="language-rust ignore">use testing_framework_core::scenario::{DynError, RunContext};
|
||
|
|
|
||
|
|
async fn start_capture(ctx: &RunContext) -> Result<(), DynError> {
|
||
|
|
let mut receiver = ctx.block_feed().subscribe();
|
||
|
|
|
||
|
|
tokio::spawn(async move {
|
||
|
|
loop {
|
||
|
|
match receiver.recv().await {
|
||
|
|
Ok(record) => {
|
||
|
|
// Access block header
|
||
|
|
let header_id = &record.header;
|
||
|
|
|
||
|
|
// Access full block
|
||
|
|
let tx_count = record.block.transactions().len();
|
||
|
|
|
||
|
|
tracing::debug!(
|
||
|
|
?header_id,
|
||
|
|
tx_count,
|
||
|
|
"observed block"
|
||
|
|
);
|
||
|
|
|
||
|
|
// Process transactions, DA blobs, etc.
|
||
|
|
}
|
||
|
|
Err(tokio::sync::broadcast::error::RecvError::Closed) => break,
|
||
|
|
Err(_) => continue,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
Ok(())
|
||
|
|
}</code></pre>
|
||
|
|
<h3 id="usage-in-workloads"><a class="header" href="#usage-in-workloads">Usage in Workloads</a></h3>
|
||
|
|
<p>Workloads can use BlockFeed to coordinate timing or wait for specific conditions before proceeding.</p>
|
||
|
|
<p><strong>Example: Wait for N blocks before starting</strong></p>
|
||
|
|
<pre><code class="language-rust ignore">use async_trait::async_trait;
|
||
|
|
use testing_framework_core::scenario::{DynError, RunContext, Workload};
|
||
|
|
|
||
|
|
struct DelayedWorkload {
|
||
|
|
wait_blocks: usize,
|
||
|
|
}
|
||
|
|
|
||
|
|
#[async_trait]
|
||
|
|
impl Workload for DelayedWorkload {
|
||
|
|
fn name(&self) -> &str {
|
||
|
|
"delayed_workload"
|
||
|
|
}
|
||
|
|
|
||
|
|
async fn start(&self, ctx: &RunContext) -> Result<(), DynError> {
|
||
|
|
tracing::info!(wait_blocks = self.wait_blocks, "waiting for blocks before starting");
|
||
|
|
|
||
|
|
// Subscribe to block feed
|
||
|
|
let mut receiver = ctx.block_feed().subscribe();
|
||
|
|
let mut count = 0;
|
||
|
|
|
||
|
|
// Wait for N blocks
|
||
|
|
while count < self.wait_blocks {
|
||
|
|
match receiver.recv().await {
|
||
|
|
Ok(_) => count += 1,
|
||
|
|
Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => continue,
|
||
|
|
Err(tokio::sync::broadcast::error::RecvError::Closed) => {
|
||
|
|
return Err("block feed closed before reaching target".into());
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
tracing::info!("warmup complete, starting actual workload");
|
||
|
|
|
||
|
|
// Now do the actual work
|
||
|
|
// ...
|
||
|
|
|
||
|
|
Ok(())
|
||
|
|
}
|
||
|
|
}</code></pre>
|
||
|
|
<p><strong>Example: Rate limiting based on block production</strong></p>
|
||
|
|
<pre><code class="language-rust ignore">use testing_framework_core::scenario::{DynError, RunContext};
|
||
|
|
|
||
|
|
async fn generate_request() -> Option<()> {
|
||
|
|
None
|
||
|
|
}
|
||
|
|
|
||
|
|
async fn start(ctx: &RunContext) -> Result<(), DynError> {
|
||
|
|
let clients = ctx.node_clients().validator_clients();
|
||
|
|
let mut receiver = ctx.block_feed().subscribe();
|
||
|
|
let mut pending_requests: Vec<()> = Vec::new();
|
||
|
|
|
||
|
|
loop {
|
||
|
|
tokio::select! {
|
||
|
|
// Issue a batch on each new block.
|
||
|
|
Ok(_record) = receiver.recv() => {
|
||
|
|
if !pending_requests.is_empty() {
|
||
|
|
tracing::debug!(count = pending_requests.len(), "issuing requests on new block");
|
||
|
|
for _req in pending_requests.drain(..) {
|
||
|
|
let _info = clients[0].consensus_info().await?;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Generate work continuously.
|
||
|
|
Some(req) = generate_request() => {
|
||
|
|
pending_requests.push(req);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}</code></pre>
|
||
|
|
<h3 id="blockfeed-vs-direct-polling"><a class="header" href="#blockfeed-vs-direct-polling">BlockFeed vs Direct Polling</a></h3>
|
||
|
|
<p><strong>Use BlockFeed when:</strong></p>
|
||
|
|
<ul>
|
||
|
|
<li>You need to react to blocks as they're produced</li>
|
||
|
|
<li>Multiple components need to observe the same blocks</li>
|
||
|
|
<li>You want automatic retry/reconnect logic</li>
|
||
|
|
<li>You're tracking statistics across many blocks</li>
|
||
|
|
</ul>
|
||
|
|
<p><strong>Use direct polling when:</strong></p>
|
||
|
|
<ul>
|
||
|
|
<li>You need to query specific historical blocks</li>
|
||
|
|
<li>You're checking final state after workloads complete</li>
|
||
|
|
<li>You need transaction receipts or other indexed data</li>
|
||
|
|
<li>You're implementing a one-time health check</li>
|
||
|
|
</ul>
|
||
|
|
<p>Example direct polling in expectations:</p>
|
||
|
|
<pre><code class="language-rust ignore">use testing_framework_core::scenario::{DynError, RunContext};
|
||
|
|
|
||
|
|
async fn evaluate(ctx: &RunContext) -> Result<(), DynError> {
|
||
|
|
let client = &ctx.node_clients().validator_clients()[0];
|
||
|
|
|
||
|
|
// Poll current height once
|
||
|
|
let info = client.consensus_info().await?;
|
||
|
|
tracing::info!(height = info.height, "final block height");
|
||
|
|
|
||
|
|
// This is simpler than BlockFeed for one-time checks
|
||
|
|
Ok(())
|
||
|
|
}</code></pre>
|
||
|
|
<h3 id="block-statistics"><a class="header" href="#block-statistics">Block Statistics</a></h3>
|
||
|
|
<p>Access aggregated statistics without subscribing to the feed:</p>
|
||
|
|
<pre><code class="language-rust ignore">use testing_framework_core::scenario::{DynError, RunContext};
|
||
|
|
|
||
|
|
async fn evaluate(ctx: &RunContext, expected_min: u64) -> Result<(), DynError> {
|
||
|
|
let stats = ctx.block_feed().stats();
|
||
|
|
let total_txs = stats.total_transactions();
|
||
|
|
|
||
|
|
tracing::info!(total_txs, "transactions observed across all blocks");
|
||
|
|
|
||
|
|
if total_txs < expected_min {
|
||
|
|
return Err(format!(
|
||
|
|
"expected at least {} transactions, observed {}",
|
||
|
|
expected_min, total_txs
|
||
|
|
).into());
|
||
|
|
}
|
||
|
|
|
||
|
|
Ok(())
|
||
|
|
}</code></pre>
|
||
|
|
<h3 id="important-notes"><a class="header" href="#important-notes">Important Notes</a></h3>
|
||
|
|
<p><strong>Subscription timing:</strong></p>
|
||
|
|
<ul>
|
||
|
|
<li>Subscribe in <code>start_capture()</code> for expectations</li>
|
||
|
|
<li>Subscribe in <code>start()</code> for workloads</li>
|
||
|
|
<li>Late subscribers miss historical blocks (no replay)</li>
|
||
|
|
</ul>
|
||
|
|
<p><strong>Lagged receivers:</strong></p>
|
||
|
|
<ul>
|
||
|
|
<li>If your subscriber is too slow, it may lag behind</li>
|
||
|
|
<li>Handle <code>RecvError::Lagged(skipped)</code> gracefully</li>
|
||
|
|
<li>Consider increasing processing speed or reducing block rate</li>
|
||
|
|
</ul>
|
||
|
|
<p><strong>Feed lifetime:</strong></p>
|
||
|
|
<ul>
|
||
|
|
<li>BlockFeed runs for the entire scenario duration</li>
|
||
|
|
<li>Automatically cleaned up when the run completes</li>
|
||
|
|
<li>Closed channels signal graceful shutdown</li>
|
||
|
|
</ul>
|
||
|
|
<p><strong>Performance:</strong></p>
|
||
|
|
<ul>
|
||
|
|
<li>BlockFeed polls nodes every 1 second</li>
|
||
|
|
<li>Broadcasts to all subscribers with minimal overhead</li>
|
||
|
|
<li>Suitable for scenarios with hundreds of blocks</li>
|
||
|
|
</ul>
|
||
|
|
<h3 id="real-world-examples"><a class="header" href="#real-world-examples">Real-World Examples</a></h3>
|
||
|
|
<p>The framework's built-in expectations use BlockFeed extensively:</p>
|
||
|
|
<ul>
|
||
|
|
<li><strong><code>ConsensusLiveness</code></strong>: Doesn't directly subscribe but uses block feed stats to verify progress</li>
|
||
|
|
<li><strong><code>DataAvailabilityExpectation</code></strong>: Subscribes to inspect DA blobs in each block and track inscription/dispersal</li>
|
||
|
|
<li><strong><code>TransactionInclusion</code></strong>: Subscribes to find specific transactions in blocks</li>
|
||
|
|
</ul>
|
||
|
|
<p>See <a href="examples.html">Examples</a> and <a href="workloads.html">Workloads & Expectations</a> for more patterns.</p>
|
||
|
|
<hr />
|
||
|
|
<h2 id="current-chaos-capabilities-and-limitations"><a class="header" href="#current-chaos-capabilities-and-limitations">Current Chaos Capabilities and Limitations</a></h2>
|
||
|
|
<p>The framework currently supports <strong>process-level chaos</strong> (node restarts) for
|
||
|
|
resilience testing:</p>
|
||
|
|
<p><strong>Supported:</strong></p>
|
||
|
|
<ul>
|
||
|
|
<li>Restart validators (<code>restart_validator</code>)</li>
|
||
|
|
<li>Restart executors (<code>restart_executor</code>)</li>
|
||
|
|
<li>Random restart workload via <code>.chaos().restart()</code></li>
|
||
|
|
</ul>
|
||
|
|
<p><strong>Not Yet Supported:</strong></p>
|
||
|
|
<ul>
|
||
|
|
<li>Network partitions (blocking peers, packet loss)</li>
|
||
|
|
<li>Resource constraints (CPU throttling, memory limits)</li>
|
||
|
|
<li>Byzantine behavior injection (invalid blocks, bad signatures)</li>
|
||
|
|
<li>Selective peer blocking/unblocking</li>
|
||
|
|
</ul>
|
||
|
|
<p>For network partition testing, see <a href="examples-advanced.html#extension-ideas">Extension Ideas</a>
|
||
|
|
which describes the proposed <code>block_peer</code>/<code>unblock_peer</code> API (not yet implemented).</p>
|
||
|
|
<h2 id="accessing-node-control-in-workloadsexpectations"><a class="header" href="#accessing-node-control-in-workloadsexpectations">Accessing node control in workloads/expectations</a></h2>
|
||
|
|
<p>Check for control support and use it conditionally:</p>
|
||
|
|
<pre><code class="language-rust ignore">use async_trait::async_trait;
|
||
|
|
use testing_framework_core::scenario::{DynError, RunContext, Workload};
|
||
|
|
|
||
|
|
struct RestartWorkload;
|
||
|
|
|
||
|
|
#[async_trait]
|
||
|
|
impl Workload for RestartWorkload {
|
||
|
|
fn name(&self) -> &str {
|
||
|
|
"restart_workload"
|
||
|
|
}
|
||
|
|
|
||
|
|
async fn start(&self, ctx: &RunContext) -> Result<(), DynError> {
|
||
|
|
if let Some(control) = ctx.node_control() {
|
||
|
|
// Restart the first validator (index 0) if supported.
|
||
|
|
control.restart_validator(0).await?;
|
||
|
|
}
|
||
|
|
Ok(())
|
||
|
|
}
|
||
|
|
}</code></pre>
|
||
|
|
<p>When chaos workloads need control, require <code>enable_node_control()</code> in the
|
||
|
|
scenario builder and deploy with a runner that supports it.</p>
|
||
|
|
<h2 id="current-api-surface"><a class="header" href="#current-api-surface">Current API surface</a></h2>
|
||
|
|
<p>The <code>NodeControlHandle</code> trait currently provides:</p>
|
||
|
|
<pre><code class="language-rust ignore">use async_trait::async_trait;
|
||
|
|
use testing_framework_core::scenario::DynError;
|
||
|
|
|
||
|
|
#[async_trait]
|
||
|
|
pub trait NodeControlHandle: Send + Sync {
|
||
|
|
async fn restart_validator(&self, index: usize) -> Result<(), DynError>;
|
||
|
|
async fn restart_executor(&self, index: usize) -> Result<(), DynError>;
|
||
|
|
}</code></pre>
|
||
|
|
<p>Future extensions may include peer blocking/unblocking or other control
|
||
|
|
operations. For now, focus on restart-based chaos patterns as shown in the
|
||
|
|
chaos workload examples.</p>
|
||
|
|
<h2 id="considerations"><a class="header" href="#considerations">Considerations</a></h2>
|
||
|
|
<ul>
|
||
|
|
<li>Always guard control usage: not all runners expose <code>NodeControlHandle</code>.</li>
|
||
|
|
<li>Treat control as best-effort: failures should surface as test failures, but
|
||
|
|
workloads should degrade gracefully when control is absent.</li>
|
||
|
|
<li>Combine control actions with expectations (e.g., restart then assert height
|
||
|
|
convergence) to keep scenarios meaningful.</li>
|
||
|
|
</ul>
|
||
|
|
|
||
|
|
</main>
|
||
|
|
|
||
|
|
<nav class="nav-wrapper" aria-label="Page navigation">
|
||
|
|
<!-- Mobile navigation buttons -->
|
||
|
|
<a rel="prev" href="runners.html" class="mobile-nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
|
||
|
|
<i class="fa fa-angle-left"></i>
|
||
|
|
</a>
|
||
|
|
|
||
|
|
<a rel="next prefetch" href="chaos.html" class="mobile-nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
|
||
|
|
<i class="fa fa-angle-right"></i>
|
||
|
|
</a>
|
||
|
|
|
||
|
|
<div style="clear: both"></div>
|
||
|
|
</nav>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<nav class="nav-wide-wrapper" aria-label="Page navigation">
|
||
|
|
<a rel="prev" href="runners.html" class="nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
|
||
|
|
<i class="fa fa-angle-left"></i>
|
||
|
|
</a>
|
||
|
|
|
||
|
|
<a rel="next prefetch" href="chaos.html" class="nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
|
||
|
|
<i class="fa fa-angle-right"></i>
|
||
|
|
</a>
|
||
|
|
</nav>
|
||
|
|
|
||
|
|
</div>
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
<script>
|
||
|
|
window.playground_copyable = true;
|
||
|
|
</script>
|
||
|
|
|
||
|
|
|
||
|
|
<script src="elasticlunr.min.js"></script>
|
||
|
|
<script src="mark.min.js"></script>
|
||
|
|
<script src="searcher.js"></script>
|
||
|
|
|
||
|
|
<script src="clipboard.min.js"></script>
|
||
|
|
<script src="highlight.js"></script>
|
||
|
|
<script src="book.js"></script>
|
||
|
|
|
||
|
|
<!-- Custom JS scripts -->
|
||
|
|
<script src="theme/mermaid-init.js"></script>
|
||
|
|
|
||
|
|
|
||
|
|
</div>
|
||
|
|
</body>
|
||
|
|
</html>
|