Skip to main content

sui_core/
transaction_orchestrator.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use std::net::SocketAddr;
5use std::path::Path;
6use std::sync::Arc;
7use std::time::Duration;
8
9use futures::FutureExt;
10use futures::stream::{FuturesUnordered, StreamExt};
11use mysten_common::{backoff, in_integration_test};
12use mysten_metrics::{TX_TYPE_SHARED_OBJ_TX, TX_TYPE_SINGLE_WRITER_TX, spawn_monitored_task};
13use mysten_metrics::{add_server_timing, spawn_logged_monitored_task};
14use prometheus::core::{AtomicI64, AtomicU64, GenericCounter, GenericGauge};
15use prometheus::{
16    HistogramVec, IntCounter, IntCounterVec, IntGauge, Registry,
17    register_histogram_vec_with_registry, register_int_counter_vec_with_registry,
18    register_int_counter_with_registry, register_int_gauge_vec_with_registry,
19    register_int_gauge_with_registry,
20};
21use rand::Rng;
22use sui_config::NodeConfig;
23use sui_storage::write_path_pending_tx_log::WritePathPendingTransactionLog;
24use sui_types::base_types::TransactionDigest;
25use sui_types::effects::TransactionEffectsAPI;
26use sui_types::error::{ErrorCategory, SuiError, SuiErrorKind, SuiResult};
27use sui_types::messages_grpc::{SubmitTxRequest, TxType};
28use sui_types::sui_system_state::SuiSystemState;
29use sui_types::transaction::{Transaction, TransactionData, VerifiedTransaction};
30use sui_types::transaction_driver_types::{
31    EffectsFinalityInfo, ExecuteTransactionRequestType, ExecuteTransactionRequestV3,
32    ExecuteTransactionResponseV3, FinalizedEffects, IsTransactionExecutedLocally,
33    TransactionSubmissionError,
34};
35use sui_types::transaction_executor::{SimulateTransactionResult, TransactionChecks};
36use tokio::sync::broadcast::Receiver;
37use tokio::time::{Instant, sleep, timeout};
38use tracing::{Instrument, debug, error_span, info, instrument, warn};
39
40use crate::authority::AuthorityState;
41use crate::authority_aggregator::AuthorityAggregator;
42use crate::authority_client::{AuthorityAPI, NetworkAuthorityClient};
43use crate::transaction_driver::{OnsiteReconfigObserver, ReconfigObserver};
44use crate::transaction_driver::{
45    QuorumTransactionResponse, SubmitTransactionOptions, TransactionDriver, TransactionDriverError,
46    TransactionDriverMetrics,
47};
48
49// How long to wait for local execution (including parents) before a timeout
50// is returned to client.
51const LOCAL_EXECUTION_TIMEOUT: Duration = Duration::from_secs(10);
52
53// Timeout for waiting for finality for each transaction.
54const WAIT_FOR_FINALITY_TIMEOUT: Duration = Duration::from_secs(90);
55
56pub type QuorumTransactionEffectsResult = Result<
57    (Transaction, QuorumTransactionResponse),
58    (TransactionDigest, TransactionSubmissionError),
59>;
60
61/// Transaction Orchestrator is a Node component that utilizes Transaction Driver to
62/// submit transactions to validators for finality. It adds inflight deduplication,
63/// waiting for local execution, recovery, early validation, and epoch change handling
64/// on top of Transaction Driver.
65/// This is used by node RPC service to support transaction submission and finality waiting.
66pub struct TransactionOrchestrator<A: Clone> {
67    inner: Arc<Inner<A>>,
68}
69
70impl TransactionOrchestrator<NetworkAuthorityClient> {
71    pub fn new_with_auth_aggregator(
72        validators: Arc<AuthorityAggregator<NetworkAuthorityClient>>,
73        validator_state: Arc<AuthorityState>,
74        reconfig_channel: Receiver<SuiSystemState>,
75        parent_path: &Path,
76        prometheus_registry: &Registry,
77        node_config: &NodeConfig,
78    ) -> Self {
79        let observer = OnsiteReconfigObserver::new(
80            reconfig_channel,
81            validator_state.get_object_cache_reader().clone(),
82            validator_state.clone_committee_store(),
83            validators.safe_client_metrics_base.clone(),
84        );
85        TransactionOrchestrator::new(
86            validators,
87            validator_state,
88            parent_path,
89            prometheus_registry,
90            observer,
91            node_config,
92        )
93    }
94}
95
96impl<A> TransactionOrchestrator<A>
97where
98    A: AuthorityAPI + Send + Sync + 'static + Clone,
99    OnsiteReconfigObserver: ReconfigObserver<A>,
100{
101    pub fn new(
102        validators: Arc<AuthorityAggregator<A>>,
103        validator_state: Arc<AuthorityState>,
104        parent_path: &Path,
105        prometheus_registry: &Registry,
106        reconfig_observer: OnsiteReconfigObserver,
107        node_config: &NodeConfig,
108    ) -> Self {
109        let metrics = Arc::new(TransactionOrchestratorMetrics::new(prometheus_registry));
110        let td_metrics = Arc::new(TransactionDriverMetrics::new(prometheus_registry));
111        let client_metrics = Arc::new(
112            crate::validator_client_monitor::ValidatorClientMetrics::new(prometheus_registry),
113        );
114        let reconfig_observer = Arc::new(reconfig_observer);
115
116        let transaction_driver = TransactionDriver::new(
117            validators.clone(),
118            reconfig_observer.clone(),
119            td_metrics,
120            Some(node_config),
121            client_metrics,
122        );
123
124        let pending_tx_log = Arc::new(WritePathPendingTransactionLog::new(
125            parent_path.join("fullnode_pending_transactions"),
126        ));
127        Inner::start_task_to_recover_txes_in_log(
128            pending_tx_log.clone(),
129            transaction_driver.clone(),
130        );
131
132        let td_allowed_submission_list = node_config
133            .transaction_driver_config
134            .as_ref()
135            .map(|config| config.allowed_submission_validators.clone())
136            .unwrap_or_default();
137
138        let td_blocked_submission_list = node_config
139            .transaction_driver_config
140            .as_ref()
141            .map(|config| config.blocked_submission_validators.clone())
142            .unwrap_or_default();
143
144        if !td_allowed_submission_list.is_empty() && !td_blocked_submission_list.is_empty() {
145            panic!(
146                "Both allowed and blocked submission lists are set, this is not allowed, {:?} {:?}",
147                td_allowed_submission_list, td_blocked_submission_list
148            );
149        }
150
151        let enable_early_validation = node_config
152            .transaction_driver_config
153            .as_ref()
154            .map(|config| config.enable_early_validation)
155            .unwrap_or(true);
156
157        let inner = Arc::new(Inner {
158            validator_state,
159            pending_tx_log,
160            metrics,
161            transaction_driver,
162            td_allowed_submission_list,
163            td_blocked_submission_list,
164            enable_early_validation,
165        });
166        Self { inner }
167    }
168}
169
170impl<A> TransactionOrchestrator<A>
171where
172    A: AuthorityAPI + Send + Sync + 'static + Clone,
173{
174    #[instrument(name = "tx_orchestrator_execute_transaction", level = "debug", skip_all,
175    fields(
176        tx_digest = ?request.transaction.digest(),
177        tx_type = ?request_type,
178    ))]
179    pub async fn execute_transaction_block(
180        &self,
181        request: ExecuteTransactionRequestV3,
182        request_type: ExecuteTransactionRequestType,
183        client_addr: Option<SocketAddr>,
184    ) -> Result<
185        (ExecuteTransactionResponseV3, IsTransactionExecutedLocally),
186        TransactionSubmissionError,
187    > {
188        let timer = Instant::now();
189        let tx_type = if request.transaction.is_consensus_tx() {
190            TxType::SharedObject
191        } else {
192            TxType::SingleWriter
193        };
194        let tx_digest = *request.transaction.digest();
195
196        let inner = self.inner.clone();
197        let (response, mut executed_locally) = spawn_monitored_task!(
198            Inner::<A>::execute_transaction_with_retry(inner, request, client_addr)
199        )
200        .await
201        .map_err(|e| TransactionSubmissionError::TransactionFailed {
202            category: ErrorCategory::Internal,
203            details: e.to_string(),
204        })??;
205
206        if matches!(
207            request_type,
208            ExecuteTransactionRequestType::WaitForLocalExecution
209        ) {
210            // Always wait for the checkpoint containing this tx to be finalized,
211            // even when effects are already available locally. With batched index
212            // writes, index data is only committed at checkpoint boundaries, so
213            // callers relying on up-to-date index data after WaitForLocalExecution
214            // need the checkpoint to be processed.
215            executed_locally = Inner::<A>::wait_for_finalized_tx_executed_locally_with_timeout(
216                &self.inner.validator_state,
217                tx_digest,
218                tx_type,
219                &self.inner.metrics,
220            )
221            .await
222            .is_ok();
223            add_server_timing("local_execution done");
224        }
225
226        let QuorumTransactionResponse {
227            effects,
228            events,
229            input_objects,
230            output_objects,
231            auxiliary_data,
232        } = response;
233
234        let response = ExecuteTransactionResponseV3 {
235            effects,
236            events,
237            input_objects,
238            output_objects,
239            auxiliary_data,
240        };
241
242        let request_latency = timer.elapsed().as_secs_f64();
243        if request_latency > 10.0 {
244            warn!(
245                ?tx_digest,
246                "Request latency {} is too high", request_latency,
247            );
248        }
249        self.inner
250            .metrics
251            .request_latency
252            .with_label_values(&[
253                tx_type.as_str(),
254                "execute_transaction_block",
255                executed_locally.to_string().as_str(),
256            ])
257            .observe(request_latency);
258
259        Ok((response, executed_locally))
260    }
261
262    // Utilize the handle_certificate_v3 validator api to request input/output objects
263    #[instrument(name = "tx_orchestrator_execute_transaction_v3", level = "debug", skip_all,
264                 fields(tx_digest = ?request.transaction.digest()))]
265    pub async fn execute_transaction_v3(
266        &self,
267        request: ExecuteTransactionRequestV3,
268        client_addr: Option<SocketAddr>,
269    ) -> Result<ExecuteTransactionResponseV3, TransactionSubmissionError> {
270        let timer = Instant::now();
271        let tx_type = if request.transaction.is_consensus_tx() {
272            TxType::SharedObject
273        } else {
274            TxType::SingleWriter
275        };
276
277        let inner = self.inner.clone();
278        let (response, _) = spawn_monitored_task!(Inner::<A>::execute_transaction_with_retry(
279            inner,
280            request,
281            client_addr
282        ))
283        .await
284        .map_err(|e| TransactionSubmissionError::TransactionFailed {
285            category: ErrorCategory::Internal,
286            details: e.to_string(),
287        })??;
288
289        self.inner
290            .metrics
291            .request_latency
292            .with_label_values(&[tx_type.as_str(), "execute_transaction_v3", "false"])
293            .observe(timer.elapsed().as_secs_f64());
294
295        let QuorumTransactionResponse {
296            effects,
297            events,
298            input_objects,
299            output_objects,
300            auxiliary_data,
301        } = response;
302
303        Ok(ExecuteTransactionResponseV3 {
304            effects,
305            events,
306            input_objects,
307            output_objects,
308            auxiliary_data,
309        })
310    }
311
312    pub fn authority_state(&self) -> &Arc<AuthorityState> {
313        &self.inner.validator_state
314    }
315
316    pub fn transaction_driver(&self) -> &Arc<TransactionDriver<A>> {
317        &self.inner.transaction_driver
318    }
319
320    pub fn clone_authority_aggregator(&self) -> Arc<AuthorityAggregator<A>> {
321        self.inner
322            .transaction_driver
323            .authority_aggregator()
324            .load_full()
325    }
326
327    pub fn load_all_pending_transactions_in_test(&self) -> SuiResult<Vec<VerifiedTransaction>> {
328        self.inner.pending_tx_log.load_all_pending_transactions()
329    }
330
331    pub fn empty_pending_tx_log_in_test(&self) -> bool {
332        self.inner.pending_tx_log.is_empty()
333    }
334}
335
336struct Inner<A: Clone> {
337    validator_state: Arc<AuthorityState>,
338    pending_tx_log: Arc<WritePathPendingTransactionLog>,
339    metrics: Arc<TransactionOrchestratorMetrics>,
340    transaction_driver: Arc<TransactionDriver<A>>,
341    td_allowed_submission_list: Vec<String>,
342    td_blocked_submission_list: Vec<String>,
343    enable_early_validation: bool,
344}
345
346impl<A> Inner<A>
347where
348    A: AuthorityAPI + Send + Sync + 'static + Clone,
349{
350    async fn execute_transaction_with_retry(
351        inner: Arc<Inner<A>>,
352        request: ExecuteTransactionRequestV3,
353        client_addr: Option<SocketAddr>,
354    ) -> Result<(QuorumTransactionResponse, IsTransactionExecutedLocally), TransactionSubmissionError>
355    {
356        let result = inner
357            .execute_transaction_with_effects_waiting(
358                request.clone(),
359                client_addr,
360                /* enforce_live_input_objects */ false,
361            )
362            .await;
363
364        // If the error is retriable, retry the transaction sufficiently long.
365        if let Err(e) = &result
366            && e.is_retriable()
367        {
368            spawn_monitored_task!(async move {
369                inner.metrics.background_retry_started.inc();
370                let backoff = backoff::ExponentialBackoff::new(
371                    Duration::from_secs(1),
372                    Duration::from_secs(300),
373                );
374                const MAX_RETRIES: usize = 10;
375                for (i, delay) in backoff.enumerate() {
376                    if i == MAX_RETRIES {
377                        break;
378                    }
379                    // Start to enforce live input after 3 retries,
380                    // to avoid excessively retrying transactions with non-existent input objects.
381                    let result = inner
382                        .execute_transaction_with_effects_waiting(
383                            request.clone(),
384                            client_addr,
385                            i > 3,
386                        )
387                        .await;
388                    match result {
389                        Ok(_) => {
390                            inner
391                                .metrics
392                                .background_retry_attempts
393                                .with_label_values(&["success"])
394                                .inc();
395                            debug!(
396                                "Background retry {i} for transaction {} succeeded",
397                                request.transaction.digest()
398                            );
399                            break;
400                        }
401                        Err(e) => {
402                            if !e.is_retriable() {
403                                inner
404                                    .metrics
405                                    .background_retry_attempts
406                                    .with_label_values(&["non-retriable"])
407                                    .inc();
408                                debug!(
409                                    "Background retry {i} for transaction {} has non-retriable error: {e:?}. Terminating...",
410                                    request.transaction.digest()
411                                );
412                                break;
413                            }
414                            inner
415                                .metrics
416                                .background_retry_attempts
417                                .with_label_values(&["retriable"])
418                                .inc();
419                            debug!(
420                                "Background retry {i} for transaction {} has retriable error: {e:?}. Continuing...",
421                                request.transaction.digest()
422                            );
423                        }
424                    };
425                    tracing::debug!("Wait for {:.3}s before next retry", delay.as_secs_f32());
426                    sleep(delay).await;
427                }
428            });
429        }
430
431        result
432    }
433
434    fn build_response_from_local_effects(
435        &self,
436        effects: sui_types::effects::TransactionEffects,
437        include_events: bool,
438        include_input_objects: bool,
439        include_output_objects: bool,
440    ) -> Result<QuorumTransactionResponse, TransactionSubmissionError> {
441        let epoch = effects.executed_epoch();
442        let events = if include_events {
443            if effects.events_digest().is_some() {
444                Some(
445                    self.validator_state
446                        .get_transaction_events(effects.transaction_digest())
447                        .map_err(TransactionSubmissionError::TransactionDriverInternalError)?,
448                )
449            } else {
450                None
451            }
452        } else {
453            None
454        };
455        let input_objects = include_input_objects
456            .then(|| self.validator_state.get_transaction_input_objects(&effects))
457            .transpose()
458            .map_err(TransactionSubmissionError::TransactionDriverInternalError)?;
459        let output_objects = include_output_objects
460            .then(|| {
461                self.validator_state
462                    .get_transaction_output_objects(&effects)
463            })
464            .transpose()
465            .map_err(TransactionSubmissionError::TransactionDriverInternalError)?;
466
467        Ok(QuorumTransactionResponse {
468            effects: FinalizedEffects {
469                effects,
470                finality_info: EffectsFinalityInfo::QuorumExecuted(epoch),
471            },
472            events,
473            input_objects,
474            output_objects,
475            auxiliary_data: None,
476        })
477    }
478
479    /// Shared implementation for executing transactions with parallel local effects waiting
480    async fn execute_transaction_with_effects_waiting(
481        &self,
482        request: ExecuteTransactionRequestV3,
483        client_addr: Option<SocketAddr>,
484        enforce_live_input_objects: bool,
485    ) -> Result<(QuorumTransactionResponse, IsTransactionExecutedLocally), TransactionSubmissionError>
486    {
487        let epoch_store = self.validator_state.load_epoch_store_one_call_per_task();
488        let verified_transaction = epoch_store
489            .verify_transaction_with_current_aliases(request.transaction.clone())
490            .map_err(TransactionSubmissionError::InvalidUserSignature)?
491            .into_tx();
492        let tx_digest = *verified_transaction.digest();
493
494        // Early validation check against local state before submission to catch non-retriable errors
495        // TODO: Consider moving this check to TransactionDriver for per-retry validation
496        if self.enable_early_validation
497            && let Err(e) = self.validator_state.check_transaction_validity(
498                &epoch_store,
499                &verified_transaction,
500                enforce_live_input_objects,
501            )
502        {
503            let error_category = e.categorize();
504            if !error_category.is_submission_retriable() {
505                // Skip early validation rejection if transaction has already been executed (allows retries to return cached results)
506                if !self.validator_state.is_tx_already_executed(&tx_digest) {
507                    self.metrics
508                        .early_validation_rejections
509                        .with_label_values(&[e.to_variant_name()])
510                        .inc();
511                    debug!(
512                        error = ?e,
513                        "Transaction rejected during early validation"
514                    );
515
516                    return Err(TransactionSubmissionError::TransactionFailed {
517                        category: error_category,
518                        details: e.to_string(),
519                    });
520                }
521            }
522        }
523        // Nothing below needs the epoch store; release it so that a reconfiguration during the
524        // finality wait does not keep the previous epoch's store alive.
525        drop(epoch_store);
526
527        // Add transaction to WAL log.
528        let guard =
529            TransactionSubmissionGuard::new(self.pending_tx_log.clone(), &verified_transaction);
530        let is_new_transaction = guard.is_new_transaction();
531
532        let include_events = request.include_events;
533        let include_input_objects = request.include_input_objects;
534        let include_output_objects = request.include_output_objects;
535        let include_auxiliary_data = request.include_auxiliary_data;
536
537        // Check if transaction has already been executed locally and return cached results
538        if let Some(effects) = self
539            .validator_state
540            .get_transaction_cache_reader()
541            .get_executed_effects(&tx_digest)
542        {
543            self.metrics.early_cached_response.inc();
544            debug!(
545                ?tx_digest,
546                "Returning cached results for already-executed transaction"
547            );
548            let response = self.build_response_from_local_effects(
549                effects,
550                include_events,
551                include_input_objects,
552                include_output_objects,
553            )?;
554            return Ok((response, true));
555        }
556
557        let finality_timeout = std::env::var("WAIT_FOR_FINALITY_TIMEOUT_SECS")
558            .ok()
559            .and_then(|v| v.parse().ok())
560            .map(Duration::from_secs)
561            .unwrap_or(WAIT_FOR_FINALITY_TIMEOUT);
562
563        let num_submissions = if !is_new_transaction {
564            // No need to submit when the transaction is already being processed.
565            0
566        } else if in_integration_test() {
567            // Allow duplicated submissions in tests.
568            let r = rand::thread_rng().gen_range(1..=100);
569            let n = if r <= 10 {
570                3
571            } else if r <= 30 {
572                2
573            } else {
574                1
575            };
576            if n > 1 {
577                debug!("Making {n} execution calls");
578            }
579            n
580        } else {
581            1
582        };
583
584        // Wait for one of the execution futures to succeed, or all of them to fail.
585        let mut execution_futures = FuturesUnordered::new();
586        for i in 0..num_submissions {
587            // Generate jitter values outside the async block
588            let should_delay = i > 0 && rand::thread_rng().gen_bool(0.8);
589            let delay_ms = if should_delay {
590                rand::thread_rng().gen_range(100..=500)
591            } else {
592                0
593            };
594
595            let request = request.clone();
596            let verified_transaction = verified_transaction.clone();
597
598            let future = async move {
599                if delay_ms > 0 {
600                    // Add jitters to duplicated submissions.
601                    sleep(Duration::from_millis(delay_ms)).await;
602                }
603                self.execute_transaction_impl(
604                    request,
605                    verified_transaction,
606                    client_addr,
607                    Some(finality_timeout),
608                )
609                .await
610            }
611            .boxed();
612            execution_futures.push(future);
613        }
614
615        // Track the last execution error.
616        let mut last_execution_error: Option<TransactionSubmissionError> = None;
617
618        // Wait for execution result outside of this call to become available.
619        let digests = [tx_digest];
620        let mut local_effects_future = self
621            .validator_state
622            .get_transaction_cache_reader()
623            .notify_read_executed_effects_may_fail(
624                "TransactionOrchestrator::notify_read_execute_transaction_with_effects_waiting",
625                &digests,
626            )
627            .boxed();
628
629        // Wait for execution timeout.
630        let mut timeout_future = tokio::time::sleep(finality_timeout).boxed();
631
632        loop {
633            tokio::select! {
634                // Local effects might be available
635                all_effects_result = &mut local_effects_future => {
636                    let all_effects = all_effects_result
637                        .map_err(TransactionSubmissionError::TransactionDriverInternalError)?;
638                    if all_effects.len() != 1 {
639                        break Err(TransactionSubmissionError::TransactionDriverInternalError(
640                            SuiErrorKind::Unknown(format!("Unexpected number of effects found: {}", all_effects.len())).into()
641                        ));
642                    }
643                    debug!(
644                        "Effects became available while execution was running"
645                    );
646                    self.metrics.concurrent_execution.inc();
647
648                    let effects = all_effects.into_iter().next().unwrap();
649                    let response = self.build_response_from_local_effects(
650                        effects,
651                        include_events,
652                        include_input_objects,
653                        include_output_objects,
654                    )?;
655                    break Ok((response, true));
656                }
657
658                // This branch is disabled if execution_futures is empty.
659                Some(result) = execution_futures.next() => {
660                    match result {
661                        Ok(resp) => {
662                            // First success gets returned.
663                            debug!("Execution succeeded, returning response");
664                            let QuorumTransactionResponse {
665                                effects,
666                                events,
667                                input_objects,
668                                output_objects,
669                                auxiliary_data,
670                            } = resp;
671                            // Filter fields based on request flags.
672                            let resp = QuorumTransactionResponse {
673                                effects,
674                                events: if include_events { events } else { None },
675                                input_objects: if include_input_objects { input_objects } else { None },
676                                output_objects: if include_output_objects { output_objects } else { None },
677                                auxiliary_data: if include_auxiliary_data { auxiliary_data } else { None },
678                            };
679                            break Ok((resp, false));
680                        }
681                        Err(e) => {
682                            debug!(?e, "Execution attempt failed, wait for other attempts");
683                            last_execution_error = Some(e);
684                        }
685                    };
686
687                    // Last error must have been recorded.
688                    if execution_futures.is_empty() {
689                        break Err(last_execution_error.unwrap());
690                    }
691                }
692
693                // A timeout has occurred while waiting for finality
694                _ = &mut timeout_future => {
695                    if let Some(e) = last_execution_error {
696                        debug!("Timeout waiting for transaction finality. Last execution error: {e}");
697                    } else {
698                        debug!("Timeout waiting for transaction finality.");
699                    }
700                    self.metrics.wait_for_finality_timeout.inc();
701
702                    // TODO: Return the last execution error.
703                    break Err(TransactionSubmissionError::TimeoutBeforeFinality);
704                }
705            }
706        }
707    }
708
709    #[instrument(level = "error", skip_all)]
710    async fn execute_transaction_impl(
711        &self,
712        request: ExecuteTransactionRequestV3,
713        verified_transaction: VerifiedTransaction,
714        client_addr: Option<SocketAddr>,
715        finality_timeout: Option<Duration>,
716    ) -> Result<QuorumTransactionResponse, TransactionSubmissionError> {
717        let timer = Instant::now();
718        let tx_digest = *verified_transaction.digest();
719        let tx_type = if verified_transaction.is_consensus_tx() {
720            TxType::SharedObject
721        } else {
722            TxType::SingleWriter
723        };
724
725        let (_in_flight_metrics_guards, good_response_metrics) =
726            self.update_metrics(&request.transaction);
727
728        // TODO: refactor all the gauge and timer metrics with `monitored_scope`
729        let wait_for_finality_gauge = self.metrics.wait_for_finality_in_flight.clone();
730        wait_for_finality_gauge.inc();
731        let _wait_for_finality_gauge = scopeguard::guard(wait_for_finality_gauge, |in_flight| {
732            in_flight.dec();
733        });
734
735        let response = self
736            .submit_with_transaction_driver(
737                &self.transaction_driver,
738                &request,
739                client_addr,
740                &verified_transaction,
741                good_response_metrics,
742                finality_timeout,
743            )
744            .await?;
745        let driver_type = "transaction_driver";
746
747        add_server_timing("wait_for_finality done");
748
749        self.metrics.wait_for_finality_finished.inc();
750
751        let elapsed = timer.elapsed().as_secs_f64();
752        if elapsed > 10.0 {
753            warn!(
754                ?tx_digest,
755                "Settlement finality latency {} is too high", elapsed,
756            );
757        }
758        self.metrics
759            .settlement_finality_latency
760            .with_label_values(&[tx_type.as_str(), driver_type])
761            .observe(elapsed);
762        good_response_metrics.inc();
763
764        Ok(response)
765    }
766
767    #[instrument(level = "error", skip_all, err(level = "info"))]
768    async fn submit_with_transaction_driver(
769        &self,
770        td: &Arc<TransactionDriver<A>>,
771        request: &ExecuteTransactionRequestV3,
772        client_addr: Option<SocketAddr>,
773        verified_transaction: &VerifiedTransaction,
774        good_response_metrics: &GenericCounter<AtomicU64>,
775        timeout_duration: Option<Duration>,
776    ) -> Result<QuorumTransactionResponse, TransactionSubmissionError> {
777        let tx_digest = *verified_transaction.digest();
778        let td_response = td
779            .drive_transaction(
780                SubmitTxRequest::new_transaction(request.transaction.clone()),
781                SubmitTransactionOptions {
782                    forwarded_client_addr: client_addr,
783                    allowed_validators: self.td_allowed_submission_list.clone(),
784                    blocked_validators: self.td_blocked_submission_list.clone(),
785                },
786                timeout_duration,
787            )
788            .await
789            .map_err(|e| match e {
790                TransactionDriverError::TimeoutWithLastRetriableError {
791                    last_error,
792                    attempts,
793                    timeout,
794                } => TransactionSubmissionError::TimeoutBeforeFinalityWithErrors {
795                    last_error: last_error.map(|e| e.to_string()).unwrap_or_default(),
796                    attempts,
797                    timeout,
798                },
799                other => TransactionSubmissionError::TransactionFailed {
800                    category: other.categorize(),
801                    details: other.to_string(),
802                },
803            });
804
805        match td_response {
806            Err(e) => {
807                warn!(?tx_digest, "TransactionDriver error: {e:?}");
808                Err(e)
809            }
810            Ok(quorum_transaction_response) => {
811                good_response_metrics.inc();
812                Ok(quorum_transaction_response)
813            }
814        }
815    }
816
817    #[instrument(
818        name = "tx_orchestrator_wait_for_finalized_tx_executed_locally_with_timeout",
819        level = "debug",
820        skip_all,
821        err(level = "info")
822    )]
823    async fn wait_for_finalized_tx_executed_locally_with_timeout(
824        validator_state: &Arc<AuthorityState>,
825        tx_digest: TransactionDigest,
826        tx_type: TxType,
827        metrics: &TransactionOrchestratorMetrics,
828    ) -> SuiResult {
829        metrics.local_execution_in_flight.inc();
830        let _metrics_guard =
831            scopeguard::guard(metrics.local_execution_in_flight.clone(), |in_flight| {
832                in_flight.dec();
833            });
834
835        let _latency_guard = metrics
836            .local_execution_latency
837            .with_label_values(&[tx_type.as_str()])
838            .start_timer();
839        debug!("Waiting for finalized tx to be executed locally.");
840        match timeout(LOCAL_EXECUTION_TIMEOUT, async move {
841            validator_state
842                .get_transaction_cache_reader()
843                .notify_read_executed_effects_digests(
844                    "TransactionOrchestrator::notify_read_wait_for_local_execution",
845                    &[tx_digest],
846                )
847                .await;
848            // Wait for the checkpoint containing this tx to be finalized.
849            // Index data is committed before the checkpoint notification fires,
850            // so it is guaranteed to be available when this resolves.
851            let epoch_store = validator_state.load_epoch_store_one_call_per_task();
852            epoch_store
853                .transactions_executed_in_checkpoint_notify(vec![tx_digest])
854                .await;
855        })
856        .instrument(error_span!(
857            "transaction_orchestrator::local_execution",
858            ?tx_digest
859        ))
860        .await
861        {
862            Err(_elapsed) => {
863                debug!(
864                    "Waiting for finalized tx to be executed locally timed out within {:?}.",
865                    LOCAL_EXECUTION_TIMEOUT
866                );
867                metrics.local_execution_timeout.inc();
868                Err(SuiErrorKind::TimeoutError.into())
869            }
870            Ok(_) => {
871                metrics.local_execution_success.inc();
872                Ok(())
873            }
874        }
875    }
876
877    fn update_metrics<'a>(
878        &'a self,
879        transaction: &Transaction,
880    ) -> (impl Drop + use<A>, &'a GenericCounter<AtomicU64>) {
881        let (in_flight, good_response) = if transaction.is_consensus_tx() {
882            self.metrics.total_req_received_shared_object.inc();
883            (
884                self.metrics.req_in_flight_shared_object.clone(),
885                &self.metrics.good_response_shared_object,
886            )
887        } else {
888            self.metrics.total_req_received_single_writer.inc();
889            (
890                self.metrics.req_in_flight_single_writer.clone(),
891                &self.metrics.good_response_single_writer,
892            )
893        };
894        in_flight.inc();
895        (
896            scopeguard::guard(in_flight, |in_flight| {
897                in_flight.dec();
898            }),
899            good_response,
900        )
901    }
902
903    fn start_task_to_recover_txes_in_log(
904        pending_tx_log: Arc<WritePathPendingTransactionLog>,
905        transaction_driver: Arc<TransactionDriver<A>>,
906    ) {
907        spawn_logged_monitored_task!(async move {
908            if std::env::var("SKIP_LOADING_FROM_PENDING_TX_LOG").is_ok() {
909                info!("Skipping loading pending transactions from pending_tx_log.");
910                return;
911            }
912            let pending_txes = pending_tx_log
913                .load_all_pending_transactions()
914                .expect("failed to load all pending transactions");
915            let num_pending_txes = pending_txes.len();
916            info!(
917                "Recovering {} pending transactions from pending_tx_log.",
918                num_pending_txes
919            );
920            let mut recovery = pending_txes
921                .into_iter()
922                .map(|tx| {
923                    let pending_tx_log = pending_tx_log.clone();
924                    let transaction_driver = transaction_driver.clone();
925                    async move {
926                        // TODO: ideally pending_tx_log would not contain VerifiedTransaction, but that
927                        // requires a migration.
928                        let tx = tx.into_inner();
929                        let tx_digest = *tx.digest();
930                        // It's not impossible we fail to enqueue a task but that's not the end of world.
931                        // TODO(william) correctly extract client_addr from logs
932                        if let Err(err) = transaction_driver
933                            .drive_transaction(
934                                SubmitTxRequest::new_transaction(tx),
935                                SubmitTransactionOptions::default(),
936                                Some(Duration::from_secs(60)),
937                            )
938                            .await
939                        {
940                            warn!(?tx_digest, "Failed to execute recovered transaction: {err}");
941                        } else {
942                            debug!(?tx_digest, "Executed recovered transaction");
943                        }
944                        if let Err(err) = pending_tx_log.finish_transaction(&tx_digest) {
945                            warn!(
946                                ?tx_digest,
947                                "Failed to clean up transaction in pending log: {err}"
948                            );
949                        } else {
950                            debug!(?tx_digest, "Cleaned up transaction in pending log");
951                        }
952                    }
953                })
954                .collect::<FuturesUnordered<_>>();
955
956            let mut num_recovered = 0;
957            while recovery.next().await.is_some() {
958                num_recovered += 1;
959                if num_recovered % 1000 == 0 {
960                    info!(
961                        "Recovered {} out of {} transactions from pending_tx_log.",
962                        num_recovered, num_pending_txes
963                    );
964                }
965            }
966            info!(
967                "Recovery finished. Recovered {} out of {} transactions from pending_tx_log.",
968                num_recovered, num_pending_txes
969            );
970        });
971    }
972}
973/// Prometheus metrics which can be displayed in Grafana, queried and alerted on
974#[derive(Clone)]
975pub struct TransactionOrchestratorMetrics {
976    total_req_received_single_writer: GenericCounter<AtomicU64>,
977    total_req_received_shared_object: GenericCounter<AtomicU64>,
978
979    good_response_single_writer: GenericCounter<AtomicU64>,
980    good_response_shared_object: GenericCounter<AtomicU64>,
981
982    req_in_flight_single_writer: GenericGauge<AtomicI64>,
983    req_in_flight_shared_object: GenericGauge<AtomicI64>,
984
985    wait_for_finality_in_flight: GenericGauge<AtomicI64>,
986    wait_for_finality_finished: GenericCounter<AtomicU64>,
987    wait_for_finality_timeout: GenericCounter<AtomicU64>,
988
989    local_execution_in_flight: GenericGauge<AtomicI64>,
990    local_execution_success: GenericCounter<AtomicU64>,
991    local_execution_timeout: GenericCounter<AtomicU64>,
992
993    early_cached_response: IntCounter,
994    concurrent_execution: IntCounter,
995
996    early_validation_rejections: IntCounterVec,
997
998    background_retry_started: IntGauge,
999    background_retry_attempts: IntCounterVec,
1000
1001    request_latency: HistogramVec,
1002    local_execution_latency: HistogramVec,
1003    settlement_finality_latency: HistogramVec,
1004}
1005
1006// Note that labeled-metrics are stored upfront individually
1007// to mitigate the perf hit by MetricsVec.
1008// See https://github.com/tikv/rust-prometheus/tree/master/static-metric
1009impl TransactionOrchestratorMetrics {
1010    pub fn new(registry: &Registry) -> Self {
1011        let total_req_received = register_int_counter_vec_with_registry!(
1012            "tx_orchestrator_total_req_received",
1013            "Total number of executions request Transaction Orchestrator receives, group by tx type",
1014            &["tx_type"],
1015            registry
1016        )
1017        .unwrap();
1018
1019        let total_req_received_single_writer =
1020            total_req_received.with_label_values(&[TX_TYPE_SINGLE_WRITER_TX]);
1021        let total_req_received_shared_object =
1022            total_req_received.with_label_values(&[TX_TYPE_SHARED_OBJ_TX]);
1023
1024        let good_response = register_int_counter_vec_with_registry!(
1025            "tx_orchestrator_good_response",
1026            "Total number of good responses Transaction Orchestrator generates, group by tx type",
1027            &["tx_type"],
1028            registry
1029        )
1030        .unwrap();
1031
1032        let good_response_single_writer =
1033            good_response.with_label_values(&[TX_TYPE_SINGLE_WRITER_TX]);
1034        let good_response_shared_object = good_response.with_label_values(&[TX_TYPE_SHARED_OBJ_TX]);
1035
1036        let req_in_flight = register_int_gauge_vec_with_registry!(
1037            "tx_orchestrator_req_in_flight",
1038            "Number of requests in flights Transaction Orchestrator processes, group by tx type",
1039            &["tx_type"],
1040            registry
1041        )
1042        .unwrap();
1043
1044        let req_in_flight_single_writer =
1045            req_in_flight.with_label_values(&[TX_TYPE_SINGLE_WRITER_TX]);
1046        let req_in_flight_shared_object = req_in_flight.with_label_values(&[TX_TYPE_SHARED_OBJ_TX]);
1047
1048        Self {
1049            total_req_received_single_writer,
1050            total_req_received_shared_object,
1051            good_response_single_writer,
1052            good_response_shared_object,
1053            req_in_flight_single_writer,
1054            req_in_flight_shared_object,
1055            wait_for_finality_in_flight: register_int_gauge_with_registry!(
1056                "tx_orchestrator_wait_for_finality_in_flight",
1057                "Number of in flight txns Transaction Orchestrator are waiting for finality for",
1058                registry,
1059            )
1060            .unwrap(),
1061            wait_for_finality_finished: register_int_counter_with_registry!(
1062                "tx_orchestrator_wait_for_finality_fnished",
1063                "Total number of txns Transaction Orchestrator gets responses from Quorum Driver before timeout, either success or failure",
1064                registry,
1065            )
1066            .unwrap(),
1067            wait_for_finality_timeout: register_int_counter_with_registry!(
1068                "tx_orchestrator_wait_for_finality_timeout",
1069                "Total number of txns timing out in waiting for finality Transaction Orchestrator handles",
1070                registry,
1071            )
1072            .unwrap(),
1073            local_execution_in_flight: register_int_gauge_with_registry!(
1074                "tx_orchestrator_local_execution_in_flight",
1075                "Number of local execution txns in flights Transaction Orchestrator handles",
1076                registry,
1077            )
1078            .unwrap(),
1079            local_execution_success: register_int_counter_with_registry!(
1080                "tx_orchestrator_local_execution_success",
1081                "Total number of successful local execution txns Transaction Orchestrator handles",
1082                registry,
1083            )
1084            .unwrap(),
1085            local_execution_timeout: register_int_counter_with_registry!(
1086                "tx_orchestrator_local_execution_timeout",
1087                "Total number of timed-out local execution txns Transaction Orchestrator handles",
1088                registry,
1089            )
1090            .unwrap(),
1091            early_cached_response: register_int_counter_with_registry!(
1092                "tx_orchestrator_early_cached_response",
1093                "Total number of requests returning cached results for already-executed transactions",
1094                registry,
1095            )
1096            .unwrap(),
1097            concurrent_execution: register_int_counter_with_registry!(
1098                "tx_orchestrator_concurrent_execution",
1099                "Total number of concurrent execution where effects are available locally finishing driving the transaction to finality",
1100                registry,
1101            )
1102            .unwrap(),
1103            early_validation_rejections: register_int_counter_vec_with_registry!(
1104                "tx_orchestrator_early_validation_rejections",
1105                "Total number of transactions rejected during early validation before submission, by reason",
1106                &["reason"],
1107                registry,
1108            )
1109            .unwrap(),
1110            background_retry_started: register_int_gauge_with_registry!(
1111                "tx_orchestrator_background_retry_started",
1112                "Number of background retry tasks kicked off for transactions with retriable errors",
1113                registry,
1114            )
1115            .unwrap(),
1116            background_retry_attempts: register_int_counter_vec_with_registry!(
1117                "tx_orchestrator_background_retry_attempts",
1118                "Total number of background retry attempts, by status",
1119                &["status"],
1120                registry,
1121            )
1122            .unwrap(),
1123            request_latency: register_histogram_vec_with_registry!(
1124                "tx_orchestrator_request_latency",
1125                "Time spent in processing one Transaction Orchestrator request",
1126                &["tx_type", "route", "wait_for_local_execution"],
1127                mysten_metrics::COARSE_LATENCY_SEC_BUCKETS.to_vec(),
1128                registry,
1129            )
1130            .unwrap(),
1131            local_execution_latency: register_histogram_vec_with_registry!(
1132                "tx_orchestrator_local_execution_latency",
1133                "Time spent in waiting for one Transaction Orchestrator gets locally executed",
1134                &["tx_type"],
1135                mysten_metrics::COARSE_LATENCY_SEC_BUCKETS.to_vec(),
1136                registry,
1137            )
1138            .unwrap(),
1139            settlement_finality_latency: register_histogram_vec_with_registry!(
1140                "tx_orchestrator_settlement_finality_latency",
1141                "Time spent in waiting for one Transaction Orchestrator gets settled and finalized",
1142                &["tx_type", "driver_type"],
1143                mysten_metrics::LATENCY_SEC_BUCKETS.to_vec(),
1144                registry,
1145            )
1146            .unwrap(),
1147        }
1148    }
1149
1150    pub fn new_for_tests() -> Self {
1151        let registry = Registry::new();
1152        Self::new(&registry)
1153    }
1154}
1155
1156#[async_trait::async_trait]
1157impl<A> sui_types::transaction_executor::TransactionExecutor for TransactionOrchestrator<A>
1158where
1159    A: AuthorityAPI + Send + Sync + 'static + Clone,
1160{
1161    async fn execute_transaction(
1162        &self,
1163        request: ExecuteTransactionRequestV3,
1164        client_addr: Option<std::net::SocketAddr>,
1165    ) -> Result<ExecuteTransactionResponseV3, TransactionSubmissionError> {
1166        self.execute_transaction_v3(request, client_addr).await
1167    }
1168
1169    fn simulate_transaction(
1170        &self,
1171        transaction: TransactionData,
1172        checks: TransactionChecks,
1173        allow_mock_gas_coin: bool,
1174    ) -> Result<SimulateTransactionResult, SuiError> {
1175        self.inner
1176            .validator_state
1177            .simulate_transaction(transaction, checks, allow_mock_gas_coin)
1178    }
1179}
1180
1181/// Keeps track of inflight transactions being submitted, and helps recover transactions
1182/// on restart.
1183struct TransactionSubmissionGuard {
1184    pending_tx_log: Arc<WritePathPendingTransactionLog>,
1185    tx_digest: TransactionDigest,
1186    is_new_transaction: bool,
1187}
1188
1189impl TransactionSubmissionGuard {
1190    pub fn new(
1191        pending_tx_log: Arc<WritePathPendingTransactionLog>,
1192        tx: &VerifiedTransaction,
1193    ) -> Self {
1194        let is_new_transaction = pending_tx_log.write_pending_transaction_maybe(tx);
1195        let tx_digest = *tx.digest();
1196        if is_new_transaction {
1197            debug!(?tx_digest, "Added transaction to inflight set");
1198        } else {
1199            debug!(
1200                ?tx_digest,
1201                "Transaction already being processed, no new submission will be made"
1202            );
1203        };
1204        Self {
1205            pending_tx_log,
1206            tx_digest,
1207            is_new_transaction,
1208        }
1209    }
1210
1211    fn is_new_transaction(&self) -> bool {
1212        self.is_new_transaction
1213    }
1214}
1215
1216impl Drop for TransactionSubmissionGuard {
1217    fn drop(&mut self) {
1218        if let Err(err) = self.pending_tx_log.finish_transaction(&self.tx_digest) {
1219            warn!(?self.tx_digest, "Failed to clean up transaction in pending log: {err}");
1220        } else {
1221            debug!(?self.tx_digest, "Cleaned up transaction in pending log");
1222        }
1223    }
1224}