iota_graphql_rpc/test_infra/
cluster.rs1use std::{net::SocketAddr, path::PathBuf, sync::Arc, time::Duration};
6
7use iota_graphql_rpc_client::simple_client::SimpleClient;
8use iota_indexer::{
9 config::RetentionConfig,
10 errors::IndexerError,
11 store::PgIndexerStore,
12 test_utils::{IndexerTypeConfig, force_delete_database, start_test_indexer_impl},
13};
14use iota_node_storage::GrpcStateReader;
15use iota_sdk_types::Transaction;
16use iota_swarm_config::genesis_config::{AccountConfig, DEFAULT_GAS_AMOUNT};
17use iota_types::transaction::TransactionEnvelope;
18use test_cluster::{TestCluster, TestClusterBuilder};
19use tokio::{join, task::JoinHandle};
20use tokio_util::sync::CancellationToken;
21use tracing::info;
22
23use crate::{
24 config::{ConnectionConfig, ServerConfig, ServiceConfig, Version},
25 server::graphiql_server::start_graphiql_server,
26};
27
28const VALIDATOR_COUNT: usize = 7;
29const EPOCH_DURATION_MS: u64 = 15000;
30
31const ACCOUNT_NUM: usize = 20;
32const GAS_OBJECT_COUNT: usize = 3;
33
34pub const DEFAULT_INTERNAL_DATA_SOURCE_PORT: u16 = 3000;
35
36pub struct ExecutorCluster {
37 pub indexer_store: PgIndexerStore,
38 pub indexer_join_handle: JoinHandle<Result<(), IndexerError>>,
39 pub graphql_server_join_handle: JoinHandle<()>,
40 pub graphql_client: SimpleClient,
41 pub graphql_connection_config: ConnectionConfig,
42 pub cancellation_token: CancellationToken,
43}
44
45pub struct Cluster {
46 pub validator_fullnode_handle: TestCluster,
47 pub indexer_store: PgIndexerStore,
48 pub indexer_join_handle: JoinHandle<Result<(), IndexerError>>,
49 pub graphql_server_join_handle: JoinHandle<()>,
50 pub graphql_client: SimpleClient,
51 pub cancellation_token: CancellationToken,
52}
53
54pub async fn start_cluster(
56 graphql_connection_config: ConnectionConfig,
57 internal_data_source_rpc_port: Option<u16>,
58 service_config: ServiceConfig,
59) -> Cluster {
60 let data_ingestion_path = iota_common::tempdir().keep();
61 let db_url = graphql_connection_config.db_url();
62 let cancellation_token = CancellationToken::new();
63 let test_cluster =
65 start_validator_with_fullnode(internal_data_source_rpc_port, data_ingestion_path.clone())
66 .await;
67
68 let grpc_url = test_cluster.grpc_url();
69 let (pg_store, pg_handle) = start_test_indexer_impl(
71 db_url,
72 true,
74 None,
75 grpc_url.clone(),
76 IndexerTypeConfig::writer_mode(None),
77 Some(data_ingestion_path),
78 cancellation_token.clone(),
79 )
80 .await;
81
82 let graphql_server_handle = start_graphql_server_with_fn_rpc(
84 graphql_connection_config.clone(),
85 Some(grpc_url),
86 Some(cancellation_token.clone()),
87 Some(service_config),
88 )
89 .await;
90
91 let server_url = format!(
92 "http://{}:{}/",
93 graphql_connection_config.host, graphql_connection_config.port
94 );
95
96 let client = SimpleClient::new(server_url);
98 wait_for_graphql_server(&client).await;
99
100 Cluster {
101 validator_fullnode_handle: test_cluster,
102 indexer_store: pg_store,
103 indexer_join_handle: pg_handle,
104 graphql_server_join_handle: graphql_server_handle,
105 graphql_client: client,
106 cancellation_token,
107 }
108}
109
110pub async fn serve_executor(
117 graphql_connection_config: ConnectionConfig,
118 internal_data_source_rpc_port: u16,
119 _executor: Arc<dyn GrpcStateReader + Send + Sync>,
120 epochs_to_keep: Option<u64>,
121 data_ingestion_path: PathBuf,
122) -> ExecutorCluster {
123 let db_url = graphql_connection_config.db_url();
124 let cancellation_token = CancellationToken::new();
127
128 let executor_server_url: SocketAddr = format!("127.0.0.1:{internal_data_source_rpc_port}")
131 .parse()
132 .unwrap();
133
134 let (pg_store, pg_handle) = start_test_indexer_impl(
137 db_url,
138 true,
139 None,
140 format!("http://{executor_server_url}"),
141 IndexerTypeConfig::writer_mode_with_retention(
142 epochs_to_keep.map(|epochs| RetentionConfig::new(epochs, Default::default())),
143 ),
144 Some(data_ingestion_path),
145 cancellation_token.clone(),
146 )
147 .await;
148
149 let graphql_server_handle = start_graphql_server_with_fn_rpc(
151 graphql_connection_config.clone(),
152 Some(format!("http://{executor_server_url}")),
154 Some(cancellation_token.clone()),
155 None,
156 )
157 .await;
158
159 let server_url = format!(
160 "http://{}:{}/",
161 graphql_connection_config.host, graphql_connection_config.port
162 );
163
164 let client = SimpleClient::new(server_url);
166 wait_for_graphql_server(&client).await;
167
168 ExecutorCluster {
169 indexer_store: pg_store,
170 indexer_join_handle: pg_handle,
171 graphql_server_join_handle: graphql_server_handle,
172 graphql_client: client,
173 graphql_connection_config,
174 cancellation_token,
175 }
176}
177
178pub async fn wait_for_graphql_checkpoint_pruned(
181 client: &SimpleClient,
182 checkpoint: u64,
183 base_timeout: Duration,
184) {
185 info!(
186 "Waiting for checkpoint to be pruned {}, base time out is {}",
187 checkpoint,
188 base_timeout.as_secs()
189 );
190 let query = format!(
191 r#"
192 {{
193 checkpoint(id: {{ sequenceNumber: {checkpoint} }}) {{
194 sequenceNumber
195 }}
196 }}"#
197 );
198
199 let timeout = base_timeout.mul_f64(checkpoint.max(1) as f64);
200
201 tokio::time::timeout(timeout, async {
202 loop {
203 let resp = match client
204 .execute_to_graphql(query.to_string(), false, vec![], vec![])
205 .await
206 {
207 Ok(resp) => resp.response_body_json(),
208 Err(e) => {
209 info!("GraphQL request failed, retrying: {e}");
210 tokio::time::sleep(Duration::from_secs(1)).await;
211 continue;
212 }
213 };
214
215 let current_checkpoint = &resp["data"]["checkpoint"];
216 if current_checkpoint.is_null() {
217 break;
218 } else {
219 tokio::time::sleep(Duration::from_secs(1)).await;
220 }
221 }
222 })
223 .await
224 .expect("timeout waiting for checkpoint to be pruned");
225}
226
227pub async fn start_graphql_server_with_fn_rpc(
228 graphql_connection_config: ConnectionConfig,
229 fn_rpc_url: Option<String>,
230 cancellation_token: Option<CancellationToken>,
231 service_config: Option<ServiceConfig>,
232) -> JoinHandle<()> {
233 let cancellation_token = cancellation_token.unwrap_or_default();
234 let mut server_config = ServerConfig {
235 connection: graphql_connection_config,
236 service: service_config.unwrap_or_else(ServiceConfig::test_defaults),
237 ..ServerConfig::default()
238 };
239 if let Some(fn_rpc_url) = fn_rpc_url {
240 server_config.tx_exec_full_node.node_rpc_url = Some(fn_rpc_url);
241 };
242
243 tokio::spawn(async move {
245 start_graphiql_server(&server_config, &Version::for_testing(), cancellation_token)
246 .await
247 .unwrap();
248 })
249}
250
251async fn start_validator_with_fullnode(
252 internal_data_source_rpc_port: Option<u16>,
253 data_ingestion_dir: PathBuf,
254) -> TestCluster {
255 let mut test_cluster_builder = TestClusterBuilder::new()
256 .with_num_validators(VALIDATOR_COUNT)
257 .with_epoch_duration_ms(EPOCH_DURATION_MS)
258 .with_data_ingestion_dir(data_ingestion_dir)
259 .with_accounts(vec![
260 AccountConfig {
261 address: None,
262 gas_amounts: vec![DEFAULT_GAS_AMOUNT; GAS_OBJECT_COUNT],
263 };
264 ACCOUNT_NUM
265 ]);
266
267 if let Some(internal_data_source_rpc_port) = internal_data_source_rpc_port {
268 test_cluster_builder =
269 test_cluster_builder.with_fullnode_rpc_port(internal_data_source_rpc_port);
270 };
271 test_cluster_builder.build().await
272}
273
274async fn wait_for_graphql_server(client: &SimpleClient) {
276 tokio::time::timeout(Duration::from_secs(10), async {
277 while client.ping().await.is_err() {
278 tokio::time::sleep(Duration::from_millis(500)).await;
279 }
280 })
281 .await
282 .expect("timeout waiting for graphql server to start");
283}
284
285async fn wait_for_graphql_checkpoint_catchup(
288 client: &SimpleClient,
289 checkpoint: u64,
290 base_timeout: Duration,
291) {
292 info!(
293 "Waiting for graphql to catchup to checkpoint {}, base time out is {}",
294 checkpoint,
295 base_timeout.as_secs()
296 );
297 let query = r#"
298 {
299 availableRange {
300 last {
301 sequenceNumber
302 }
303 }
304 }"#;
305
306 let timeout = base_timeout.mul_f64(checkpoint.max(1) as f64);
307
308 tokio::time::timeout(timeout, async {
309 loop {
310 let resp = match client
311 .execute_to_graphql(query.to_string(), false, vec![], vec![])
312 .await
313 {
314 Ok(resp) => resp.response_body_json(),
315 Err(e) => {
316 info!("GraphQL request failed, retrying: {e}");
317 tokio::time::sleep(Duration::from_secs(1)).await;
318 continue;
319 }
320 };
321
322 let current_checkpoint = resp["data"]["availableRange"]["last"].get("sequenceNumber");
323 info!("Current checkpoint: {:?}", current_checkpoint);
324 let Some(current_checkpoint) = current_checkpoint else {
326 tokio::time::sleep(Duration::from_secs(1)).await;
327 continue;
328 };
329
330 let current_checkpoint = current_checkpoint.as_u64().unwrap();
332 if current_checkpoint < checkpoint {
333 tokio::time::sleep(Duration::from_secs(1)).await;
334 } else {
335 break;
336 }
337 }
338 })
339 .await
340 .expect("timeout waiting for graphql to catchup to checkpoint");
341}
342
343impl Cluster {
344 pub async fn wait_for_checkpoint_catchup(&self, checkpoint: u64, base_timeout: Duration) {
348 wait_for_graphql_checkpoint_catchup(&self.graphql_client, checkpoint, base_timeout).await
349 }
350
351 pub async fn wait_for_checkpoint_pruned(&self, checkpoint: u64, base_timeout: Duration) {
353 wait_for_graphql_checkpoint_pruned(&self.graphql_client, checkpoint, base_timeout).await
354 }
355
356 pub async fn build_transfer_iota_for_test(&self) -> Transaction {
358 let addresses = self.validator_fullnode_handle.wallet.get_addresses();
359
360 let recipient = addresses[1];
361 self.validator_fullnode_handle
362 .test_transaction_builder()
363 .await
364 .transfer_iota(Some(1_000), recipient)
365 .build()
366 }
367
368 pub fn sign_transaction(&self, transaction: &Transaction) -> TransactionEnvelope {
370 self.validator_fullnode_handle
371 .wallet
372 .sign_transaction(transaction)
373 }
374}
375
376impl ExecutorCluster {
377 pub async fn wait_for_checkpoint_catchup(&self, checkpoint: u64, base_timeout: Duration) {
381 wait_for_graphql_checkpoint_catchup(&self.graphql_client, checkpoint, base_timeout).await
382 }
383
384 pub async fn wait_for_checkpoint_pruned(&self, checkpoint: u64, base_timeout: Duration) {
386 wait_for_graphql_checkpoint_pruned(&self.graphql_client, checkpoint, base_timeout).await
387 }
388
389 pub async fn cleanup_resources(self) {
393 self.cancellation_token.cancel();
394 let _ = join!(self.graphql_server_join_handle, self.indexer_join_handle);
395 let db_url = self.graphql_connection_config.db_url();
396 force_delete_database(db_url).await;
397 }
398}