1use std::{collections::BTreeSet, fmt::Display, num::NonZeroU32};
6
7use async_graphql::*;
8use iota_graphql_config::GraphQLConfig;
9use iota_indexer::db::DbUrl;
10use iota_names::config::IotaNamesConfig;
11use serde::{Deserialize, Serialize};
12use url::Url;
13
14use crate::{functional_group::FunctionalGroup, types::int::try_into_int};
15
16const DEFAULT_DB_URL: &str = "postgres://postgres:postgrespw@localhost:5432/iota_indexer";
17
18pub(crate) const DEFAULT_PAGE_SIZE: u32 = 20;
19pub(crate) const MAX_PAGE_SIZE: u32 = 50;
20
21#[GraphQLConfig]
23#[derive(Default)]
24pub struct ServerConfig {
25 pub service: ServiceConfig,
26 pub connection: ConnectionConfig,
27 pub internal_features: InternalFeatureConfig,
28 pub tx_exec_full_node: TxExecFullNodeConfig,
29 pub ide: Ide,
30 pub historic_fallback: HistoricFallbackOptions,
31}
32
33#[GraphQLConfig]
38#[derive(clap::Args, Clone, Eq, PartialEq)]
39pub struct ConnectionConfig {
40 #[arg(short, long, default_value_t = ConnectionConfig::default().port)]
42 pub port: u16,
43 #[arg(long, default_value_t = ConnectionConfig::default().host)]
45 pub host: String,
46 #[arg(short, long, default_value = DEFAULT_DB_URL)]
48 pub db_url: DbUrl,
49 #[arg(long, default_value_t = ConnectionConfig::default().db_pool_size)]
51 pub db_pool_size: u32,
52 #[arg(long, default_value_t = ConnectionConfig::default().prom_host)]
54 pub prom_host: String,
55 #[arg(long, default_value_t = ConnectionConfig::default().prom_port)]
57 pub prom_port: u16,
58 #[arg(long, default_value_t = ConnectionConfig::default().skip_migration_consistency_check)]
61 pub skip_migration_consistency_check: bool,
62 #[arg(
67 long,
68 default_value_t = ConnectionConfig::default().max_available_range,
69 env = "MAX_AVAILABLE_RANGE",
70 )]
71 pub max_available_range: u64,
72}
73
74#[GraphQLConfig]
77#[derive(clap::Args, Debug, Clone)]
78pub struct HistoricFallbackOptions {
79 #[arg(
80 long,
81 help = "Experimental: REST KV store URL for historic fallback. Depends on the iota-rest-kv API which is still being finalized."
82 )]
83 pub fallback_kv_url: Option<Url>,
84
85 #[arg(
86 long,
87 default_value_t = HistoricFallbackOptions::DEFAULT_MULTI_FETCH_BATCH_SIZE,
88 env = "FALLBACK_KV_MULTI_FETCH_BATCH_SIZE",
89 help = "Experimental: Maximum number of keys per batch request to fallback KV store."
90 )]
91 pub fallback_kv_multi_fetch_batch_size: usize,
92
93 #[arg(
94 long,
95 default_value_t = HistoricFallbackOptions::DEFAULT_CONCURRENT_FETCHES,
96 env = "FALLBACK_KV_CONCURRENT_FETCHES",
97 help = "Experimental: Maximum number of concurrent batch requests to fallback KV store."
98 )]
99 pub fallback_kv_concurrent_fetches: usize,
100
101 #[arg(
102 long,
103 default_value_t = HistoricFallbackOptions::DEFAULT_CACHE_SIZE,
104 env = "FALLBACK_KV_CACHE_SIZE",
105 help = "Experimental: Cache size for historic fallback."
106 )]
107 pub fallback_kv_cache_size: u64,
108
109 #[arg(
110 long,
111 default_value_t = HistoricFallbackOptions::DEFAULT_MAX_RESPONSE_SIZE_BYTES,
112 env = "FALLBACK_KV_MAX_RESPONSE_SIZE_BYTES",
113 help = "Experimental: Maximum size in bytes of a single response body accepted from the fallback KV store. Larger responses are rejected."
114 )]
115 pub fallback_kv_max_response_size_bytes: usize,
116}
117
118impl HistoricFallbackOptions {
119 pub const DEFAULT_MULTI_FETCH_BATCH_SIZE: usize = 100;
120 pub const DEFAULT_CONCURRENT_FETCHES: usize = 10;
121 pub const DEFAULT_CACHE_SIZE: u64 = 100_000;
122 pub const DEFAULT_MAX_RESPONSE_SIZE_BYTES: usize = 128 * 1024 * 1024;
123}
124
125impl Default for HistoricFallbackOptions {
126 fn default() -> Self {
127 Self {
128 fallback_kv_url: None,
129 fallback_kv_multi_fetch_batch_size: Self::DEFAULT_MULTI_FETCH_BATCH_SIZE,
130 fallback_kv_concurrent_fetches: Self::DEFAULT_CONCURRENT_FETCHES,
131 fallback_kv_cache_size: Self::DEFAULT_CACHE_SIZE,
132 fallback_kv_max_response_size_bytes: Self::DEFAULT_MAX_RESPONSE_SIZE_BYTES,
133 }
134 }
135}
136
137#[GraphQLConfig]
141#[derive(Default)]
142pub struct ServiceConfig {
143 pub versions: Versions,
144 pub limits: Limits,
145 pub disabled_features: BTreeSet<FunctionalGroup>,
146 pub experiments: Experiments,
147 pub iota_names: IotaNamesConfig,
148 pub background_tasks: BackgroundTasksConfig,
149}
150
151#[GraphQLConfig]
152pub struct Versions {
153 versions: Vec<String>,
154}
155
156#[GraphQLConfig]
157pub struct Limits {
158 pub max_query_depth: u32,
160 pub max_query_nodes: u32,
162 pub max_output_nodes: u32,
164 pub max_tx_payload_size: u32,
168 pub max_query_payload_size: u32,
171 pub max_db_query_cost: u32,
175 pub default_page_size: u32,
178 pub max_page_size: u32,
180 pub mutation_timeout_ms: u32,
185 pub request_timeout_ms: u32,
189 pub max_type_argument_depth: u32,
192 pub max_type_argument_width: u32,
194 pub max_type_nodes: u32,
196 pub max_move_value_depth: u32,
198 pub max_transaction_ids: u32,
201 pub max_scan_limit: u32,
203 pub max_subscription_recovery_checkpoints: NonZeroU32,
209 pub max_active_subscriptions: NonZeroU32,
212}
213
214#[GraphQLConfig]
215#[derive(Copy)]
216pub struct BackgroundTasksConfig {
217 pub watermark_update_ms: u64,
220}
221
222#[derive(Copy, Clone, Debug)]
226pub struct Version {
227 pub year: &'static str,
229 pub month: &'static str,
231 pub patch: &'static str,
234 pub sha: &'static str,
236 pub full: &'static str,
240}
241
242impl Version {
243 pub fn for_testing() -> Self {
245 Self {
246 year: env!("CARGO_PKG_VERSION_MAJOR"),
247 month: env!("CARGO_PKG_VERSION_MINOR"),
248 patch: env!("CARGO_PKG_VERSION_PATCH"),
249 sha: "testing-no-sha",
250 full: const_str::concat!(
252 env!("CARGO_PKG_VERSION_MAJOR"),
253 ".",
254 env!("CARGO_PKG_VERSION_MINOR"),
255 ".",
256 env!("CARGO_PKG_VERSION_PATCH"),
257 "-testing-no-sha"
258 ),
259 }
260 }
261}
262
263#[GraphQLConfig]
264#[derive(clap::Args)]
265pub struct Ide {
266 #[arg(short, long, default_value_t = Ide::default().ide_title)]
268 pub(crate) ide_title: String,
269}
270
271#[GraphQLConfig]
272#[derive(Default)]
273pub struct Experiments {
274 #[cfg(test)]
277 test_flag: bool,
278}
279
280#[GraphQLConfig]
281pub struct InternalFeatureConfig {
282 pub(crate) query_limits_checker: bool,
283 pub(crate) directive_checker: bool,
284 pub(crate) feature_gate: bool,
285 pub(crate) logger: bool,
286 pub(crate) query_timeout: bool,
287 pub(crate) metrics: bool,
288 pub(crate) tracing: bool,
289 pub(crate) apollo_tracing: bool,
290 pub(crate) open_telemetry: bool,
291}
292
293#[GraphQLConfig]
294#[derive(clap::Args, Default)]
295pub struct TxExecFullNodeConfig {
296 #[arg(long)]
298 pub(crate) node_rpc_url: Option<String>,
299}
300
301#[Object]
303impl ServiceConfig {
304 async fn is_enabled(&self, feature: FunctionalGroup) -> bool {
306 !self.disabled_features.contains(&feature)
307 }
308
309 async fn available_versions(&self) -> Vec<String> {
311 self.versions.versions.clone()
312 }
313
314 async fn enabled_features(&self) -> Vec<FunctionalGroup> {
316 FunctionalGroup::all()
317 .iter()
318 .filter(|g| !self.disabled_features.contains(g))
319 .copied()
320 .collect()
321 }
322
323 pub async fn max_query_depth(&self) -> Result<i32> {
325 try_into_int(self.limits.max_query_depth).extend()
326 }
327
328 pub async fn max_query_nodes(&self) -> Result<i32> {
331 try_into_int(self.limits.max_query_nodes).extend()
332 }
333
334 pub async fn max_output_nodes(&self) -> Result<i32> {
347 try_into_int(self.limits.max_output_nodes).extend()
348 }
349
350 async fn max_db_query_cost(&self) -> Result<i32> {
354 try_into_int(self.limits.max_db_query_cost).extend()
355 }
356
357 async fn default_page_size(&self) -> Result<i32> {
359 try_into_int(self.limits.default_page_size).extend()
360 }
361
362 async fn max_page_size(&self) -> Result<i32> {
364 try_into_int(self.limits.max_page_size).extend()
365 }
366
367 async fn mutation_timeout_ms(&self) -> Result<i32> {
374 try_into_int(self.limits.mutation_timeout_ms).extend()
375 }
376
377 async fn request_timeout_ms(&self) -> Result<i32> {
380 try_into_int(self.limits.request_timeout_ms).extend()
381 }
382
383 async fn max_transaction_payload_size(&self) -> Result<i32> {
393 try_into_int(self.limits.max_tx_payload_size).extend()
394 }
395
396 async fn max_query_payload_size(&self) -> Result<i32> {
401 try_into_int(self.limits.max_query_payload_size).extend()
402 }
403
404 async fn max_type_argument_depth(&self) -> Result<i32> {
407 try_into_int(self.limits.max_type_argument_depth).extend()
408 }
409
410 async fn max_type_argument_width(&self) -> Result<i32> {
413 try_into_int(self.limits.max_type_argument_width).extend()
414 }
415
416 async fn max_type_nodes(&self) -> Result<i32> {
419 try_into_int(self.limits.max_type_nodes).extend()
420 }
421
422 async fn max_move_value_depth(&self) -> Result<i32> {
425 try_into_int(self.limits.max_move_value_depth).extend()
426 }
427
428 async fn max_transaction_ids(&self) -> Result<i32> {
431 try_into_int(self.limits.max_transaction_ids).extend()
432 }
433
434 async fn max_scan_limit(&self) -> Result<i32> {
436 try_into_int(self.limits.max_scan_limit).extend()
437 }
438}
439
440impl ConnectionConfig {
441 pub fn new(
442 port: Option<u16>,
443 host: Option<String>,
444 db_url: Option<String>,
445 db_pool_size: Option<u32>,
446 prom_host: Option<String>,
447 prom_port: Option<u16>,
448 skip_migration_consistency_check: Option<bool>,
449 max_available_range: Option<u64>,
450 ) -> Self {
451 let default = Self::default();
452 Self {
453 port: port.unwrap_or(default.port),
454 host: host.unwrap_or(default.host),
455 db_url: db_url.map_or(default.db_url, DbUrl::from),
456 db_pool_size: db_pool_size.unwrap_or(default.db_pool_size),
457 prom_host: prom_host.unwrap_or(default.prom_host),
458 prom_port: prom_port.unwrap_or(default.prom_port),
459 skip_migration_consistency_check: skip_migration_consistency_check
460 .unwrap_or(default.skip_migration_consistency_check),
461 max_available_range: max_available_range.unwrap_or(default.max_available_range),
462 }
463 }
464
465 pub fn ci_integration_test_cfg() -> Self {
466 Self {
467 db_url: "postgres://postgres:postgrespw@localhost:5432/iota_graphql_rpc_e2e_tests"
468 .to_string()
469 .into(),
470 ..Default::default()
471 }
472 }
473
474 pub fn ci_integration_test_cfg_with_db_name(
475 db_name: String,
476 port: u16,
477 prom_port: u16,
478 ) -> Self {
479 Self {
480 db_url: format!("postgres://postgres:postgrespw@localhost:5432/{db_name}").into(),
481 port,
482 prom_port,
483 ..Default::default()
484 }
485 }
486
487 pub fn db_name(&self) -> String {
488 self.db_url
489 .as_str()
490 .split('/')
491 .next_back()
492 .unwrap()
493 .to_string()
494 }
495
496 pub fn db_url(&self) -> String {
497 self.db_url.as_str().to_string()
498 }
499
500 pub fn db_pool_size(&self) -> u32 {
501 self.db_pool_size
502 }
503
504 pub fn server_address(&self) -> String {
505 format!("{}:{}", self.host, self.port)
506 }
507}
508
509impl ServiceConfig {
510 pub fn read(contents: &str) -> Result<Self, toml::de::Error> {
511 toml::de::from_str::<Self>(contents)
512 }
513
514 pub fn test_defaults() -> Self {
515 Self {
516 background_tasks: BackgroundTasksConfig::test_defaults(),
517 ..Default::default()
518 }
519 }
520}
521
522impl Limits {
523 pub fn package_resolver_limits(&self) -> iota_package_resolver::Limits {
525 iota_package_resolver::Limits {
526 max_type_argument_depth: self.max_type_argument_depth as usize,
527 max_type_argument_width: self.max_type_argument_width as usize,
528 max_type_nodes: self.max_type_nodes as usize,
529 max_move_value_depth: self.max_move_value_depth as usize,
530 }
531 }
532}
533
534impl BackgroundTasksConfig {
535 pub fn test_defaults() -> Self {
536 Self {
537 watermark_update_ms: 100, }
539 }
540}
541
542impl Default for Versions {
543 fn default() -> Self {
544 Self {
545 versions: vec![format!(
546 "{}.{}",
547 env!("CARGO_PKG_VERSION_MAJOR"),
548 env!("CARGO_PKG_VERSION_MINOR")
549 )],
550 }
551 }
552}
553
554impl Default for Ide {
555 fn default() -> Self {
556 Self {
557 ide_title: "IOTA GraphQL IDE".to_string(),
558 }
559 }
560}
561
562impl Default for ConnectionConfig {
563 fn default() -> Self {
564 Self {
565 port: 8000,
566 host: "127.0.0.1".to_string(),
567 db_url: DEFAULT_DB_URL.to_string().into(),
568 db_pool_size: 10,
569 prom_host: "0.0.0.0".to_string(),
570 prom_port: 9184,
571 skip_migration_consistency_check: false,
572 max_available_range: 9_000,
573 }
574 }
575}
576
577impl Default for Limits {
578 fn default() -> Self {
579 Self {
582 max_query_depth: 20,
583 max_query_nodes: 300,
584 max_output_nodes: 100_000,
585 max_query_payload_size: 5_000,
586 max_db_query_cost: 20_000,
587 default_page_size: DEFAULT_PAGE_SIZE,
588 max_page_size: MAX_PAGE_SIZE,
589 mutation_timeout_ms: 74_000,
594 request_timeout_ms: 40_000,
595 max_type_argument_depth: 16,
598 max_type_argument_width: 32,
600 max_type_nodes: 256,
602 max_move_value_depth: 128,
604 max_transaction_ids: 1000,
607 max_scan_limit: 1_000_000,
608 max_tx_payload_size: (128u32 * 1024u32 * 4u32).div_ceil(3),
613 max_subscription_recovery_checkpoints: NonZeroU32::new(20_000)
614 .expect("value must be non zero"),
615 max_active_subscriptions: NonZeroU32::new(1000).expect("value must be non zero"),
616 }
617 }
618}
619
620impl Default for InternalFeatureConfig {
621 fn default() -> Self {
622 Self {
623 query_limits_checker: true,
624 directive_checker: true,
625 feature_gate: true,
626 logger: true,
627 query_timeout: true,
628 metrics: true,
629 tracing: false,
630 apollo_tracing: false,
631 open_telemetry: false,
632 }
633 }
634}
635
636impl Default for BackgroundTasksConfig {
637 fn default() -> Self {
638 Self {
639 watermark_update_ms: 500,
640 }
641 }
642}
643
644impl Display for Version {
645 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
646 write!(f, "{}", self.full)
647 }
648}
649
650#[cfg(test)]
651mod tests {
652 use super::*;
653
654 #[test]
655 fn test_read_empty_service_config() {
656 let actual = ServiceConfig::read("").unwrap();
657 let expect = ServiceConfig::default();
658 assert_eq!(actual, expect);
659 }
660
661 #[test]
662 fn test_read_limits_in_service_config() {
663 let actual = ServiceConfig::read(
664 r#" [limits]
665 max-query-depth = 100
666 max-query-nodes = 300
667 max-output-nodes = 200000
668 max-tx-payload-size = 174763
669 max-query-payload-size = 2000
670 max-db-query-cost = 50
671 default-page-size = 20
672 max-page-size = 50
673 mutation-timeout-ms = 74000
674 request-timeout-ms = 27000
675 max-type-argument-depth = 32
676 max-type-argument-width = 64
677 max-type-nodes = 128
678 max-move-value-depth = 256
679 max-transaction-ids = 11
680 max-scan-limit = 50
681 max-subscription-recovery-checkpoints = 500
682 max-active-subscriptions = 20
683 "#,
684 )
685 .unwrap();
686
687 let expect = ServiceConfig {
688 limits: Limits {
689 max_query_depth: 100,
690 max_query_nodes: 300,
691 max_output_nodes: 200000,
692 max_tx_payload_size: 174763,
693 max_query_payload_size: 2000,
694 max_db_query_cost: 50,
695 default_page_size: 20,
696 max_page_size: 50,
697 mutation_timeout_ms: 74_000,
698 request_timeout_ms: 27_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: 11,
704 max_scan_limit: 50,
705 max_subscription_recovery_checkpoints: NonZeroU32::new(500).unwrap(),
706 max_active_subscriptions: NonZeroU32::new(20).unwrap(),
707 },
708 ..Default::default()
709 };
710
711 assert_eq!(actual, expect)
712 }
713
714 #[test]
715 fn test_read_enabled_features_in_service_config() {
716 let actual = ServiceConfig::read(
717 r#" disabled-features = [
718 "coins",
719 ]
720 "#,
721 )
722 .unwrap();
723
724 use FunctionalGroup as G;
725 let expect = ServiceConfig {
726 disabled_features: BTreeSet::from([G::Coins]),
727 ..Default::default()
728 };
729
730 assert_eq!(actual, expect)
731 }
732
733 #[test]
734 fn test_read_experiments_in_service_config() {
735 let actual = ServiceConfig::read(
736 r#" [experiments]
737 test-flag = true
738 "#,
739 )
740 .unwrap();
741
742 let expect = ServiceConfig {
743 experiments: Experiments { test_flag: true },
744 ..Default::default()
745 };
746
747 assert_eq!(actual, expect)
748 }
749
750 #[test]
751 fn test_read_everything_in_service_config() {
752 let actual = ServiceConfig::read(
753 r#" disabled-features = ["analytics"]
754
755 [limits]
756 max-query-depth = 42
757 max-query-nodes = 320
758 max-output-nodes = 200000
759 max-tx-payload-size = 181017
760 max-query-payload-size = 200
761 max-db-query-cost = 20
762 default-page-size = 10
763 max-page-size = 20
764 mutation-timeout-ms = 74000
765 request-timeout-ms = 30000
766 max-type-argument-depth = 32
767 max-type-argument-width = 64
768 max-type-nodes = 128
769 max-move-value-depth = 256
770 max-transaction-ids = 42
771 max-scan-limit = 420
772 max-subscription-recovery-checkpoints = 4200
773 max-active-subscriptions = 42
774
775 [experiments]
776 test-flag = true
777 "#,
778 )
779 .unwrap();
780
781 let expect = ServiceConfig {
782 limits: Limits {
783 max_query_depth: 42,
784 max_query_nodes: 320,
785 max_output_nodes: 200000,
786 max_tx_payload_size: 181017,
787 max_query_payload_size: 200,
788 max_db_query_cost: 20,
789 default_page_size: 10,
790 max_page_size: 20,
791 mutation_timeout_ms: 74_000,
792 request_timeout_ms: 30_000,
793 max_type_argument_depth: 32,
794 max_type_argument_width: 64,
795 max_type_nodes: 128,
796 max_move_value_depth: 256,
797 max_transaction_ids: 42,
798 max_scan_limit: 420,
799 max_subscription_recovery_checkpoints: NonZeroU32::new(4200).unwrap(),
800 max_active_subscriptions: NonZeroU32::new(42).unwrap(),
801 },
802 disabled_features: BTreeSet::from([FunctionalGroup::Analytics]),
803 experiments: Experiments { test_flag: true },
804 ..Default::default()
805 };
806
807 assert_eq!(actual, expect);
808 }
809
810 #[test]
811 fn test_read_partial_in_service_config() {
812 let actual = ServiceConfig::read(
813 r#" disabled-features = ["analytics"]
814
815 [limits]
816 max-query-depth = 42
817 max-query-nodes = 320
818 "#,
819 )
820 .unwrap();
821
822 let expect = ServiceConfig {
825 limits: Limits {
826 max_query_depth: 42,
827 max_query_nodes: 320,
828 ..Default::default()
829 },
830 disabled_features: BTreeSet::from([FunctionalGroup::Analytics]),
831 ..Default::default()
832 };
833
834 assert_eq!(actual, expect);
835 }
836
837 #[test]
838 fn test_server_config_debug_hides_db_password() {
839 let config = ServerConfig {
840 connection: ConnectionConfig {
841 db_url: "postgres://user:hunter2@localhost:5432/iota_indexer"
842 .to_string()
843 .into(),
844 ..Default::default()
845 },
846 ..Default::default()
847 };
848
849 let printed = format!("{config:#?}");
850 assert!(!printed.contains("hunter2"), "{printed}");
851 assert!(printed.contains("****"), "{printed}");
852 }
853}