Skip to main content

iota_graphql_rpc/
config.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2024 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5use std::{collections::BTreeSet, fmt::Display, num::NonZeroU32};
6
7use async_graphql::*;
8use iota_graphql_config::GraphQLConfig;
9use iota_indexer::db::DbUrl;
10use iota_names::config::IotaNamesConfig;
11use serde::{Deserialize, Serialize};
12use url::Url;
13
14use crate::{functional_group::FunctionalGroup, types::int::try_into_int};
15
16const DEFAULT_DB_URL: &str = "postgres://postgres:postgrespw@localhost:5432/iota_indexer";
17
18pub(crate) const DEFAULT_PAGE_SIZE: u32 = 20;
19pub(crate) const MAX_PAGE_SIZE: u32 = 50;
20
21/// The combination of all configurations for the GraphQL service.
22#[GraphQLConfig]
23#[derive(Default)]
24pub struct ServerConfig {
25    pub service: ServiceConfig,
26    pub connection: ConnectionConfig,
27    pub internal_features: InternalFeatureConfig,
28    pub tx_exec_full_node: TxExecFullNodeConfig,
29    pub ide: Ide,
30    pub historic_fallback: HistoricFallbackOptions,
31}
32
33/// Configuration for connections for the RPC, passed in as command-line
34/// arguments. This configures specific connections between this service and
35/// other services, and might differ from instance to instance of the GraphQL
36/// service.
37#[GraphQLConfig]
38#[derive(clap::Args, Clone, Eq, PartialEq)]
39pub struct ConnectionConfig {
40    /// Port to bind the server to
41    #[arg(short, long, default_value_t = ConnectionConfig::default().port)]
42    pub port: u16,
43    /// Host to bind the server to
44    #[arg(long, default_value_t = ConnectionConfig::default().host)]
45    pub host: String,
46    /// DB URL for data fetching
47    #[arg(short, long, default_value = DEFAULT_DB_URL)]
48    pub db_url: DbUrl,
49    /// Pool size for DB connections
50    #[arg(long, default_value_t = ConnectionConfig::default().db_pool_size)]
51    pub db_pool_size: u32,
52    /// Host to bind the prom server to
53    #[arg(long, default_value_t = ConnectionConfig::default().prom_host)]
54    pub prom_host: String,
55    /// Port to bind the prom server to
56    #[arg(long, default_value_t = ConnectionConfig::default().prom_port)]
57    pub prom_port: u16,
58    /// Skip checking whether the service is compatible with the DB it is about
59    /// to connect to, on start-up.
60    #[arg(long, default_value_t = ConnectionConfig::default().skip_migration_consistency_check)]
61    pub skip_migration_consistency_check: bool,
62    /// Maximum number of checkpoints to look back for consistent view queries.
63    /// Directly influences the `availableRange` size. Larger values let
64    /// pagination cursors stay valid for longer, downside is that older cursors
65    /// have higher DB cost.
66    #[arg(
67        long,
68        default_value_t = ConnectionConfig::default().max_available_range,
69        env = "MAX_AVAILABLE_RANGE",
70    )]
71    pub max_available_range: u64,
72}
73
74/// CLI options that control the archival fallback used when Postgres data has
75/// been pruned.
76#[GraphQLConfig]
77#[derive(clap::Args, Debug, Clone)]
78pub struct HistoricFallbackOptions {
79    #[arg(
80        long,
81        help = "Experimental: REST KV store URL for historic fallback. Depends on the iota-rest-kv API which is still being finalized."
82    )]
83    pub fallback_kv_url: Option<Url>,
84
85    #[arg(
86        long,
87        default_value_t = HistoricFallbackOptions::DEFAULT_MULTI_FETCH_BATCH_SIZE,
88        env = "FALLBACK_KV_MULTI_FETCH_BATCH_SIZE",
89        help = "Experimental: Maximum number of keys per batch request to fallback KV store."
90    )]
91    pub fallback_kv_multi_fetch_batch_size: usize,
92
93    #[arg(
94        long,
95        default_value_t = HistoricFallbackOptions::DEFAULT_CONCURRENT_FETCHES,
96        env = "FALLBACK_KV_CONCURRENT_FETCHES",
97        help = "Experimental: Maximum number of concurrent batch requests to fallback KV store."
98    )]
99    pub fallback_kv_concurrent_fetches: usize,
100
101    #[arg(
102        long,
103        default_value_t = HistoricFallbackOptions::DEFAULT_CACHE_SIZE,
104        env = "FALLBACK_KV_CACHE_SIZE",
105        help = "Experimental: Cache size for historic fallback."
106    )]
107    pub fallback_kv_cache_size: u64,
108
109    #[arg(
110        long,
111        default_value_t = HistoricFallbackOptions::DEFAULT_MAX_RESPONSE_SIZE_BYTES,
112        env = "FALLBACK_KV_MAX_RESPONSE_SIZE_BYTES",
113        help = "Experimental: Maximum size in bytes of a single response body accepted from the fallback KV store. Larger responses are rejected."
114    )]
115    pub fallback_kv_max_response_size_bytes: usize,
116}
117
118impl HistoricFallbackOptions {
119    pub const DEFAULT_MULTI_FETCH_BATCH_SIZE: usize = 100;
120    pub const DEFAULT_CONCURRENT_FETCHES: usize = 10;
121    pub const DEFAULT_CACHE_SIZE: u64 = 100_000;
122    pub const DEFAULT_MAX_RESPONSE_SIZE_BYTES: usize = 128 * 1024 * 1024;
123}
124
125impl Default for HistoricFallbackOptions {
126    fn default() -> Self {
127        Self {
128            fallback_kv_url: None,
129            fallback_kv_multi_fetch_batch_size: Self::DEFAULT_MULTI_FETCH_BATCH_SIZE,
130            fallback_kv_concurrent_fetches: Self::DEFAULT_CONCURRENT_FETCHES,
131            fallback_kv_cache_size: Self::DEFAULT_CACHE_SIZE,
132            fallback_kv_max_response_size_bytes: Self::DEFAULT_MAX_RESPONSE_SIZE_BYTES,
133        }
134    }
135}
136
137/// Configuration on features supported by the GraphQL service, passed in a
138/// TOML-based file. These configurations are shared across fleets of the
139/// service, i.e. all testnet services will have the same `ServiceConfig`.
140#[GraphQLConfig]
141#[derive(Default)]
142pub struct ServiceConfig {
143    pub versions: Versions,
144    pub limits: Limits,
145    pub disabled_features: BTreeSet<FunctionalGroup>,
146    pub experiments: Experiments,
147    pub iota_names: IotaNamesConfig,
148    pub background_tasks: BackgroundTasksConfig,
149}
150
151#[GraphQLConfig]
152pub struct Versions {
153    versions: Vec<String>,
154}
155
156#[GraphQLConfig]
157pub struct Limits {
158    /// Maximum depth of nodes in the requests.
159    pub max_query_depth: u32,
160    /// Maximum number of nodes in the requests.
161    pub max_query_nodes: u32,
162    /// Maximum number of output nodes allowed in the response.
163    pub max_output_nodes: u32,
164    /// Maximum size in bytes allowed for the `txBytes` and `signatures` fields
165    /// of a GraphQL mutation request in the `executeTransactionBlock` node,
166    /// and for the `txBytes` of a `dryRunTransactionBlock` node.
167    pub max_tx_payload_size: u32,
168    /// Maximum size in bytes of the JSON payload of a GraphQL read request
169    /// (excluding `max_tx_payload_size`).
170    pub max_query_payload_size: u32,
171    /// Queries whose EXPLAIN cost are more than this will be logged. Given in
172    /// the units used by the database (where 1.0 is roughly the cost of a
173    /// sequential page access).
174    pub max_db_query_cost: u32,
175    /// Paginated queries will return this many elements if a page size is not
176    /// provided.
177    pub default_page_size: u32,
178    /// Paginated queries can return at most this many elements.
179    pub max_page_size: u32,
180    /// Time (in milliseconds) to wait for a transaction to be executed and the
181    /// results returned from GraphQL. If the transaction takes longer than
182    /// this time to execute, the request will return a timeout error, but
183    /// the transaction may continue executing.
184    pub mutation_timeout_ms: u32,
185    /// Time (in milliseconds) to wait for a read request from the GraphQL
186    /// service. Requests that take longer than this time to return a result
187    /// will return a timeout error.
188    pub request_timeout_ms: u32,
189    /// Maximum amount of nesting among type arguments (type arguments nest when
190    /// a type argument is itself generic and has arguments).
191    pub max_type_argument_depth: u32,
192    /// Maximum number of type parameters a type can have.
193    pub max_type_argument_width: u32,
194    /// Maximum size of a fully qualified type.
195    pub max_type_nodes: u32,
196    /// Maximum deph of a move value.
197    pub max_move_value_depth: u32,
198    /// Maximum number of transaction ids that can be passed to a
199    /// `TransactionBlockFilter` or to `transaction_blocks_by_digests`.
200    pub max_transaction_ids: u32,
201    /// Maximum number of candidates to scan when gathering a page of results.
202    pub max_scan_limit: u32,
203    /// Maximum number of checkpoints a subscription may replay from the
204    /// database when resuming from `startAfter`, measured as the difference
205    /// between the sequence number of the latest indexed checkpoint and that
206    /// of the checkpoint containing the `startAfter` digest. A digest further
207    /// behind is rejected.
208    pub max_subscription_recovery_checkpoints: NonZeroU32,
209    /// Maximum number of subscriptions that may be active at the same time
210    /// across all connections.
211    pub max_active_subscriptions: NonZeroU32,
212}
213
214#[GraphQLConfig]
215#[derive(Copy)]
216pub struct BackgroundTasksConfig {
217    /// How often the watermark task checks the indexer database to update the
218    /// checkpoint and epoch watermarks.
219    pub watermark_update_ms: u64,
220}
221
222/// The Version of the service. `year.month` represents the major release.
223/// New `patch` versions represent backwards compatible fixes for their major
224/// release. The `full` version is `year.month.patch-sha`.
225#[derive(Copy, Clone, Debug)]
226pub struct Version {
227    /// The year of this release.
228    pub year: &'static str,
229    /// The month of this release.
230    pub month: &'static str,
231    /// The patch is a positive number incremented for every compatible release
232    /// on top of the major.month release.
233    pub patch: &'static str,
234    /// The commit sha for this release.
235    pub sha: &'static str,
236    /// The full version string.
237    /// Note that this extra field is used only for the uptime_metric function
238    /// which requires a &'static str.
239    pub full: &'static str,
240}
241
242impl Version {
243    /// Use for testing when you need the Version obj and a year.month &str
244    pub fn for_testing() -> Self {
245        Self {
246            year: env!("CARGO_PKG_VERSION_MAJOR"),
247            month: env!("CARGO_PKG_VERSION_MINOR"),
248            patch: env!("CARGO_PKG_VERSION_PATCH"),
249            sha: "testing-no-sha",
250            // note that this full field is needed for metrics but not for testing
251            full: const_str::concat!(
252                env!("CARGO_PKG_VERSION_MAJOR"),
253                ".",
254                env!("CARGO_PKG_VERSION_MINOR"),
255                ".",
256                env!("CARGO_PKG_VERSION_PATCH"),
257                "-testing-no-sha"
258            ),
259        }
260    }
261}
262
263#[GraphQLConfig]
264#[derive(clap::Args)]
265pub struct Ide {
266    /// The title to display at the top of the web-based GraphiQL IDE.
267    #[arg(short, long, default_value_t = Ide::default().ide_title)]
268    pub(crate) ide_title: String,
269}
270
271#[GraphQLConfig]
272#[derive(Default)]
273pub struct Experiments {
274    // Add experimental flags here, to provide access to them through-out the GraphQL
275    // implementation.
276    #[cfg(test)]
277    test_flag: bool,
278}
279
280#[GraphQLConfig]
281pub struct InternalFeatureConfig {
282    pub(crate) query_limits_checker: bool,
283    pub(crate) directive_checker: bool,
284    pub(crate) feature_gate: bool,
285    pub(crate) logger: bool,
286    pub(crate) query_timeout: bool,
287    pub(crate) metrics: bool,
288    pub(crate) tracing: bool,
289    pub(crate) apollo_tracing: bool,
290    pub(crate) open_telemetry: bool,
291}
292
293#[GraphQLConfig]
294#[derive(clap::Args, Default)]
295pub struct TxExecFullNodeConfig {
296    /// RPC URL for the fullnode to send transactions to execute and dry-run.
297    #[arg(long)]
298    pub(crate) node_rpc_url: Option<String>,
299}
300
301/// The enabled features and service limits configured by the server.
302#[Object]
303impl ServiceConfig {
304    /// Check whether `feature` is enabled on this GraphQL service.
305    async fn is_enabled(&self, feature: FunctionalGroup) -> bool {
306        !self.disabled_features.contains(&feature)
307    }
308
309    /// List the available versions for this GraphQL service.
310    async fn available_versions(&self) -> Vec<String> {
311        self.versions.versions.clone()
312    }
313
314    /// List of all features that are enabled on this GraphQL service.
315    async fn enabled_features(&self) -> Vec<FunctionalGroup> {
316        FunctionalGroup::all()
317            .iter()
318            .filter(|g| !self.disabled_features.contains(g))
319            .copied()
320            .collect()
321    }
322
323    /// The maximum depth a GraphQL query can be to be accepted by this service.
324    pub async fn max_query_depth(&self) -> Result<i32> {
325        try_into_int(self.limits.max_query_depth).extend()
326    }
327
328    /// The maximum number of nodes (field names) the service will accept in a
329    /// single query.
330    pub async fn max_query_nodes(&self) -> Result<i32> {
331        try_into_int(self.limits.max_query_nodes).extend()
332    }
333
334    /// The maximum number of output nodes in a GraphQL response.
335    ///
336    /// Non-connection nodes have a count of 1, while connection nodes are
337    /// counted as the specified 'first' or 'last' number of items, or the
338    /// default_page_size as set by the server if those arguments are not
339    /// set.
340    ///
341    /// Counts accumulate multiplicatively down the query tree. For example, if
342    /// a query starts with a connection of first: 10 and has a field to a
343    /// connection with last: 20, the count at the second level would be 200
344    /// nodes. This is then summed to the count of 10 nodes at the first
345    /// level, for a total of 210 nodes.
346    pub async fn max_output_nodes(&self) -> Result<i32> {
347        try_into_int(self.limits.max_output_nodes).extend()
348    }
349
350    /// Maximum estimated cost of a database query used to serve a GraphQL
351    /// request.  This is measured in the same units that the database uses
352    /// in EXPLAIN queries.
353    async fn max_db_query_cost(&self) -> Result<i32> {
354        try_into_int(self.limits.max_db_query_cost).extend()
355    }
356
357    /// Default number of elements allowed on a single page of a connection.
358    async fn default_page_size(&self) -> Result<i32> {
359        try_into_int(self.limits.default_page_size).extend()
360    }
361
362    /// Maximum number of elements allowed on a single page of a connection.
363    async fn max_page_size(&self) -> Result<i32> {
364        try_into_int(self.limits.max_page_size).extend()
365    }
366
367    /// Maximum time in milliseconds spent waiting for a response from fullnode
368    /// after issuing a transaction to execute. Note that the transaction
369    /// may still succeed even in the case of a timeout. Transactions are
370    /// idempotent, so a transaction that times out should be resubmitted
371    /// until the network returns a definite response (success or failure, not
372    /// timeout).
373    async fn mutation_timeout_ms(&self) -> Result<i32> {
374        try_into_int(self.limits.mutation_timeout_ms).extend()
375    }
376
377    /// Maximum time in milliseconds that will be spent to serve one query
378    /// request.
379    async fn request_timeout_ms(&self) -> Result<i32> {
380        try_into_int(self.limits.request_timeout_ms).extend()
381    }
382
383    /// The maximum bytes allowed for transactions in queries.
384    ///
385    /// This corresponds to the `txBytes` and `signatures` fields of the GraphQL
386    /// mutation `executeTransactionBlock` node, or the `txBytes` of a
387    /// `dryRunTransactionBlock`.
388    ///
389    /// By default, this is set to the value of the maximum transaction bytes
390    /// (including the signatures) allowed by the protocol, plus the Base64
391    /// overhead (roughly 1/3 of the original string).
392    async fn max_transaction_payload_size(&self) -> Result<i32> {
393        try_into_int(self.limits.max_tx_payload_size).extend()
394    }
395
396    /// The maximum bytes allowed for the read part of GraphQL queries.
397    ///
398    /// In case of mutations or `dryRunTransactionBlocks` the `txBytes` and
399    /// `signatures` are not included in this limit.
400    async fn max_query_payload_size(&self) -> Result<i32> {
401        try_into_int(self.limits.max_query_payload_size).extend()
402    }
403
404    /// Maximum nesting allowed in type arguments in Move Types resolved by this
405    /// service.
406    async fn max_type_argument_depth(&self) -> Result<i32> {
407        try_into_int(self.limits.max_type_argument_depth).extend()
408    }
409
410    /// Maximum number of type arguments passed into a generic instantiation of
411    /// a Move Type resolved by this service.
412    async fn max_type_argument_width(&self) -> Result<i32> {
413        try_into_int(self.limits.max_type_argument_width).extend()
414    }
415
416    /// Maximum number of structs that need to be processed when calculating the
417    /// layout of a single Move Type.
418    async fn max_type_nodes(&self) -> Result<i32> {
419        try_into_int(self.limits.max_type_nodes).extend()
420    }
421
422    /// Maximum nesting allowed in struct fields when calculating the layout of
423    /// a single Move Type.
424    async fn max_move_value_depth(&self) -> Result<i32> {
425        try_into_int(self.limits.max_move_value_depth).extend()
426    }
427
428    /// Maximum number of transaction ids that can be passed to a
429    /// `TransactionBlockFilter`.
430    async fn max_transaction_ids(&self) -> Result<i32> {
431        try_into_int(self.limits.max_transaction_ids).extend()
432    }
433
434    /// Maximum number of candidates to scan when gathering a page of results.
435    async fn max_scan_limit(&self) -> Result<i32> {
436        try_into_int(self.limits.max_scan_limit).extend()
437    }
438}
439
440impl ConnectionConfig {
441    pub fn new(
442        port: Option<u16>,
443        host: Option<String>,
444        db_url: Option<String>,
445        db_pool_size: Option<u32>,
446        prom_host: Option<String>,
447        prom_port: Option<u16>,
448        skip_migration_consistency_check: Option<bool>,
449        max_available_range: Option<u64>,
450    ) -> Self {
451        let default = Self::default();
452        Self {
453            port: port.unwrap_or(default.port),
454            host: host.unwrap_or(default.host),
455            db_url: db_url.map_or(default.db_url, DbUrl::from),
456            db_pool_size: db_pool_size.unwrap_or(default.db_pool_size),
457            prom_host: prom_host.unwrap_or(default.prom_host),
458            prom_port: prom_port.unwrap_or(default.prom_port),
459            skip_migration_consistency_check: skip_migration_consistency_check
460                .unwrap_or(default.skip_migration_consistency_check),
461            max_available_range: max_available_range.unwrap_or(default.max_available_range),
462        }
463    }
464
465    pub fn ci_integration_test_cfg() -> Self {
466        Self {
467            db_url: "postgres://postgres:postgrespw@localhost:5432/iota_graphql_rpc_e2e_tests"
468                .to_string()
469                .into(),
470            ..Default::default()
471        }
472    }
473
474    pub fn ci_integration_test_cfg_with_db_name(
475        db_name: String,
476        port: u16,
477        prom_port: u16,
478    ) -> Self {
479        Self {
480            db_url: format!("postgres://postgres:postgrespw@localhost:5432/{db_name}").into(),
481            port,
482            prom_port,
483            ..Default::default()
484        }
485    }
486
487    pub fn db_name(&self) -> String {
488        self.db_url
489            .as_str()
490            .split('/')
491            .next_back()
492            .unwrap()
493            .to_string()
494    }
495
496    pub fn db_url(&self) -> String {
497        self.db_url.as_str().to_string()
498    }
499
500    pub fn db_pool_size(&self) -> u32 {
501        self.db_pool_size
502    }
503
504    pub fn server_address(&self) -> String {
505        format!("{}:{}", self.host, self.port)
506    }
507}
508
509impl ServiceConfig {
510    pub fn read(contents: &str) -> Result<Self, toml::de::Error> {
511        toml::de::from_str::<Self>(contents)
512    }
513
514    pub fn test_defaults() -> Self {
515        Self {
516            background_tasks: BackgroundTasksConfig::test_defaults(),
517            ..Default::default()
518        }
519    }
520}
521
522impl Limits {
523    /// Extract limits for the package resolver.
524    pub fn package_resolver_limits(&self) -> iota_package_resolver::Limits {
525        iota_package_resolver::Limits {
526            max_type_argument_depth: self.max_type_argument_depth as usize,
527            max_type_argument_width: self.max_type_argument_width as usize,
528            max_type_nodes: self.max_type_nodes as usize,
529            max_move_value_depth: self.max_move_value_depth as usize,
530        }
531    }
532}
533
534impl BackgroundTasksConfig {
535    pub fn test_defaults() -> Self {
536        Self {
537            watermark_update_ms: 100, // Set to 100ms for testing
538        }
539    }
540}
541
542impl Default for Versions {
543    fn default() -> Self {
544        Self {
545            versions: vec![format!(
546                "{}.{}",
547                env!("CARGO_PKG_VERSION_MAJOR"),
548                env!("CARGO_PKG_VERSION_MINOR")
549            )],
550        }
551    }
552}
553
554impl Default for Ide {
555    fn default() -> Self {
556        Self {
557            ide_title: "IOTA GraphQL IDE".to_string(),
558        }
559    }
560}
561
562impl Default for ConnectionConfig {
563    fn default() -> Self {
564        Self {
565            port: 8000,
566            host: "127.0.0.1".to_string(),
567            db_url: DEFAULT_DB_URL.to_string().into(),
568            db_pool_size: 10,
569            prom_host: "0.0.0.0".to_string(),
570            prom_port: 9184,
571            skip_migration_consistency_check: false,
572            max_available_range: 9_000,
573        }
574    }
575}
576
577impl Default for Limits {
578    fn default() -> Self {
579        // Picked so that TS SDK shim layer queries all pass limit.
580        // TODO: calculate proper cost limits
581        Self {
582            max_query_depth: 20,
583            max_query_nodes: 300,
584            max_output_nodes: 100_000,
585            max_query_payload_size: 5_000,
586            max_db_query_cost: 20_000,
587            default_page_size: DEFAULT_PAGE_SIZE,
588            max_page_size: MAX_PAGE_SIZE,
589            // This default was picked as the sum of pre- and post- quorum timeouts from
590            // [`iota_core::authority_aggregator::TimeoutConfig`], with a 10% buffer.
591            //
592            // <https://github.com/iotaledger/iota/blob/eaf05fe5d293c06e3a2dfc22c87ba2aef419d8ea/crates/iota-core/src/authority_aggregator.rs#L84-L85>
593            mutation_timeout_ms: 74_000,
594            request_timeout_ms: 40_000,
595            // The following limits reflect the max values set in ProtocolConfig, at time of
596            // writing. <https://github.com/iotaledger/iota/blob/333f87061f0656607b1928aba423fa14ca16899e/crates/iota-protocol-config/src/lib.rs#L1580>
597            max_type_argument_depth: 16,
598            // <https://github.com/iotaledger/iota/blob/4b934f87acae862cecbcbefb3da34cabb79805aa/crates/iota-protocol-config/src/lib.rs#L1618>
599            max_type_argument_width: 32,
600            // <https://github.com/iotaledger/iota/blob/4b934f87acae862cecbcbefb3da34cabb79805aa/crates/iota-protocol-config/src/lib.rs#L1622>
601            max_type_nodes: 256,
602            // <https://github.com/iotaledger/iota/blob/4b934f87acae862cecbcbefb3da34cabb79805aa/crates/iota-protocol-config/src/lib.rs#L1988>
603            max_move_value_depth: 128,
604            // Filter-specific limits, such as the number of transaction ids that can be specified
605            // for the `TransactionBlockFilter`.
606            max_transaction_ids: 1000,
607            max_scan_limit: 1_000_000,
608            // Protocol limit for max transaction bytes allowed + base64
609            // overhead (roughly 1/3 of the original string). This is rounded up.
610            //
611            // <https://github.com/iotaledger/iota/blob/29c410ac809dd7c71dbf0237a96f08d72b406e52/crates/iota-protocol-config/src/lib.rs#L1566>
612            max_tx_payload_size: (128u32 * 1024u32 * 4u32).div_ceil(3),
613            max_subscription_recovery_checkpoints: NonZeroU32::new(20_000)
614                .expect("value must be non zero"),
615            max_active_subscriptions: NonZeroU32::new(1000).expect("value must be non zero"),
616        }
617    }
618}
619
620impl Default for InternalFeatureConfig {
621    fn default() -> Self {
622        Self {
623            query_limits_checker: true,
624            directive_checker: true,
625            feature_gate: true,
626            logger: true,
627            query_timeout: true,
628            metrics: true,
629            tracing: false,
630            apollo_tracing: false,
631            open_telemetry: false,
632        }
633    }
634}
635
636impl Default for BackgroundTasksConfig {
637    fn default() -> Self {
638        Self {
639            watermark_update_ms: 500,
640        }
641    }
642}
643
644impl Display for Version {
645    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
646        write!(f, "{}", self.full)
647    }
648}
649
650#[cfg(test)]
651mod tests {
652    use super::*;
653
654    #[test]
655    fn test_read_empty_service_config() {
656        let actual = ServiceConfig::read("").unwrap();
657        let expect = ServiceConfig::default();
658        assert_eq!(actual, expect);
659    }
660
661    #[test]
662    fn test_read_limits_in_service_config() {
663        let actual = ServiceConfig::read(
664            r#" [limits]
665                max-query-depth = 100
666                max-query-nodes = 300
667                max-output-nodes = 200000
668                max-tx-payload-size = 174763
669                max-query-payload-size = 2000
670                max-db-query-cost = 50
671                default-page-size = 20
672                max-page-size = 50
673                mutation-timeout-ms = 74000
674                request-timeout-ms = 27000
675                max-type-argument-depth = 32
676                max-type-argument-width = 64
677                max-type-nodes = 128
678                max-move-value-depth = 256
679                max-transaction-ids = 11
680                max-scan-limit = 50
681                max-subscription-recovery-checkpoints = 500
682                max-active-subscriptions = 20
683            "#,
684        )
685        .unwrap();
686
687        let expect = ServiceConfig {
688            limits: Limits {
689                max_query_depth: 100,
690                max_query_nodes: 300,
691                max_output_nodes: 200000,
692                max_tx_payload_size: 174763,
693                max_query_payload_size: 2000,
694                max_db_query_cost: 50,
695                default_page_size: 20,
696                max_page_size: 50,
697                mutation_timeout_ms: 74_000,
698                request_timeout_ms: 27_000,
699                max_type_argument_depth: 32,
700                max_type_argument_width: 64,
701                max_type_nodes: 128,
702                max_move_value_depth: 256,
703                max_transaction_ids: 11,
704                max_scan_limit: 50,
705                max_subscription_recovery_checkpoints: NonZeroU32::new(500).unwrap(),
706                max_active_subscriptions: NonZeroU32::new(20).unwrap(),
707            },
708            ..Default::default()
709        };
710
711        assert_eq!(actual, expect)
712    }
713
714    #[test]
715    fn test_read_enabled_features_in_service_config() {
716        let actual = ServiceConfig::read(
717            r#" disabled-features = [
718                  "coins",
719                ]
720            "#,
721        )
722        .unwrap();
723
724        use FunctionalGroup as G;
725        let expect = ServiceConfig {
726            disabled_features: BTreeSet::from([G::Coins]),
727            ..Default::default()
728        };
729
730        assert_eq!(actual, expect)
731    }
732
733    #[test]
734    fn test_read_experiments_in_service_config() {
735        let actual = ServiceConfig::read(
736            r#" [experiments]
737                test-flag = true
738            "#,
739        )
740        .unwrap();
741
742        let expect = ServiceConfig {
743            experiments: Experiments { test_flag: true },
744            ..Default::default()
745        };
746
747        assert_eq!(actual, expect)
748    }
749
750    #[test]
751    fn test_read_everything_in_service_config() {
752        let actual = ServiceConfig::read(
753            r#" disabled-features = ["analytics"]
754
755                [limits]
756                max-query-depth = 42
757                max-query-nodes = 320
758                max-output-nodes = 200000
759                max-tx-payload-size = 181017
760                max-query-payload-size = 200
761                max-db-query-cost = 20
762                default-page-size = 10
763                max-page-size = 20
764                mutation-timeout-ms = 74000
765                request-timeout-ms = 30000
766                max-type-argument-depth = 32
767                max-type-argument-width = 64
768                max-type-nodes = 128
769                max-move-value-depth = 256
770                max-transaction-ids = 42
771                max-scan-limit = 420
772                max-subscription-recovery-checkpoints = 4200
773                max-active-subscriptions = 42
774
775                [experiments]
776                test-flag = true
777            "#,
778        )
779        .unwrap();
780
781        let expect = ServiceConfig {
782            limits: Limits {
783                max_query_depth: 42,
784                max_query_nodes: 320,
785                max_output_nodes: 200000,
786                max_tx_payload_size: 181017,
787                max_query_payload_size: 200,
788                max_db_query_cost: 20,
789                default_page_size: 10,
790                max_page_size: 20,
791                mutation_timeout_ms: 74_000,
792                request_timeout_ms: 30_000,
793                max_type_argument_depth: 32,
794                max_type_argument_width: 64,
795                max_type_nodes: 128,
796                max_move_value_depth: 256,
797                max_transaction_ids: 42,
798                max_scan_limit: 420,
799                max_subscription_recovery_checkpoints: NonZeroU32::new(4200).unwrap(),
800                max_active_subscriptions: NonZeroU32::new(42).unwrap(),
801            },
802            disabled_features: BTreeSet::from([FunctionalGroup::Analytics]),
803            experiments: Experiments { test_flag: true },
804            ..Default::default()
805        };
806
807        assert_eq!(actual, expect);
808    }
809
810    #[test]
811    fn test_read_partial_in_service_config() {
812        let actual = ServiceConfig::read(
813            r#" disabled-features = ["analytics"]
814
815                [limits]
816                max-query-depth = 42
817                max-query-nodes = 320
818            "#,
819        )
820        .unwrap();
821
822        // When reading partially, the other parts will come from the default
823        // implementation.
824        let expect = ServiceConfig {
825            limits: Limits {
826                max_query_depth: 42,
827                max_query_nodes: 320,
828                ..Default::default()
829            },
830            disabled_features: BTreeSet::from([FunctionalGroup::Analytics]),
831            ..Default::default()
832        };
833
834        assert_eq!(actual, expect);
835    }
836
837    #[test]
838    fn test_server_config_debug_hides_db_password() {
839        let config = ServerConfig {
840            connection: ConnectionConfig {
841                db_url: "postgres://user:hunter2@localhost:5432/iota_indexer"
842                    .to_string()
843                    .into(),
844                ..Default::default()
845            },
846            ..Default::default()
847        };
848
849        let printed = format!("{config:#?}");
850        assert!(!printed.contains("hunter2"), "{printed}");
851        assert!(printed.contains("****"), "{printed}");
852    }
853}