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