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