diff --git a/CHANGELOG.md b/CHANGELOG.md index 06cf04b56ab..a74cf588efa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - [2532](https://github.com/FuelLabs/fuel-core/pull/2532): Getters for inner rocksdb database handles. - [2524](https://github.com/FuelLabs/fuel-core/pull/2524): Adds a new lock type which is optimized for certain workloads to the txpool and p2p services. - [2535](https://github.com/FuelLabs/fuel-core/pull/2535): Expose `backup` and `restore` APIs on the `CombinedDatabase` struct to create portable backups and restore from them. +- [2550](https://github.com/FuelLabs/fuel-core/pull/2550): Add statistics and more limits infos about txpool on the node_info endpoint ### Fixed - [2558](https://github.com/FuelLabs/fuel-core/pull/2558): Rename `cost` and `reward` to remove `excess` wording diff --git a/crates/client/assets/schema.sdl b/crates/client/assets/schema.sdl index 95c2935241c..6829d9d3953 100644 --- a/crates/client/assets/schema.sdl +++ b/crates/client/assets/schema.sdl @@ -760,8 +760,11 @@ type NodeInfo { utxoValidation: Boolean! vmBacktrace: Boolean! maxTx: U64! + maxGas: U64! + maxSize: U64! maxDepth: U64! nodeVersion: String! + txPoolStats: TxPoolStats! peers: [PeerInfo!]! } @@ -1276,6 +1279,21 @@ enum TxParametersVersion { scalar TxPointer +type TxPoolStats { + """ + The number of transactions in the pool + """ + txCount: U64! + """ + The total size of the transactions in the pool + """ + totalSize: U64! + """ + The total gas of the transactions in the pool + """ + totalGas: U64! +} + scalar U128 scalar U16 diff --git a/crates/client/src/client/schema/node_info.rs b/crates/client/src/client/schema/node_info.rs index 8eb6c9212ab..1a3e7b5d859 100644 --- a/crates/client/src/client/schema/node_info.rs +++ b/crates/client/src/client/schema/node_info.rs @@ -23,8 +23,11 @@ pub struct NodeInfo { pub utxo_validation: bool, pub vm_backtrace: bool, pub max_tx: U64, + pub max_gas: U64, + pub max_size: U64, pub max_depth: U64, pub node_version: String, + pub tx_pool_stats: TxPoolStats, } #[derive(cynic::QueryFragment, Clone, Debug)] @@ -77,6 +80,14 @@ impl From for fuel_core_types::services::p2p::PeerInfo { } } +#[derive(cynic::QueryFragment, Clone, Debug, PartialEq, Eq)] +#[cynic(schema_path = "./assets/schema.sdl")] +pub struct TxPoolStats { + pub tx_count: U64, + pub total_gas: U64, + pub total_size: U64, +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/client/src/client/schema/snapshots/fuel_core_client__client__schema__node_info__tests__node_info_query_gql_output.snap b/crates/client/src/client/schema/snapshots/fuel_core_client__client__schema__node_info__tests__node_info_query_gql_output.snap index e89674bf28c..d4f70ba6e26 100644 --- a/crates/client/src/client/schema/snapshots/fuel_core_client__client__schema__node_info__tests__node_info_query_gql_output.snap +++ b/crates/client/src/client/schema/snapshots/fuel_core_client__client__schema__node_info__tests__node_info_query_gql_output.snap @@ -1,13 +1,21 @@ --- source: crates/client/src/client/schema/node_info.rs expression: operation.query +snapshot_kind: text --- query QueryNodeInfo { nodeInfo { utxoValidation vmBacktrace maxTx + maxGas + maxSize maxDepth nodeVersion + txPoolStats { + txCount + totalGas + totalSize + } } } diff --git a/crates/client/src/client/types/node_info.rs b/crates/client/src/client/types/node_info.rs index b4e8c306c9b..dce6a6a1174 100644 --- a/crates/client/src/client/types/node_info.rs +++ b/crates/client/src/client/types/node_info.rs @@ -1,12 +1,18 @@ -use crate::client::schema; +use crate::client::schema::{ + self, + node_info::TxPoolStats, +}; #[derive(Clone, Debug, PartialEq, Eq)] pub struct NodeInfo { pub utxo_validation: bool, pub vm_backtrace: bool, pub max_tx: u64, + pub max_gas: u64, + pub max_size: u64, pub max_depth: u64, pub node_version: String, + pub tx_pool_stats: TxPoolStats, } // GraphQL Translation @@ -17,8 +23,11 @@ impl From for NodeInfo { utxo_validation: value.utxo_validation, vm_backtrace: value.vm_backtrace, max_tx: value.max_tx.into(), + max_gas: value.max_gas.into(), + max_size: value.max_size.into(), max_depth: value.max_depth.into(), node_version: value.node_version, + tx_pool_stats: value.tx_pool_stats, } } } diff --git a/crates/fuel-core/src/graphql_api.rs b/crates/fuel-core/src/graphql_api.rs index 7a4f3746c2b..c8ebcddf875 100644 --- a/crates/fuel-core/src/graphql_api.rs +++ b/crates/fuel-core/src/graphql_api.rs @@ -26,6 +26,8 @@ pub struct Config { pub debug: bool, pub vm_backtrace: bool, pub max_tx: usize, + pub max_gas: u64, + pub max_size: usize, pub max_txpool_dependency_chain_length: usize, pub chain_name: String, } diff --git a/crates/fuel-core/src/graphql_api/ports.rs b/crates/fuel-core/src/graphql_api/ports.rs index 52bf74bdad7..85113b5f9b4 100644 --- a/crates/fuel-core/src/graphql_api/ports.rs +++ b/crates/fuel-core/src/graphql_api/ports.rs @@ -24,7 +24,10 @@ use fuel_core_storage::{ StorageInspect, StorageRead, }; -use fuel_core_txpool::TxStatusMessage; +use fuel_core_txpool::{ + TxPoolStats, + TxStatusMessage, +}; use fuel_core_types::{ blockchain::{ block::CompressedBlock, @@ -238,6 +241,8 @@ pub trait TxPoolPort: Send + Sync { &self, tx_id: TxId, ) -> anyhow::Result>; + + fn latest_pool_stats(&self) -> TxPoolStats; } #[async_trait] diff --git a/crates/fuel-core/src/schema/node_info.rs b/crates/fuel-core/src/schema/node_info.rs index 5805c76dc60..d02070b361b 100644 --- a/crates/fuel-core/src/schema/node_info.rs +++ b/crates/fuel-core/src/schema/node_info.rs @@ -2,9 +2,12 @@ use super::scalars::{ U32, U64, }; -use crate::fuel_core_graphql_api::{ - query_costs, - Config as GraphQLConfig, +use crate::{ + fuel_core_graphql_api::{ + query_costs, + Config as GraphQLConfig, + }, + graphql_api::api_service::TxPool, }; use async_graphql::{ Context, @@ -16,6 +19,8 @@ pub struct NodeInfo { utxo_validation: bool, vm_backtrace: bool, max_tx: U64, + max_gas: U64, + max_size: U64, max_depth: U64, node_version: String, } @@ -34,6 +39,14 @@ impl NodeInfo { self.max_tx } + async fn max_gas(&self) -> U64 { + self.max_gas + } + + async fn max_size(&self) -> U64 { + self.max_size + } + async fn max_depth(&self) -> U64 { self.max_depth } @@ -42,6 +55,15 @@ impl NodeInfo { self.node_version.to_owned() } + #[graphql(complexity = "query_costs().storage_read + child_complexity")] + async fn tx_pool_stats( + &self, + ctx: &Context<'_>, + ) -> async_graphql::Result { + let tx_pool = ctx.data_unchecked::(); + Ok(TxPoolStats(tx_pool.latest_pool_stats())) + } + #[graphql(complexity = "query_costs().get_peers + child_complexity")] async fn peers(&self, _ctx: &Context<'_>) -> async_graphql::Result> { #[cfg(feature = "p2p")] @@ -76,6 +98,8 @@ impl NodeQuery { utxo_validation: config.utxo_validation, vm_backtrace: config.vm_backtrace, max_tx: (config.max_tx as u64).into(), + max_gas: config.max_gas.into(), + max_size: (config.max_size as u64).into(), max_depth: (config.max_txpool_dependency_chain_length as u64).into(), node_version: VERSION.to_owned(), }) @@ -124,3 +148,23 @@ impl PeerInfo { self.0.app_score } } + +struct TxPoolStats(fuel_core_txpool::TxPoolStats); + +#[Object] +impl TxPoolStats { + /// The number of transactions in the pool + async fn tx_count(&self) -> U64 { + self.0.tx_count.into() + } + + /// The total size of the transactions in the pool + async fn total_size(&self) -> U64 { + self.0.total_size.into() + } + + /// The total gas of the transactions in the pool + async fn total_gas(&self) -> U64 { + self.0.total_gas.into() + } +} diff --git a/crates/fuel-core/src/service/adapters/graphql_api.rs b/crates/fuel-core/src/service/adapters/graphql_api.rs index 43cd0471794..2a626978ed5 100644 --- a/crates/fuel-core/src/service/adapters/graphql_api.rs +++ b/crates/fuel-core/src/service/adapters/graphql_api.rs @@ -30,7 +30,10 @@ use crate::{ use async_trait::async_trait; use fuel_core_services::stream::BoxStream; use fuel_core_storage::Result as StorageResult; -use fuel_core_txpool::TxStatusMessage; +use fuel_core_txpool::{ + TxPoolStats, + TxStatusMessage, +}; use fuel_core_types::{ blockchain::header::ConsensusParametersVersion, entities::relayer::message::MerkleProof, @@ -97,6 +100,10 @@ impl TxPoolPort for TxPoolAdapter { ) -> anyhow::Result> { self.service.tx_update_subscribe(id) } + + fn latest_pool_stats(&self) -> TxPoolStats { + self.service.latest_stats() + } } impl DatabaseMessageProof for OnChainIterableKeyValueView { diff --git a/crates/fuel-core/src/service/sub_services.rs b/crates/fuel-core/src/service/sub_services.rs index 37c3b559456..b29b76afe2e 100644 --- a/crates/fuel-core/src/service/sub_services.rs +++ b/crates/fuel-core/src/service/sub_services.rs @@ -318,6 +318,8 @@ pub fn init_sub_services( debug: config.debug, vm_backtrace: config.vm.backtrace, max_tx: config.txpool.pool_limits.max_txs, + max_gas: config.txpool.pool_limits.max_gas, + max_size: config.txpool.pool_limits.max_bytes_size, max_txpool_dependency_chain_length: config.txpool.max_txs_chain_count, chain_name, }; diff --git a/crates/services/txpool_v2/src/lib.rs b/crates/services/txpool_v2/src/lib.rs index c462d43b760..95bc3f99212 100644 --- a/crates/services/txpool_v2/src/lib.rs +++ b/crates/services/txpool_v2/src/lib.rs @@ -61,6 +61,7 @@ mod tests; fuel_core_trace::enable_tracing!(); use fuel_core_types::fuel_asm::Word; +pub use pool::TxPoolStats; pub use selection_algorithms::Constraints; pub use service::{ new_service, diff --git a/crates/services/txpool_v2/src/pool.rs b/crates/services/txpool_v2/src/pool.rs index 07c7774f95a..de80ded6b7e 100644 --- a/crates/services/txpool_v2/src/pool.rs +++ b/crates/services/txpool_v2/src/pool.rs @@ -49,6 +49,13 @@ use crate::{ #[cfg(test)] use std::collections::HashSet; +#[derive(Debug, Clone, Copy, Default)] +pub struct TxPoolStats { + pub tx_count: u64, + pub total_size: u64, + pub total_gas: u64, +} + /// The pool is the main component of the txpool service. It is responsible for storing transactions /// and allowing the selection of transactions for inclusion in a block. pub struct Pool { @@ -66,6 +73,8 @@ pub struct Pool { pub(crate) current_gas: u64, /// Current pool size in bytes. pub(crate) current_bytes_size: usize, + /// The current pool gas. + pub(crate) pool_stats_sender: tokio::sync::watch::Sender, } impl Pool { @@ -75,6 +84,7 @@ impl Pool { collision_manager: CM, selection_algorithm: SA, config: Config, + pool_stats_sender: tokio::sync::watch::Sender, ) -> Self { Pool { storage, @@ -84,6 +94,7 @@ impl Pool { tx_id_to_storage_id: HashMap::new(), current_gas: 0, current_bytes_size: 0, + pool_stats_sender, } } @@ -183,10 +194,18 @@ where .into_iter() .map(|data| data.transaction) .collect::>(); - + self.update_stats(); Ok(removed_transactions) } + fn update_stats(&self) { + let _ = self.pool_stats_sender.send(TxPoolStats { + tx_count: self.tx_count() as u64, + total_size: self.current_bytes_size as u64, + total_gas: self.current_gas, + }); + } + /// Check if a transaction can be inserted into the pool. pub fn can_insert_transaction( &self, @@ -308,13 +327,17 @@ where }); } - best_txs + let txs = best_txs .into_iter() .map(|storage_entry| { self.update_components_and_caches_on_removal(iter::once(&storage_entry)); storage_entry.transaction }) - .collect::>() + .collect::>(); + + self.update_stats(); + + txs } pub fn find_one(&self, tx_id: &TxId) -> Option<&StorageData> { @@ -362,6 +385,8 @@ where self.update_components_and_caches_on_removal(iter::once(&transaction)); } } + + self.update_stats(); } /// Check if the pool has enough space to store a transaction. @@ -513,6 +538,9 @@ where .extend(removed.into_iter().map(|data| data.transaction)); } } + + self.update_stats(); + removed_transactions } @@ -526,6 +554,9 @@ where self.update_components_and_caches_on_removal(removed.iter()); txs_removed.extend(removed.into_iter().map(|data| data.transaction)); } + + self.update_stats(); + txs_removed } diff --git a/crates/services/txpool_v2/src/service.rs b/crates/services/txpool_v2/src/service.rs index a40946cf774..5ae046c757e 100644 --- a/crates/services/txpool_v2/src/service.rs +++ b/crates/services/txpool_v2/src/service.rs @@ -1,5 +1,6 @@ use crate::{ self as fuel_core_txpool, + pool::TxPoolStats, }; use fuel_core_services::TaskNextAction; @@ -765,6 +766,8 @@ where mpsc::channel(1); let (read_pool_requests_sender, read_pool_requests_receiver) = mpsc::channel(config.service_channel_limits.max_pending_read_pool_requests); + let (pool_stats_sender, pool_stats_receiver) = + tokio::sync::watch::channel(TxPoolStats::default()); let tx_status_sender = TxStatusChange::new( config.max_tx_update_subscriptions, // The connection should be closed automatically after the `SqueezedOut` event. @@ -781,6 +784,7 @@ where select_transactions_requests_sender, read_pool_requests_sender, new_txs_notifier, + latest_stats: pool_stats_receiver, }; let subscriptions = Subscriptions { @@ -831,6 +835,7 @@ where BasicCollisionManager::new(), RatioTipGasSelection::new(), config, + pool_stats_sender, ); // BlockHeight is < 64 bytes, so we can use SeqLock diff --git a/crates/services/txpool_v2/src/shared_state.rs b/crates/services/txpool_v2/src/shared_state.rs index a7dbaaf0df1..b57325e39e0 100644 --- a/crates/services/txpool_v2/src/shared_state.rs +++ b/crates/services/txpool_v2/src/shared_state.rs @@ -20,6 +20,7 @@ use tokio::sync::{ use crate::{ error::Error, + pool::TxPoolStats, service::{ BorrowTxPoolRequest, ReadPoolRequest, @@ -54,6 +55,7 @@ pub struct SharedState { pub(crate) read_pool_requests_sender: mpsc::Sender, pub(crate) tx_status_sender: TxStatusChange, pub(crate) new_txs_notifier: tokio::sync::watch::Sender<()>, + pub(crate) latest_stats: tokio::sync::watch::Receiver, } impl SharedState { @@ -178,4 +180,8 @@ impl SharedState { .write_pool_requests_sender .try_send(WritePoolRequest::RemoveCoinDependents { transactions }); } + + pub fn latest_stats(&self) -> TxPoolStats { + *self.latest_stats.borrow() + } } diff --git a/crates/services/txpool_v2/src/tests/universe.rs b/crates/services/txpool_v2/src/tests/universe.rs index d2f1a747e33..cea372a71d1 100644 --- a/crates/services/txpool_v2/src/tests/universe.rs +++ b/crates/services/txpool_v2/src/tests/universe.rs @@ -62,7 +62,10 @@ use crate::{ config::Config, error::Error, new_service, - pool::Pool, + pool::{ + Pool, + TxPoolStats, + }, selection_algorithms::ratio_tip_gas::RatioTipGasSelection, service::{ memory::MemoryPool, @@ -101,6 +104,7 @@ pub struct TestPoolUniverse { rng: StdRng, pub config: Config, pool: Option>, + stats_receiver: Option>, } impl Default for TestPoolUniverse { @@ -110,6 +114,7 @@ impl Default for TestPoolUniverse { rng: StdRng::seed_from_u64(0), config: Default::default(), pool: None, + stats_receiver: None, } } } @@ -123,6 +128,14 @@ impl TestPoolUniverse { &self.mock_db } + pub fn latest_stats(&self) -> TxPoolStats { + if let Some(receiver) = &self.stats_receiver { + *receiver.borrow() + } else { + TxPoolStats::default() + } + } + pub fn config(self, config: Config) -> Self { if self.pool.is_some() { panic!("Pool already built"); @@ -131,6 +144,7 @@ impl TestPoolUniverse { } pub fn build_pool(&mut self) { + let (tx, rx) = tokio::sync::watch::channel(TxPoolStats::default()); let pool = Arc::new(RwLock::new(Pool::new( GraphStorage::new(GraphConfig { max_txs_chain_count: self.config.max_txs_chain_count, @@ -138,7 +152,9 @@ impl TestPoolUniverse { BasicCollisionManager::new(), RatioTipGasSelection::new(), self.config.clone(), + tx, ))); + self.stats_receiver = Some(rx); self.pool = Some(pool.clone()); } @@ -302,6 +318,18 @@ impl TestPoolUniverse { } pub fn assert_pool_integrity(&self, expected_txs: &[ArcPoolTx]) { + let stats = self.latest_stats(); + assert_eq!(stats.tx_count, expected_txs.len() as u64); + let mut total_gas: u64 = 0; + let mut total_size: u64 = 0; + for tx in expected_txs { + total_gas = total_gas.checked_add(tx.max_gas()).unwrap(); + total_size = total_gas + .checked_add(tx.metered_bytes_size() as u64) + .unwrap(); + } + assert_eq!(stats.total_gas, total_gas); + assert_eq!(stats.total_size, total_size); let pool = self.pool.as_ref().unwrap(); let pool = pool.read(); let storage_ids_dependencies = pool.storage.assert_integrity(expected_txs); diff --git a/tests/tests/node_info.rs b/tests/tests/node_info.rs index fb4a2867290..b21a3673540 100644 --- a/tests/tests/node_info.rs +++ b/tests/tests/node_info.rs @@ -1,3 +1,5 @@ +#![allow(non_snake_case)] + use fuel_core::service::{ Config, FuelService, @@ -6,6 +8,8 @@ use fuel_core_client::client::{ types::NodeInfo, FuelClient, }; +use fuel_core_poa::Trigger; +use fuel_core_types::fuel_tx::Transaction; #[tokio::test] async fn node_info() { @@ -27,6 +31,38 @@ async fn node_info() { assert_eq!(max_tx, node_config.txpool.pool_limits.max_txs as u64); } +#[tokio::test] +async fn tx_pool_stats__should_be_updated_when_transaction_is_submitted() { + // Given + let mut node_config = Config::local_node(); + node_config.block_production = Trigger::Never; + + let srv = FuelService::new_node(node_config.clone()).await.unwrap(); + let client = FuelClient::from(srv.bound_address); + + // When + let NodeInfo { + tx_pool_stats: initial_tx_pool_stats, + .. + } = client.node_info().await.unwrap(); + + let tx = Transaction::default_test_tx(); + client.submit(&tx).await.unwrap(); + + let NodeInfo { + tx_pool_stats: updated_tx_pool_stats, + .. + } = client.node_info().await.unwrap(); + + // Then + assert_eq!(initial_tx_pool_stats.tx_count.0, 0); + assert_eq!(initial_tx_pool_stats.total_gas.0, 0); + assert_eq!(initial_tx_pool_stats.total_size.0, 0); + assert_eq!(updated_tx_pool_stats.tx_count.0, 1); + assert_eq!(updated_tx_pool_stats.total_gas.0, 4330); + assert_eq!(updated_tx_pool_stats.total_size.0, 344); +} + #[tokio::test(flavor = "multi_thread")] async fn test_peer_info() { use fuel_core::p2p_test_helpers::{