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