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};
11
12use crate::functional_group::FunctionalGroup;
13
14pub(crate) const DEFAULT_PAGE_SIZE: u32 = 20;
15pub(crate) const MAX_PAGE_SIZE: u32 = 50;
16
17/// The combination of all configurations for the GraphQL service.
18#[GraphQLConfig]
19#[derive(Default)]
20pub struct ServerConfig {
21    pub service: ServiceConfig,
22    pub connection: ConnectionConfig,
23    pub internal_features: InternalFeatureConfig,
24    pub tx_exec_full_node: TxExecFullNodeConfig,
25    pub ide: Ide,
26}
27
28/// Configuration for connections for the RPC, passed in as command-line
29/// arguments. This configures specific connections between this service and
30/// other services, and might differ from instance to instance of the GraphQL
31/// service.
32#[GraphQLConfig]
33#[derive(clap::Args, Clone, Eq, PartialEq)]
34pub struct ConnectionConfig {
35    /// Port to bind the server to
36    #[arg(short, long, default_value_t = ConnectionConfig::default().port)]
37    pub port: u16,
38    /// Host to bind the server to
39    #[arg(long, default_value_t = ConnectionConfig::default().host)]
40    pub host: String,
41    /// DB URL for data fetching
42    #[arg(short, long, default_value_t = ConnectionConfig::default().db_url)]
43    pub db_url: String,
44    /// Pool size for DB connections
45    #[arg(long, default_value_t = ConnectionConfig::default().db_pool_size)]
46    pub db_pool_size: u32,
47    /// Host to bind the prom server to
48    #[arg(long, default_value_t = ConnectionConfig::default().prom_host)]
49    pub prom_host: String,
50    /// Port to bind the prom server to
51    #[arg(long, default_value_t = ConnectionConfig::default().prom_port)]
52    pub prom_port: u16,
53    /// Skip checking whether the service is compatible with the DB it is about
54    /// to connect to, on start-up.
55    #[arg(long, default_value_t = ConnectionConfig::default().skip_migration_consistency_check)]
56    pub skip_migration_consistency_check: bool,
57    /// Maximum number of checkpoints to look back for consistent view queries.
58    /// Directly influences the `availableRange` size. Larger values let
59    /// pagination cursors stay valid for longer, downside is that older cursors
60    /// have higher DB cost.
61    #[arg(
62        long,
63        default_value_t = ConnectionConfig::default().max_available_range,
64        env = "MAX_AVAILABLE_RANGE",
65    )]
66    pub max_available_range: u64,
67}
68
69/// Configuration on features supported by the GraphQL service, passed in a
70/// TOML-based file. These configurations are shared across fleets of the
71/// service, i.e. all testnet services will have the same `ServiceConfig`.
72#[GraphQLConfig]
73#[derive(Default)]
74pub struct ServiceConfig {
75    pub versions: Versions,
76    pub limits: Limits,
77    pub disabled_features: BTreeSet<FunctionalGroup>,
78    pub experiments: Experiments,
79    pub iota_names: IotaNamesConfig,
80    pub background_tasks: BackgroundTasksConfig,
81}
82
83#[GraphQLConfig]
84pub struct Versions {
85    versions: Vec<String>,
86}
87
88#[GraphQLConfig]
89pub struct Limits {
90    /// Maximum depth of nodes in the requests.
91    pub max_query_depth: u32,
92    /// Maximum number of nodes in the requests.
93    pub max_query_nodes: u32,
94    /// Maximum number of output nodes allowed in the response.
95    pub max_output_nodes: u32,
96    /// Maximum size in bytes allowed for the `txBytes` and `signatures` fields
97    /// of a GraphQL mutation request in the `executeTransactionBlock` node,
98    /// and for the `txBytes` of a `dryRunTransactionBlock` node.
99    pub max_tx_payload_size: u32,
100    /// Maximum size in bytes of the JSON payload of a GraphQL read request
101    /// (excluding `max_tx_payload_size`).
102    pub max_query_payload_size: u32,
103    /// Queries whose EXPLAIN cost are more than this will be logged. Given in
104    /// the units used by the database (where 1.0 is roughly the cost of a
105    /// sequential page access).
106    pub max_db_query_cost: u32,
107    /// Paginated queries will return this many elements if a page size is not
108    /// provided.
109    pub default_page_size: u32,
110    /// Paginated queries can return at most this many elements.
111    pub max_page_size: u32,
112    /// Time (in milliseconds) to wait for a transaction to be executed and the
113    /// results returned from GraphQL. If the transaction takes longer than
114    /// this time to execute, the request will return a timeout error, but
115    /// the transaction may continue executing.
116    pub mutation_timeout_ms: u32,
117    /// Time (in milliseconds) to wait for a read request from the GraphQL
118    /// service. Requests that take longer than this time to return a result
119    /// will return a timeout error.
120    pub request_timeout_ms: u32,
121    /// Maximum amount of nesting among type arguments (type arguments nest when
122    /// a type argument is itself generic and has arguments).
123    pub max_type_argument_depth: u32,
124    /// Maximum number of type parameters a type can have.
125    pub max_type_argument_width: u32,
126    /// Maximum size of a fully qualified type.
127    pub max_type_nodes: u32,
128    /// Maximum deph of a move value.
129    pub max_move_value_depth: u32,
130    /// Maximum number of transaction ids that can be passed to a
131    /// `TransactionBlockFilter` or to `transaction_blocks_by_digests`.
132    pub max_transaction_ids: u32,
133    /// Maximum number of candidates to scan when gathering a page of results.
134    pub max_scan_limit: u32,
135}
136
137#[GraphQLConfig]
138#[derive(Copy)]
139pub struct BackgroundTasksConfig {
140    /// How often the watermark task checks the indexer database to update the
141    /// checkpoint and epoch watermarks.
142    pub watermark_update_ms: u64,
143}
144
145/// The Version of the service. `year.month` represents the major release.
146/// New `patch` versions represent backwards compatible fixes for their major
147/// release. The `full` version is `year.month.patch-sha`.
148#[derive(Copy, Clone, Debug)]
149pub struct Version {
150    /// The year of this release.
151    pub year: &'static str,
152    /// The month of this release.
153    pub month: &'static str,
154    /// The patch is a positive number incremented for every compatible release
155    /// on top of the major.month release.
156    pub patch: &'static str,
157    /// The commit sha for this release.
158    pub sha: &'static str,
159    /// The full version string.
160    /// Note that this extra field is used only for the uptime_metric function
161    /// which requires a &'static str.
162    pub full: &'static str,
163}
164
165impl Version {
166    /// Use for testing when you need the Version obj and a year.month &str
167    pub fn for_testing() -> Self {
168        Self {
169            year: env!("CARGO_PKG_VERSION_MAJOR"),
170            month: env!("CARGO_PKG_VERSION_MINOR"),
171            patch: env!("CARGO_PKG_VERSION_PATCH"),
172            sha: "testing-no-sha",
173            // note that this full field is needed for metrics but not for testing
174            full: const_str::concat!(
175                env!("CARGO_PKG_VERSION_MAJOR"),
176                ".",
177                env!("CARGO_PKG_VERSION_MINOR"),
178                ".",
179                env!("CARGO_PKG_VERSION_PATCH"),
180                "-testing-no-sha"
181            ),
182        }
183    }
184}
185
186#[GraphQLConfig]
187#[derive(clap::Args)]
188pub struct Ide {
189    /// The title to display at the top of the web-based GraphiQL IDE.
190    #[arg(short, long, default_value_t = Ide::default().ide_title)]
191    pub(crate) ide_title: String,
192}
193
194#[GraphQLConfig]
195#[derive(Default)]
196pub struct Experiments {
197    // Add experimental flags here, to provide access to them through-out the GraphQL
198    // implementation.
199    #[cfg(test)]
200    test_flag: bool,
201}
202
203#[GraphQLConfig]
204pub struct InternalFeatureConfig {
205    pub(crate) query_limits_checker: bool,
206    pub(crate) directive_checker: bool,
207    pub(crate) feature_gate: bool,
208    pub(crate) logger: bool,
209    pub(crate) query_timeout: bool,
210    pub(crate) metrics: bool,
211    pub(crate) tracing: bool,
212    pub(crate) apollo_tracing: bool,
213    pub(crate) open_telemetry: bool,
214}
215
216#[GraphQLConfig]
217#[derive(clap::Args, Default)]
218pub struct TxExecFullNodeConfig {
219    /// RPC URL for the fullnode to send transactions to execute and dry-run.
220    #[arg(long)]
221    pub(crate) node_rpc_url: Option<String>,
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        max_available_range: Option<u64>,
373    ) -> Self {
374        let default = Self::default();
375        Self {
376            port: port.unwrap_or(default.port),
377            host: host.unwrap_or(default.host),
378            db_url: db_url.unwrap_or(default.db_url),
379            db_pool_size: db_pool_size.unwrap_or(default.db_pool_size),
380            prom_host: prom_host.unwrap_or(default.prom_host),
381            prom_port: prom_port.unwrap_or(default.prom_port),
382            skip_migration_consistency_check: skip_migration_consistency_check
383                .unwrap_or(default.skip_migration_consistency_check),
384            max_available_range: max_available_range.unwrap_or(default.max_available_range),
385        }
386    }
387
388    pub fn ci_integration_test_cfg() -> Self {
389        Self {
390            db_url: "postgres://postgres:postgrespw@localhost:5432/iota_graphql_rpc_e2e_tests"
391                .to_string(),
392            ..Default::default()
393        }
394    }
395
396    pub fn ci_integration_test_cfg_with_db_name(
397        db_name: String,
398        port: u16,
399        prom_port: u16,
400    ) -> Self {
401        Self {
402            db_url: format!("postgres://postgres:postgrespw@localhost:5432/{db_name}"),
403            port,
404            prom_port,
405            ..Default::default()
406        }
407    }
408
409    pub fn db_name(&self) -> String {
410        self.db_url.split('/').next_back().unwrap().to_string()
411    }
412
413    pub fn db_url(&self) -> String {
414        self.db_url.clone()
415    }
416
417    pub fn db_pool_size(&self) -> u32 {
418        self.db_pool_size
419    }
420
421    pub fn server_address(&self) -> String {
422        format!("{}:{}", self.host, self.port)
423    }
424}
425
426impl ServiceConfig {
427    pub fn read(contents: &str) -> Result<Self, toml::de::Error> {
428        toml::de::from_str::<Self>(contents)
429    }
430
431    pub fn test_defaults() -> Self {
432        Self {
433            background_tasks: BackgroundTasksConfig::test_defaults(),
434            ..Default::default()
435        }
436    }
437}
438
439impl Limits {
440    /// Extract limits for the package resolver.
441    pub fn package_resolver_limits(&self) -> iota_package_resolver::Limits {
442        iota_package_resolver::Limits {
443            max_type_argument_depth: self.max_type_argument_depth as usize,
444            max_type_argument_width: self.max_type_argument_width as usize,
445            max_type_nodes: self.max_type_nodes as usize,
446            max_move_value_depth: self.max_move_value_depth as usize,
447        }
448    }
449}
450
451impl BackgroundTasksConfig {
452    pub fn test_defaults() -> Self {
453        Self {
454            watermark_update_ms: 100, // Set to 100ms for testing
455        }
456    }
457}
458
459impl Default for Versions {
460    fn default() -> Self {
461        Self {
462            versions: vec![format!(
463                "{}.{}",
464                env!("CARGO_PKG_VERSION_MAJOR"),
465                env!("CARGO_PKG_VERSION_MINOR")
466            )],
467        }
468    }
469}
470
471impl Default for Ide {
472    fn default() -> Self {
473        Self {
474            ide_title: "IOTA GraphQL IDE".to_string(),
475        }
476    }
477}
478
479impl Default for ConnectionConfig {
480    fn default() -> Self {
481        Self {
482            port: 8000,
483            host: "127.0.0.1".to_string(),
484            db_url: "postgres://postgres:postgrespw@localhost:5432/iota_indexer".to_string(),
485            db_pool_size: 10,
486            prom_host: "0.0.0.0".to_string(),
487            prom_port: 9184,
488            skip_migration_consistency_check: false,
489            max_available_range: 9_000,
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}