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