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