+272
-40
Cargo.lock
+272
-40
Cargo.lock
···
276
276
checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
277
277
278
278
[[package]]
279
+
name = "aws-lc-rs"
280
+
version = "1.13.1"
281
+
source = "registry+https://github.com/rust-lang/crates.io-index"
282
+
checksum = "93fcc8f365936c834db5514fc45aee5b1202d677e6b40e48468aaaa8183ca8c7"
283
+
dependencies = [
284
+
"aws-lc-sys",
285
+
"zeroize",
286
+
]
287
+
288
+
[[package]]
289
+
name = "aws-lc-sys"
290
+
version = "0.29.0"
291
+
source = "registry+https://github.com/rust-lang/crates.io-index"
292
+
checksum = "61b1d86e7705efe1be1b569bab41d4fa1e14e220b60a160f78de2db687add079"
293
+
dependencies = [
294
+
"bindgen 0.69.5",
295
+
"cc",
296
+
"cmake",
297
+
"dunce",
298
+
"fs_extra",
299
+
]
300
+
301
+
[[package]]
279
302
name = "axum"
280
303
version = "0.8.3"
281
304
source = "registry+https://github.com/rust-lang/crates.io-index"
···
450
473
"itertools 0.12.1",
451
474
"lazy_static",
452
475
"lazycell",
476
+
"log",
477
+
"prettyplease",
453
478
"proc-macro2",
454
479
"quote",
455
480
"regex",
456
481
"rustc-hash 1.1.0",
457
482
"shlex",
458
483
"syn",
484
+
"which",
459
485
]
460
486
461
487
[[package]]
···
654
680
655
681
[[package]]
656
682
name = "clap"
657
-
version = "4.5.35"
683
+
version = "4.5.40"
658
684
source = "registry+https://github.com/rust-lang/crates.io-index"
659
-
checksum = "d8aa86934b44c19c50f87cc2790e19f54f7a67aedb64101c2e1a2e5ecfb73944"
685
+
checksum = "40b6887a1d8685cebccf115538db5c0efe625ccac9696ad45c409d96566e910f"
660
686
dependencies = [
661
687
"clap_builder",
662
688
"clap_derive",
···
664
690
665
691
[[package]]
666
692
name = "clap_builder"
667
-
version = "4.5.35"
693
+
version = "4.5.40"
668
694
source = "registry+https://github.com/rust-lang/crates.io-index"
669
-
checksum = "2414dbb2dd0695280da6ea9261e327479e9d37b0630f6b53ba2a11c60c679fd9"
695
+
checksum = "e0c66c08ce9f0c698cbce5c0279d0bb6ac936d8674174fe48f736533b964f59e"
670
696
dependencies = [
671
697
"anstream",
672
698
"anstyle",
···
676
702
677
703
[[package]]
678
704
name = "clap_derive"
679
-
version = "4.5.32"
705
+
version = "4.5.40"
680
706
source = "registry+https://github.com/rust-lang/crates.io-index"
681
-
checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7"
707
+
checksum = "d2c7947ae4cc3d851207c1adb5b5e260ff0cca11446b1d6d1423788e442257ce"
682
708
dependencies = [
683
709
"heck",
684
710
"proc-macro2",
···
704
730
]
705
731
706
732
[[package]]
733
+
name = "cmake"
734
+
version = "0.1.54"
735
+
source = "registry+https://github.com/rust-lang/crates.io-index"
736
+
checksum = "e7caa3f9de89ddbe2c607f4101924c5abec803763ae9534e4f4d7d8f84aa81f0"
737
+
dependencies = [
738
+
"cc",
739
+
]
740
+
741
+
[[package]]
707
742
name = "colorchoice"
708
743
version = "1.0.3"
709
744
source = "registry+https://github.com/rust-lang/crates.io-index"
···
755
790
"tokio",
756
791
"tokio-util",
757
792
"tower-http",
758
-
"tungstenite",
793
+
"tungstenite 0.26.2",
759
794
"zstd",
760
795
]
761
796
···
764
799
version = "0.9.4"
765
800
source = "registry+https://github.com/rust-lang/crates.io-index"
766
801
checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
802
+
dependencies = [
803
+
"core-foundation-sys",
804
+
"libc",
805
+
]
806
+
807
+
[[package]]
808
+
name = "core-foundation"
809
+
version = "0.10.1"
810
+
source = "registry+https://github.com/rust-lang/crates.io-index"
811
+
checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6"
767
812
dependencies = [
768
813
"core-foundation-sys",
769
814
"libc",
···
839
884
840
885
[[package]]
841
886
name = "ctrlc"
842
-
version = "3.4.6"
887
+
version = "3.4.7"
843
888
source = "registry+https://github.com/rust-lang/crates.io-index"
844
-
checksum = "697b5419f348fd5ae2478e8018cb016c00a5881c7f46c717de98ffd135a5651c"
889
+
checksum = "46f93780a459b7d656ef7f071fe699c4d3d2cb201c4b24d085b6ddc505276e73"
845
890
dependencies = [
846
891
"nix",
847
892
"windows-sys 0.59.0",
···
988
1033
989
1034
[[package]]
990
1035
name = "dropshot"
991
-
version = "0.16.0"
1036
+
version = "0.16.2"
992
1037
source = "registry+https://github.com/rust-lang/crates.io-index"
993
-
checksum = "a37c505dad56e0c1fa5ed47e29fab1a1ab2d1a9d93e952024bb47168969705f6"
1038
+
checksum = "50e8fed669e35e757646ad10f97c4d26dd22cce3da689b307954f7000d2719d0"
994
1039
dependencies = [
995
1040
"async-stream",
996
1041
"async-trait",
···
1012
1057
"openapiv3",
1013
1058
"paste",
1014
1059
"percent-encoding",
1015
-
"rustls",
1060
+
"rustls 0.22.4",
1016
1061
"rustls-pemfile",
1017
1062
"schemars",
1018
1063
"scopeguard",
···
1029
1074
"slog-term",
1030
1075
"thiserror 2.0.12",
1031
1076
"tokio",
1032
-
"tokio-rustls",
1077
+
"tokio-rustls 0.25.0",
1033
1078
"toml",
1034
1079
"uuid",
1035
1080
"version_check",
···
1038
1083
1039
1084
[[package]]
1040
1085
name = "dropshot_endpoint"
1041
-
version = "0.16.0"
1086
+
version = "0.16.2"
1042
1087
source = "registry+https://github.com/rust-lang/crates.io-index"
1043
-
checksum = "8b1a6db3728f0195e3ad62807649913aaba06d45421e883416e555e51464ef67"
1088
+
checksum = "acebb687581abdeaa2c89fa448818a5f803b0e68e5d7e7a1cf585a8f3c5c57ac"
1044
1089
dependencies = [
1045
1090
"heck",
1046
1091
"proc-macro2",
···
1052
1097
]
1053
1098
1054
1099
[[package]]
1100
+
name = "dunce"
1101
+
version = "1.0.5"
1102
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1103
+
checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
1104
+
1105
+
[[package]]
1055
1106
name = "dyn-clone"
1056
1107
version = "1.0.19"
1057
1108
source = "registry+https://github.com/rust-lang/crates.io-index"
···
1231
1282
"rustix 0.38.44",
1232
1283
"windows-sys 0.52.0",
1233
1284
]
1285
+
1286
+
[[package]]
1287
+
name = "fs_extra"
1288
+
version = "1.3.0"
1289
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1290
+
checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
1234
1291
1235
1292
[[package]]
1236
1293
name = "futures"
···
1481
1538
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
1482
1539
1483
1540
[[package]]
1541
+
name = "home"
1542
+
version = "0.5.11"
1543
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1544
+
checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf"
1545
+
dependencies = [
1546
+
"windows-sys 0.59.0",
1547
+
]
1548
+
1549
+
[[package]]
1484
1550
name = "hostname"
1485
1551
version = "0.3.1"
1486
1552
source = "registry+https://github.com/rust-lang/crates.io-index"
···
1579
1645
]
1580
1646
1581
1647
[[package]]
1648
+
name = "hyper-rustls"
1649
+
version = "0.27.7"
1650
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1651
+
checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58"
1652
+
dependencies = [
1653
+
"http",
1654
+
"hyper",
1655
+
"hyper-util",
1656
+
"rustls 0.23.28",
1657
+
"rustls-native-certs",
1658
+
"rustls-pki-types",
1659
+
"tokio",
1660
+
"tokio-rustls 0.26.2",
1661
+
"tower-service",
1662
+
]
1663
+
1664
+
[[package]]
1582
1665
name = "hyper-util"
1583
1666
version = "0.1.11"
1584
1667
source = "registry+https://github.com/rust-lang/crates.io-index"
···
1872
1955
"serde_json",
1873
1956
"thiserror 2.0.12",
1874
1957
"tokio",
1875
-
"tokio-tungstenite",
1958
+
"tokio-tungstenite 0.26.2",
1876
1959
"url",
1877
1960
"zstd",
1878
1961
]
···
2202
2285
2203
2286
[[package]]
2204
2287
name = "metrics-exporter-prometheus"
2205
-
version = "0.17.0"
2288
+
version = "0.17.1"
2206
2289
source = "registry+https://github.com/rust-lang/crates.io-index"
2207
-
checksum = "df88858cd28baaaf2cfc894e37789ed4184be0e1351157aec7bf3c2266c793fd"
2290
+
checksum = "989903b4c7abfa6827a8d1128ef42faf83f8969d429797c5431f236f2cae8b8b"
2208
2291
dependencies = [
2209
2292
"base64 0.22.1",
2210
2293
"http-body-util",
2211
2294
"hyper",
2295
+
"hyper-rustls",
2212
2296
"hyper-util",
2213
2297
"indexmap 2.9.0",
2214
2298
"ipnet",
···
2367
2451
"openssl-probe",
2368
2452
"openssl-sys",
2369
2453
"schannel",
2370
-
"security-framework",
2454
+
"security-framework 2.11.1",
2371
2455
"security-framework-sys",
2372
2456
"tempfile",
2373
2457
]
2374
2458
2375
2459
[[package]]
2376
2460
name = "nix"
2377
-
version = "0.29.0"
2461
+
version = "0.30.1"
2378
2462
source = "registry+https://github.com/rust-lang/crates.io-index"
2379
-
checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
2463
+
checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6"
2380
2464
dependencies = [
2381
2465
"bitflags",
2382
2466
"cfg-if",
···
2632
2716
]
2633
2717
2634
2718
[[package]]
2719
+
name = "prettyplease"
2720
+
version = "0.2.34"
2721
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2722
+
checksum = "6837b9e10d61f45f987d50808f83d1ee3d206c66acf650c3e4ae2e1f6ddedf55"
2723
+
dependencies = [
2724
+
"proc-macro2",
2725
+
"syn",
2726
+
]
2727
+
2728
+
[[package]]
2635
2729
name = "proc-macro2"
2636
2730
version = "1.0.94"
2637
2731
source = "registry+https://github.com/rust-lang/crates.io-index"
···
2969
3063
"log",
2970
3064
"ring",
2971
3065
"rustls-pki-types",
2972
-
"rustls-webpki",
3066
+
"rustls-webpki 0.102.8",
3067
+
"subtle",
3068
+
"zeroize",
3069
+
]
3070
+
3071
+
[[package]]
3072
+
name = "rustls"
3073
+
version = "0.23.28"
3074
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3075
+
checksum = "7160e3e10bf4535308537f3c4e1641468cd0e485175d6163087c0393c7d46643"
3076
+
dependencies = [
3077
+
"aws-lc-rs",
3078
+
"once_cell",
3079
+
"rustls-pki-types",
3080
+
"rustls-webpki 0.103.3",
2973
3081
"subtle",
2974
3082
"zeroize",
2975
3083
]
2976
3084
2977
3085
[[package]]
3086
+
name = "rustls-native-certs"
3087
+
version = "0.8.1"
3088
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3089
+
checksum = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3"
3090
+
dependencies = [
3091
+
"openssl-probe",
3092
+
"rustls-pki-types",
3093
+
"schannel",
3094
+
"security-framework 3.2.0",
3095
+
]
3096
+
3097
+
[[package]]
2978
3098
name = "rustls-pemfile"
2979
3099
version = "2.2.0"
2980
3100
source = "registry+https://github.com/rust-lang/crates.io-index"
···
2995
3115
source = "registry+https://github.com/rust-lang/crates.io-index"
2996
3116
checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9"
2997
3117
dependencies = [
3118
+
"ring",
3119
+
"rustls-pki-types",
3120
+
"untrusted",
3121
+
]
3122
+
3123
+
[[package]]
3124
+
name = "rustls-webpki"
3125
+
version = "0.103.3"
3126
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3127
+
checksum = "e4a72fe2bcf7a6ac6fd7d0b9e5cb68aeb7d4c0a0271730218b3e92d43b4eb435"
3128
+
dependencies = [
3129
+
"aws-lc-rs",
2998
3130
"ring",
2999
3131
"rustls-pki-types",
3000
3132
"untrusted",
···
3066
3198
checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
3067
3199
dependencies = [
3068
3200
"bitflags",
3069
-
"core-foundation",
3201
+
"core-foundation 0.9.4",
3202
+
"core-foundation-sys",
3203
+
"libc",
3204
+
"security-framework-sys",
3205
+
]
3206
+
3207
+
[[package]]
3208
+
name = "security-framework"
3209
+
version = "3.2.0"
3210
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3211
+
checksum = "271720403f46ca04f7ba6f55d438f8bd878d6b8ca0a1046e8228c4145bcbb316"
3212
+
dependencies = [
3213
+
"bitflags",
3214
+
"core-foundation 0.10.1",
3070
3215
"core-foundation-sys",
3071
3216
"libc",
3072
3217
"security-framework-sys",
···
3184
3329
3185
3330
[[package]]
3186
3331
name = "serde_spanned"
3187
-
version = "0.6.8"
3332
+
version = "0.6.9"
3188
3333
source = "registry+https://github.com/rust-lang/crates.io-index"
3189
-
checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1"
3334
+
checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
3190
3335
dependencies = [
3191
3336
"serde",
3192
3337
]
···
3378
3523
]
3379
3524
3380
3525
[[package]]
3526
+
name = "spacedust"
3527
+
version = "0.1.0"
3528
+
dependencies = [
3529
+
"async-trait",
3530
+
"clap",
3531
+
"ctrlc",
3532
+
"dropshot",
3533
+
"env_logger",
3534
+
"futures",
3535
+
"http",
3536
+
"jetstream",
3537
+
"links",
3538
+
"log",
3539
+
"metrics",
3540
+
"metrics-exporter-prometheus 0.17.1",
3541
+
"rand 0.9.1",
3542
+
"schemars",
3543
+
"semver",
3544
+
"serde",
3545
+
"serde_json",
3546
+
"serde_qs",
3547
+
"thiserror 2.0.12",
3548
+
"tinyjson",
3549
+
"tokio",
3550
+
"tokio-tungstenite 0.27.0",
3551
+
"tokio-util",
3552
+
]
3553
+
3554
+
[[package]]
3381
3555
name = "spin"
3382
3556
version = "0.9.8"
3383
3557
source = "registry+https://github.com/rust-lang/crates.io-index"
···
3412
3586
3413
3587
[[package]]
3414
3588
name = "syn"
3415
-
version = "2.0.100"
3589
+
version = "2.0.103"
3416
3590
source = "registry+https://github.com/rust-lang/crates.io-index"
3417
-
checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0"
3591
+
checksum = "e4307e30089d6fd6aff212f2da3a1f9e32f3223b1f010fb09b7c95f90f3ca1e8"
3418
3592
dependencies = [
3419
3593
"proc-macro2",
3420
3594
"quote",
···
3595
3769
3596
3770
[[package]]
3597
3771
name = "tokio"
3598
-
version = "1.44.2"
3772
+
version = "1.45.1"
3599
3773
source = "registry+https://github.com/rust-lang/crates.io-index"
3600
-
checksum = "e6b88822cbe49de4185e3a4cbf8321dd487cf5fe0c5c65695fef6346371e9c48"
3774
+
checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779"
3601
3775
dependencies = [
3602
3776
"backtrace",
3603
3777
"bytes",
···
3638
3812
source = "registry+https://github.com/rust-lang/crates.io-index"
3639
3813
checksum = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f"
3640
3814
dependencies = [
3641
-
"rustls",
3815
+
"rustls 0.22.4",
3642
3816
"rustls-pki-types",
3643
3817
"tokio",
3644
3818
]
3645
3819
3646
3820
[[package]]
3821
+
name = "tokio-rustls"
3822
+
version = "0.26.2"
3823
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3824
+
checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b"
3825
+
dependencies = [
3826
+
"rustls 0.23.28",
3827
+
"tokio",
3828
+
]
3829
+
3830
+
[[package]]
3647
3831
name = "tokio-tungstenite"
3648
3832
version = "0.26.2"
3649
3833
source = "registry+https://github.com/rust-lang/crates.io-index"
···
3654
3838
"native-tls",
3655
3839
"tokio",
3656
3840
"tokio-native-tls",
3657
-
"tungstenite",
3841
+
"tungstenite 0.26.2",
3842
+
]
3843
+
3844
+
[[package]]
3845
+
name = "tokio-tungstenite"
3846
+
version = "0.27.0"
3847
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3848
+
checksum = "489a59b6730eda1b0171fcfda8b121f4bee2b35cba8645ca35c5f7ba3eb736c1"
3849
+
dependencies = [
3850
+
"futures-util",
3851
+
"log",
3852
+
"tokio",
3853
+
"tungstenite 0.27.0",
3658
3854
]
3659
3855
3660
3856
[[package]]
···
3672
3868
3673
3869
[[package]]
3674
3870
name = "toml"
3675
-
version = "0.8.20"
3871
+
version = "0.8.23"
3676
3872
source = "registry+https://github.com/rust-lang/crates.io-index"
3677
-
checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148"
3873
+
checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
3678
3874
dependencies = [
3679
3875
"serde",
3680
3876
"serde_spanned",
···
3684
3880
3685
3881
[[package]]
3686
3882
name = "toml_datetime"
3687
-
version = "0.6.8"
3883
+
version = "0.6.11"
3688
3884
source = "registry+https://github.com/rust-lang/crates.io-index"
3689
-
checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41"
3885
+
checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
3690
3886
dependencies = [
3691
3887
"serde",
3692
3888
]
3693
3889
3694
3890
[[package]]
3695
3891
name = "toml_edit"
3696
-
version = "0.22.24"
3892
+
version = "0.22.27"
3697
3893
source = "registry+https://github.com/rust-lang/crates.io-index"
3698
-
checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474"
3894
+
checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
3699
3895
dependencies = [
3700
3896
"indexmap 2.9.0",
3701
3897
"serde",
3702
3898
"serde_spanned",
3703
3899
"toml_datetime",
3900
+
"toml_write",
3704
3901
"winnow",
3705
3902
]
3706
3903
3707
3904
[[package]]
3905
+
name = "toml_write"
3906
+
version = "0.1.2"
3907
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3908
+
checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
3909
+
3910
+
[[package]]
3708
3911
name = "tower"
3709
3912
version = "0.5.2"
3710
3913
source = "registry+https://github.com/rust-lang/crates.io-index"
···
3833
4036
]
3834
4037
3835
4038
[[package]]
4039
+
name = "tungstenite"
4040
+
version = "0.27.0"
4041
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4042
+
checksum = "eadc29d668c91fcc564941132e17b28a7ceb2f3ebf0b9dae3e03fd7a6748eb0d"
4043
+
dependencies = [
4044
+
"bytes",
4045
+
"data-encoding",
4046
+
"http",
4047
+
"httparse",
4048
+
"log",
4049
+
"rand 0.9.1",
4050
+
"sha1",
4051
+
"thiserror 2.0.12",
4052
+
"utf-8",
4053
+
]
4054
+
4055
+
[[package]]
3836
4056
name = "typenum"
3837
4057
version = "1.18.0"
3838
4058
source = "registry+https://github.com/rust-lang/crates.io-index"
···
3858
4078
"log",
3859
4079
"lsm-tree",
3860
4080
"metrics",
3861
-
"metrics-exporter-prometheus 0.17.0",
4081
+
"metrics-exporter-prometheus 0.17.1",
3862
4082
"schemars",
3863
4083
"semver",
3864
4084
"serde",
···
4118
4338
]
4119
4339
4120
4340
[[package]]
4341
+
name = "which"
4342
+
version = "4.4.2"
4343
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4344
+
checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7"
4345
+
dependencies = [
4346
+
"either",
4347
+
"home",
4348
+
"once_cell",
4349
+
"rustix 0.38.44",
4350
+
]
4351
+
4352
+
[[package]]
4121
4353
name = "winapi"
4122
4354
version = "0.3.9"
4123
4355
source = "registry+https://github.com/rust-lang/crates.io-index"
···
4346
4578
4347
4579
[[package]]
4348
4580
name = "winnow"
4349
-
version = "0.7.6"
4581
+
version = "0.7.11"
4350
4582
source = "registry+https://github.com/rust-lang/crates.io-index"
4351
-
checksum = "63d3fcd9bba44b03821e7d699eeee959f3126dcc4aa8e4ae18ec617c2a5cea10"
4583
+
checksum = "74c7b26e3480b707944fc872477815d29a8e429d2f93a1ce000f5fa84a15cbcd"
4352
4584
dependencies = [
4353
4585
"memchr",
4354
4586
]
+2
-1
jetstream/src/error.rs
+2
-1
jetstream/src/error.rs
···
38
38
pub enum JetstreamEventError {
39
39
#[error("failed to load built-in zstd dictionary for decoding: {0}")]
40
40
CompressionDictionaryError(io::Error),
41
-
42
41
#[error("failed to send ping or pong: {0}")]
43
42
PingPongError(#[from] tokio_tungstenite::tungstenite::Error),
43
+
#[error("no messages received within ttl")]
44
+
NoMessagesReceived,
44
45
#[error("jetstream event receiver closed")]
45
46
ReceiverClosedError,
46
47
}
+29
-4
jetstream/src/lib.rs
+29
-4
jetstream/src/lib.rs
···
27
27
Receiver,
28
28
Sender,
29
29
},
30
+
time::timeout,
30
31
};
31
32
use tokio_tungstenite::{
32
33
connect_async,
···
200
201
/// can help prevent that if your consumer sometimes pauses, at a cost of higher memory
201
202
/// usage while events are buffered.
202
203
pub channel_size: usize,
204
+
/// How long since the last jetstream message before we consider the connection dead
205
+
///
206
+
/// Default: 15s
207
+
pub liveliness_ttl: Duration,
203
208
}
204
209
205
210
impl Default for JetstreamConfig {
···
213
218
omit_user_agent_jetstream_info: false,
214
219
replay_on_reconnect: false,
215
220
channel_size: 4096, // a few seconds of firehose buffer
221
+
liveliness_ttl: Duration::from_secs(15),
216
222
}
217
223
}
218
224
}
···
380
386
381
387
let (send_channel, receive_channel) = channel(self.config.channel_size);
382
388
let replay_on_reconnect = self.config.replay_on_reconnect;
389
+
let liveliness_ttl = self.config.liveliness_ttl;
383
390
let build_request = self.config.get_request_builder();
384
391
385
392
tokio::task::spawn(async move {
···
414
421
if let Ok((ws_stream, _)) = connect_async(req).await {
415
422
let t_connected = Instant::now();
416
423
log::info!("jetstream connected. starting websocket task...");
417
-
if let Err(e) =
418
-
websocket_task(dict, ws_stream, send_channel.clone(), &mut last_cursor)
419
-
.await
424
+
if let Err(e) = websocket_task(
425
+
dict,
426
+
ws_stream,
427
+
send_channel.clone(),
428
+
&mut last_cursor,
429
+
liveliness_ttl,
430
+
)
431
+
.await
420
432
{
421
433
match e {
422
434
JetstreamEventError::ReceiverClosedError => {
···
428
440
JetstreamEventError::CompressionDictionaryError(_) => {
429
441
#[cfg(feature="metrics")]
430
442
counter!("jetstream_disconnects", "reason" => "zstd", "fatal" => "no").increment(1);
443
+
}
444
+
JetstreamEventError::NoMessagesReceived => {
445
+
#[cfg(feature="metrics")]
446
+
counter!("jetstream_disconnects", "reason" => "ttl", "fatal" => "no").increment(1);
431
447
}
432
448
JetstreamEventError::PingPongError(_) => {
433
449
#[cfg(feature="metrics")]
···
481
497
ws: WebSocketStream<MaybeTlsStream<TcpStream>>,
482
498
send_channel: JetstreamSender,
483
499
last_cursor: &mut Option<Cursor>,
500
+
liveliness_ttl: Duration,
484
501
) -> Result<(), JetstreamEventError> {
485
502
// TODO: Use the write half to allow the user to change configuration settings on the fly.
486
503
let (mut socket_write, mut socket_read) = ws.split();
487
504
488
505
let mut closing_connection = false;
489
506
loop {
490
-
match socket_read.next().await {
507
+
let next = match timeout(liveliness_ttl, socket_read.next()).await {
508
+
Ok(n) => n,
509
+
Err(_) => {
510
+
log::warn!("jetstream no events for {liveliness_ttl:?}, closing");
511
+
_ = socket_write.close().await;
512
+
return Err(JetstreamEventError::NoMessagesReceived);
513
+
}
514
+
};
515
+
match next {
491
516
Some(Ok(message)) => match message {
492
517
Message::Text(json) => {
493
518
#[cfg(feature = "metrics")]
+29
spacedust/Cargo.toml
+29
spacedust/Cargo.toml
···
1
+
[package]
2
+
name = "spacedust"
3
+
version = "0.1.0"
4
+
edition = "2024"
5
+
6
+
[dependencies]
7
+
async-trait = "0.1.88"
8
+
clap = { version = "4.5.40", features = ["derive"] }
9
+
ctrlc = "3.4.7"
10
+
dropshot = "0.16.2"
11
+
env_logger = "0.11.8"
12
+
futures = "0.3.31"
13
+
http = "1.3.1"
14
+
jetstream = { path = "../jetstream", features = ["metrics"] }
15
+
links = { path = "../links" }
16
+
log = "0.4.27"
17
+
metrics = "0.24.2"
18
+
metrics-exporter-prometheus = { version = "0.17.1", features = ["http-listener"] }
19
+
rand = "0.9.1"
20
+
schemars = "0.8.22"
21
+
semver = "1.0.26"
22
+
serde = { version = "1.0.219", features = ["derive"] }
23
+
serde_json = "1.0.140"
24
+
serde_qs = "1.0.0-rc.3"
25
+
thiserror = "2.0.12"
26
+
tinyjson = "2.5.1"
27
+
tokio = { version = "1.45.1", features = ["full"] }
28
+
tokio-tungstenite = "0.27.0"
29
+
tokio-util = "0.7.15"
+104
spacedust/src/consumer.rs
+104
spacedust/src/consumer.rs
···
1
+
use tokio_util::sync::CancellationToken;
2
+
use crate::LinkEvent;
3
+
use crate::error::ConsumerError;
4
+
use crate::removable_delay_queue;
5
+
use jetstream::{
6
+
DefaultJetstreamEndpoints, JetstreamCompression, JetstreamConfig, JetstreamConnector,
7
+
events::{CommitOp, Cursor, EventKind},
8
+
};
9
+
use links::collect_links;
10
+
use tokio::sync::broadcast;
11
+
12
+
const MAX_LINKS_PER_EVENT: usize = 100;
13
+
14
+
pub async fn consume(
15
+
b: broadcast::Sender<LinkEvent>,
16
+
d: removable_delay_queue::Input<(String, usize), LinkEvent>,
17
+
jetstream_endpoint: String,
18
+
cursor: Option<Cursor>,
19
+
no_zstd: bool,
20
+
shutdown: CancellationToken,
21
+
) -> Result<(), ConsumerError> {
22
+
let endpoint = DefaultJetstreamEndpoints::endpoint_or_shortcut(&jetstream_endpoint);
23
+
if endpoint == jetstream_endpoint {
24
+
log::info!("connecting to jetstream at {endpoint}");
25
+
} else {
26
+
log::info!("connecting to jetstream at {jetstream_endpoint} => {endpoint}");
27
+
}
28
+
let config: JetstreamConfig = JetstreamConfig {
29
+
endpoint,
30
+
compression: if no_zstd {
31
+
JetstreamCompression::None
32
+
} else {
33
+
JetstreamCompression::Zstd
34
+
},
35
+
replay_on_reconnect: true,
36
+
channel_size: 1024, // buffer up to ~1s of jetstream events
37
+
..Default::default()
38
+
};
39
+
let mut receiver = JetstreamConnector::new(config)?
40
+
.connect_cursor(cursor)
41
+
.await?;
42
+
43
+
log::info!("receiving jetstream messages..");
44
+
loop {
45
+
if shutdown.is_cancelled() {
46
+
log::info!("exiting consumer for shutdown");
47
+
return Ok(());
48
+
}
49
+
let Some(event) = receiver.recv().await else {
50
+
log::error!("could not receive jetstream event, bailing");
51
+
break;
52
+
};
53
+
54
+
if event.kind != EventKind::Commit {
55
+
continue;
56
+
}
57
+
let Some(commit) = event.commit else {
58
+
log::warn!("jetstream commit event missing commit data, ignoring");
59
+
continue;
60
+
};
61
+
62
+
let at_uri = format!("at://{}/{}/{}", &*event.did, &*commit.collection, &*commit.rkey);
63
+
64
+
// TODO: keep a buffer and remove quick deletes to debounce notifs
65
+
// for now we just drop all deletes eek
66
+
if commit.operation == CommitOp::Delete {
67
+
d.remove_range((at_uri.clone(), 0)..=(at_uri.clone(), MAX_LINKS_PER_EVENT)).await;
68
+
continue;
69
+
}
70
+
let Some(record) = commit.record else {
71
+
log::warn!("jetstream commit update/delete missing record, ignoring");
72
+
continue;
73
+
};
74
+
75
+
let jv = match record.get().parse() {
76
+
Ok(v) => v,
77
+
Err(e) => {
78
+
log::warn!("jetstream record failed to parse, ignoring: {e}");
79
+
continue;
80
+
}
81
+
};
82
+
83
+
// todo: indicate if the link limit was reached (-> links omitted)
84
+
for (i, link) in collect_links(&jv).into_iter().enumerate() {
85
+
if i >= MAX_LINKS_PER_EVENT {
86
+
log::warn!("jetstream event has too many links, ignoring the rest");
87
+
break;
88
+
}
89
+
let link_ev = LinkEvent {
90
+
collection: commit.collection.to_string(),
91
+
path: link.path,
92
+
origin: at_uri.clone(),
93
+
rev: commit.rev.to_string(),
94
+
target: link.target.into_string(),
95
+
};
96
+
let _ = b.send(link_ev.clone()); // only errors if no subscribers are connected, which is just fine.
97
+
d.enqueue((at_uri.clone(), i), link_ev)
98
+
.await
99
+
.map_err(|_| ConsumerError::DelayQueueOutputDropped)?;
100
+
}
101
+
}
102
+
103
+
Err(ConsumerError::JetstreamEnded)
104
+
}
+23
spacedust/src/delay.rs
+23
spacedust/src/delay.rs
···
1
+
use crate::removable_delay_queue;
2
+
use crate::LinkEvent;
3
+
use tokio_util::sync::CancellationToken;
4
+
use tokio::sync::broadcast;
5
+
use crate::error::DelayError;
6
+
7
+
pub async fn to_broadcast(
8
+
source: removable_delay_queue::Output<(String, usize), LinkEvent>,
9
+
dest: broadcast::Sender<LinkEvent>,
10
+
shutdown: CancellationToken,
11
+
) -> Result<(), DelayError> {
12
+
loop {
13
+
tokio::select! {
14
+
ev = source.next() => match ev {
15
+
Some(event) => {
16
+
let _ = dest.send(event); // only errors of there are no listeners, but that's normal
17
+
},
18
+
None => return Err(DelayError::DelayEnded),
19
+
},
20
+
_ = shutdown.cancelled() => return Ok(()),
21
+
}
22
+
}
23
+
}
+43
spacedust/src/error.rs
+43
spacedust/src/error.rs
···
1
+
use thiserror::Error;
2
+
3
+
#[derive(Debug, Error)]
4
+
pub enum MainTaskError {
5
+
#[error(transparent)]
6
+
ConsumerTaskError(#[from] ConsumerError),
7
+
#[error(transparent)]
8
+
ServerTaskError(#[from] ServerError),
9
+
#[error(transparent)]
10
+
DelayTaskError(#[from] DelayError),
11
+
}
12
+
13
+
#[derive(Debug, Error)]
14
+
pub enum ConsumerError {
15
+
#[error(transparent)]
16
+
JetstreamConnectionError(#[from] jetstream::error::ConnectionError),
17
+
#[error(transparent)]
18
+
JetstreamConfigValidationError(#[from] jetstream::error::ConfigValidationError),
19
+
#[error("jetstream ended")]
20
+
JetstreamEnded,
21
+
#[error("delay queue output dropped")]
22
+
DelayQueueOutputDropped,
23
+
}
24
+
25
+
#[derive(Debug, Error)]
26
+
pub enum DelayError {
27
+
#[error("delay ended")]
28
+
DelayEnded,
29
+
}
30
+
31
+
#[derive(Debug, Error)]
32
+
pub enum ServerError {
33
+
#[error("failed to configure server logger: {0}")]
34
+
ConfigLogError(std::io::Error),
35
+
#[error("failed to render json for openapi: {0}")]
36
+
OpenApiJsonFail(serde_json::Error),
37
+
#[error(transparent)]
38
+
FailedToBuildServer(#[from] dropshot::BuildError),
39
+
#[error("server exited: {0}")]
40
+
ServerExited(String),
41
+
#[error("server closed badly: {0}")]
42
+
BadClose(String),
43
+
}
+51
spacedust/src/lib.rs
+51
spacedust/src/lib.rs
···
1
+
pub mod consumer;
2
+
pub mod delay;
3
+
pub mod error;
4
+
pub mod server;
5
+
pub mod subscriber;
6
+
pub mod removable_delay_queue;
7
+
8
+
use serde::Serialize;
9
+
10
+
#[derive(Debug, Clone)]
11
+
pub struct LinkEvent {
12
+
collection: String,
13
+
path: String,
14
+
origin: String,
15
+
target: String,
16
+
rev: String,
17
+
}
18
+
19
+
#[derive(Debug, Serialize)]
20
+
#[serde(rename_all="snake_case")]
21
+
pub struct ClientEvent {
22
+
kind: String, // "link"
23
+
origin: String, // "live", "replay", "backfill"
24
+
link: ClientLinkEvent,
25
+
}
26
+
27
+
#[derive(Debug, Serialize)]
28
+
struct ClientLinkEvent {
29
+
operation: String, // "create", "delete" (prob no update, though maybe for rev?)
30
+
source: String,
31
+
source_record: String,
32
+
source_rev: String,
33
+
subject: String,
34
+
// TODO: include the record too? would save clients a level of hydration
35
+
}
36
+
37
+
impl From<LinkEvent> for ClientLinkEvent {
38
+
fn from(link: LinkEvent) -> Self {
39
+
let undotted = link.path.strip_prefix('.').unwrap_or_else(|| {
40
+
eprintln!("link path did not have expected '.' prefix: {}", link.path);
41
+
""
42
+
});
43
+
Self {
44
+
operation: "create".to_string(),
45
+
source: format!("{}:{undotted}", link.collection),
46
+
source_record: link.origin,
47
+
source_rev: link.rev,
48
+
subject: link.target,
49
+
}
50
+
}
51
+
}
+139
spacedust/src/main.rs
+139
spacedust/src/main.rs
···
1
+
use spacedust::error::MainTaskError;
2
+
use spacedust::consumer;
3
+
use spacedust::server;
4
+
use spacedust::delay;
5
+
use spacedust::removable_delay_queue::removable_delay_queue;
6
+
7
+
use clap::Parser;
8
+
use metrics_exporter_prometheus::PrometheusBuilder;
9
+
use tokio::sync::broadcast;
10
+
use tokio_util::sync::CancellationToken;
11
+
use std::time::Duration;
12
+
13
+
/// Aggregate links in the at-mosphere
14
+
#[derive(Parser, Debug, Clone)]
15
+
#[command(version, about, long_about = None)]
16
+
struct Args {
17
+
/// Jetstream server to connect to (exclusive with --fixture). Provide either a wss:// URL, or a shorhand value:
18
+
/// 'us-east-1', 'us-east-2', 'us-west-1', or 'us-west-2'
19
+
#[arg(long)]
20
+
jetstream: String,
21
+
/// don't request zstd-compressed jetstream events
22
+
///
23
+
/// reduces CPU at the expense of more ingress bandwidth
24
+
#[arg(long, action)]
25
+
jetstream_no_zstd: bool,
26
+
}
27
+
28
+
#[tokio::main]
29
+
async fn main() -> Result<(), String> {
30
+
env_logger::init();
31
+
32
+
// tokio broadcast keeps a single main output queue for all subscribers.
33
+
// each subscriber clones off a copy of an individual value for each recv.
34
+
// since there's no large per-client buffer, we can make this one kind of
35
+
// big and accommodate more slow/bursty clients.
36
+
//
37
+
// in fact, we *could* even keep lagging clients alive, inserting lag-
38
+
// indicating messages to their output.... but for now we'll drop them to
39
+
// avoid accumulating zombies.
40
+
//
41
+
// events on the channel are individual links as they are discovered. a link
42
+
// contains a source and a target. the target is an at-uri, so it's up to
43
+
// ~1KB max; source is a collection + link path, which can be more but in
44
+
// practice the whole link rarely approaches 1KB total.
45
+
//
46
+
// TODO: determine if a pathological case could blow this up (eg 1MB link
47
+
// paths + slow subscriber -> 16GiB queue)
48
+
let (b, _) = broadcast::channel(16_384);
49
+
let consumer_sender = b.clone();
50
+
let (d, _) = broadcast::channel(16_384);
51
+
let consumer_delayed_sender = d.clone();
52
+
53
+
let delay = Duration::from_secs(21);
54
+
let (delay_queue_sender, delay_queue_receiver) = removable_delay_queue(delay);
55
+
56
+
let shutdown = CancellationToken::new();
57
+
58
+
let ctrlc_shutdown = shutdown.clone();
59
+
ctrlc::set_handler(move || ctrlc_shutdown.cancel()).expect("failed to set ctrl-c handler");
60
+
61
+
let args = Args::parse();
62
+
63
+
if let Err(e) = install_metrics_server() {
64
+
log::error!("failed to install metrics server: {e:?}");
65
+
};
66
+
67
+
let mut tasks: tokio::task::JoinSet<Result<(), MainTaskError>> = tokio::task::JoinSet::new();
68
+
69
+
let server_shutdown = shutdown.clone();
70
+
tasks.spawn(async move {
71
+
server::serve(b, d, server_shutdown).await?;
72
+
Ok(())
73
+
});
74
+
75
+
let consumer_shutdown = shutdown.clone();
76
+
tasks.spawn(async move {
77
+
consumer::consume(
78
+
consumer_sender,
79
+
delay_queue_sender,
80
+
args.jetstream,
81
+
None,
82
+
args.jetstream_no_zstd,
83
+
consumer_shutdown
84
+
)
85
+
.await?;
86
+
Ok(())
87
+
});
88
+
89
+
let delay_shutdown = shutdown.clone();
90
+
tasks.spawn(async move {
91
+
delay::to_broadcast(delay_queue_receiver, consumer_delayed_sender, delay_shutdown).await?;
92
+
Ok(())
93
+
});
94
+
95
+
tokio::select! {
96
+
_ = shutdown.cancelled() => log::warn!("shutdown requested"),
97
+
Some(r) = tasks.join_next() => {
98
+
log::warn!("a task exited, shutting down: {r:?}");
99
+
shutdown.cancel();
100
+
}
101
+
}
102
+
103
+
tokio::select! {
104
+
_ = async {
105
+
while let Some(completed) = tasks.join_next().await {
106
+
log::info!("shutdown: task completed: {completed:?}");
107
+
}
108
+
} => {},
109
+
_ = tokio::time::sleep(std::time::Duration::from_secs(3)) => {
110
+
log::info!("shutdown: not all tasks completed on time. aborting...");
111
+
tasks.shutdown().await;
112
+
},
113
+
}
114
+
115
+
log::info!("bye!");
116
+
117
+
Ok(())
118
+
}
119
+
120
+
fn install_metrics_server() -> Result<(), metrics_exporter_prometheus::BuildError> {
121
+
log::info!("installing metrics server...");
122
+
let host = [0, 0, 0, 0];
123
+
let port = 8765;
124
+
PrometheusBuilder::new()
125
+
.set_quantiles(&[0.5, 0.9, 0.99, 1.0])?
126
+
.set_bucket_duration(std::time::Duration::from_secs(300))?
127
+
.set_bucket_count(std::num::NonZero::new(12).unwrap()) // count * duration = 60 mins. stuff doesn't happen that fast here.
128
+
.set_enable_unit_suffix(false) // this seemed buggy for constellation (sometimes wouldn't engage)
129
+
.with_http_listener((host, port))
130
+
.install()?;
131
+
log::info!(
132
+
"metrics server installed! listening on http://{}.{}.{}.{}:{port}",
133
+
host[0],
134
+
host[1],
135
+
host[2],
136
+
host[3]
137
+
);
138
+
Ok(())
139
+
}
+121
spacedust/src/removable_delay_queue.rs
+121
spacedust/src/removable_delay_queue.rs
···
1
+
use std::ops::RangeBounds;
2
+
use std::collections::{BTreeMap, VecDeque};
3
+
use std::time::{Duration, Instant};
4
+
use tokio::sync::Mutex;
5
+
use std::sync::Arc;
6
+
use thiserror::Error;
7
+
8
+
#[derive(Debug, Error)]
9
+
pub enum EnqueueError<T> {
10
+
#[error("queue ouput dropped")]
11
+
OutputDropped(T),
12
+
}
13
+
14
+
pub trait Key: Eq + Ord + Clone {}
15
+
impl<T: Eq + Ord + Clone> Key for T {}
16
+
17
+
#[derive(Debug)]
18
+
struct Queue<K: Key, T> {
19
+
queue: VecDeque<(Instant, K)>,
20
+
items: BTreeMap<K, T>
21
+
}
22
+
23
+
pub struct Input<K: Key, T> {
24
+
q: Arc<Mutex<Queue<K, T>>>,
25
+
}
26
+
27
+
impl<K: Key, T> Input<K, T> {
28
+
/// if a key is already present, its previous item will be overwritten and
29
+
/// its delay time will be reset for the new item.
30
+
///
31
+
/// errors if the remover has been dropped
32
+
pub async fn enqueue(&self, key: K, item: T) -> Result<(), EnqueueError<T>> {
33
+
if Arc::strong_count(&self.q) == 1 {
34
+
return Err(EnqueueError::OutputDropped(item));
35
+
}
36
+
// TODO: try to push out an old element first
37
+
// for now we just hope there's a listener
38
+
let now = Instant::now();
39
+
let mut q = self.q.lock().await;
40
+
q.queue.push_back((now, key.clone()));
41
+
q.items.insert(key, item);
42
+
Ok(())
43
+
}
44
+
/// remove an item from the queue, by key
45
+
///
46
+
/// the item itself is removed, but the key will remain in the queue -- it
47
+
/// will simply be skipped over when a new output item is requested. this
48
+
/// keeps the removal cheap (=btreemap remove), for a bit of space overhead
49
+
pub async fn remove_range(&self, range: impl RangeBounds<K>) {
50
+
let n = {
51
+
let mut q = self.q.lock().await;
52
+
let keys = q.items.range(range).map(|(k, _)| k).cloned().collect::<Vec<_>>();
53
+
for k in &keys {
54
+
q.items.remove(k);
55
+
}
56
+
keys.len()
57
+
};
58
+
if n == 0 {
59
+
metrics::counter!("delay_queue_remove_not_found").increment(1);
60
+
} else {
61
+
metrics::counter!("delay_queue_remove_total_records").increment(1);
62
+
metrics::counter!("delay_queue_remove_total_links").increment(n as u64);
63
+
}
64
+
}
65
+
}
66
+
67
+
pub struct Output<K: Key, T> {
68
+
delay: Duration,
69
+
q: Arc<Mutex<Queue<K, T>>>,
70
+
}
71
+
72
+
impl<K: Key, T> Output<K, T> {
73
+
pub async fn next(&self) -> Option<T> {
74
+
let get = || async {
75
+
let mut q = self.q.lock().await;
76
+
metrics::gauge!("delay_queue_queue_len").set(q.queue.len() as f64);
77
+
metrics::gauge!("delay_queue_queue_capacity").set(q.queue.capacity() as f64);
78
+
while let Some((t, k)) = q.queue.pop_front() {
79
+
// skip over queued keys that were removed from items
80
+
if let Some(item) = q.items.remove(&k) {
81
+
return Some((t, item));
82
+
}
83
+
}
84
+
None
85
+
};
86
+
loop {
87
+
if let Some((t, item)) = get().await {
88
+
let now = Instant::now();
89
+
let expected_release = t + self.delay;
90
+
if expected_release.saturating_duration_since(now) > Duration::from_millis(1) {
91
+
tokio::time::sleep_until(expected_release.into()).await;
92
+
metrics::counter!("delay_queue_emit_total", "early" => "yes").increment(1);
93
+
metrics::histogram!("delay_queue_emit_overshoot").record(0);
94
+
} else {
95
+
let overshoot = now.saturating_duration_since(expected_release);
96
+
metrics::counter!("delay_queue_emit_total", "early" => "no").increment(1);
97
+
metrics::histogram!("delay_queue_emit_overshoot").record(overshoot.as_secs_f64());
98
+
}
99
+
return Some(item)
100
+
} else if Arc::strong_count(&self.q) == 1 {
101
+
return None;
102
+
}
103
+
// the queue is *empty*, so we need to wait at least as long as the current delay
104
+
tokio::time::sleep(self.delay).await;
105
+
metrics::counter!("delay_queue_entirely_empty_total").increment(1);
106
+
};
107
+
}
108
+
}
109
+
110
+
pub fn removable_delay_queue<K: Key, T>(
111
+
delay: Duration,
112
+
) -> (Input<K, T>, Output<K, T>) {
113
+
let q: Arc<Mutex<Queue<K, T>>> = Arc::new(Mutex::new(Queue {
114
+
queue: VecDeque::new(),
115
+
items: BTreeMap::new(),
116
+
}));
117
+
118
+
let input = Input::<K, T> { q: q.clone() };
119
+
let output = Output::<K, T> { q, delay };
120
+
(input, output)
121
+
}
+324
spacedust/src/server.rs
+324
spacedust/src/server.rs
···
1
+
use crate::error::ServerError;
2
+
use crate::subscriber::Subscriber;
3
+
use metrics::{histogram, counter};
4
+
use std::sync::Arc;
5
+
use crate::LinkEvent;
6
+
use http::{
7
+
header::{ORIGIN, USER_AGENT},
8
+
Response, StatusCode,
9
+
};
10
+
use dropshot::{
11
+
Body,
12
+
ApiDescription, ConfigDropshot, ConfigLogging, ConfigLoggingLevel, Query, RequestContext,
13
+
ServerBuilder, WebsocketConnection, channel, endpoint, HttpResponse,
14
+
ApiEndpointBodyContentType, ExtractorMetadata, HttpError, ServerContext,
15
+
SharedExtractor,
16
+
};
17
+
18
+
use schemars::JsonSchema;
19
+
use serde::{Deserialize, Serialize};
20
+
use tokio::sync::broadcast;
21
+
use tokio::time::Instant;
22
+
use tokio_tungstenite::tungstenite::protocol::Role;
23
+
use tokio_util::sync::CancellationToken;
24
+
use async_trait::async_trait;
25
+
use std::collections::HashSet;
26
+
27
+
const INDEX_HTML: &str = include_str!("../static/index.html");
28
+
const FAVICON: &[u8] = include_bytes!("../static/favicon.ico");
29
+
30
+
pub async fn serve(
31
+
b: broadcast::Sender<LinkEvent>,
32
+
d: broadcast::Sender<LinkEvent>,
33
+
shutdown: CancellationToken
34
+
) -> Result<(), ServerError> {
35
+
let config_logging = ConfigLogging::StderrTerminal {
36
+
level: ConfigLoggingLevel::Info,
37
+
};
38
+
39
+
let log = config_logging
40
+
.to_logger("example-basic")
41
+
.map_err(ServerError::ConfigLogError)?;
42
+
43
+
let mut api = ApiDescription::new();
44
+
api.register(index).unwrap();
45
+
api.register(favicon).unwrap();
46
+
api.register(openapi).unwrap();
47
+
api.register(subscribe).unwrap();
48
+
49
+
// TODO: put spec in a once cell / lazy lock thing?
50
+
let spec = Arc::new(
51
+
api.openapi(
52
+
"Spacedust",
53
+
env!("CARGO_PKG_VERSION")
54
+
.parse()
55
+
.inspect_err(|e| {
56
+
eprintln!("failed to parse cargo package version for openapi: {e:?}")
57
+
})
58
+
.unwrap_or(semver::Version::new(0, 0, 1)),
59
+
)
60
+
.description("A configurable ATProto notifications firehose.")
61
+
.contact_name("part of @microcosm.blue")
62
+
.contact_url("https://microcosm.blue")
63
+
.json()
64
+
.map_err(ServerError::OpenApiJsonFail)?,
65
+
);
66
+
67
+
let sub_shutdown = shutdown.clone();
68
+
let ctx = Context { spec, b, d, shutdown: sub_shutdown };
69
+
70
+
let server = ServerBuilder::new(api, ctx, log)
71
+
.config(ConfigDropshot {
72
+
bind_address: "0.0.0.0:9998".parse().unwrap(),
73
+
..Default::default()
74
+
})
75
+
.start()?;
76
+
77
+
tokio::select! {
78
+
s = server.wait_for_shutdown() => {
79
+
s.map_err(ServerError::ServerExited)?;
80
+
log::info!("server shut down normally.");
81
+
},
82
+
_ = shutdown.cancelled() => {
83
+
log::info!("shutting down: closing server");
84
+
server.close().await.map_err(ServerError::BadClose)?;
85
+
},
86
+
}
87
+
Ok(())
88
+
}
89
+
90
+
#[derive(Debug, Clone)]
91
+
struct Context {
92
+
pub spec: Arc<serde_json::Value>,
93
+
pub b: broadcast::Sender<LinkEvent>,
94
+
pub d: broadcast::Sender<LinkEvent>,
95
+
pub shutdown: CancellationToken,
96
+
}
97
+
98
+
async fn instrument_handler<T, H, R>(ctx: &RequestContext<T>, handler: H) -> Result<R, HttpError>
99
+
where
100
+
R: HttpResponse,
101
+
H: Future<Output = Result<R, HttpError>>,
102
+
T: ServerContext,
103
+
{
104
+
let start = Instant::now();
105
+
let result = handler.await;
106
+
let latency = start.elapsed();
107
+
let status_code = match &result {
108
+
Ok(response) => response.status_code(),
109
+
Err(e) => e.status_code.as_status(),
110
+
}
111
+
.as_str() // just the number (.to_string()'s Display does eg `200 OK`)
112
+
.to_string();
113
+
let endpoint = ctx.endpoint.operation_id.clone();
114
+
let headers = ctx.request.headers();
115
+
let origin = headers
116
+
.get(ORIGIN)
117
+
.and_then(|v| v.to_str().ok())
118
+
.unwrap_or("")
119
+
.to_string();
120
+
let ua = headers
121
+
.get(USER_AGENT)
122
+
.and_then(|v| v.to_str().ok())
123
+
.map(|ua| {
124
+
if ua.starts_with("Mozilla/5.0 ") {
125
+
"browser"
126
+
} else {
127
+
ua
128
+
}
129
+
})
130
+
.unwrap_or("")
131
+
.to_string();
132
+
counter!("server_requests_total",
133
+
"endpoint" => endpoint.clone(),
134
+
"origin" => origin,
135
+
"ua" => ua,
136
+
"status_code" => status_code,
137
+
)
138
+
.increment(1);
139
+
histogram!("server_handler_latency", "endpoint" => endpoint).record(latency.as_micros() as f64);
140
+
result
141
+
}
142
+
143
+
use dropshot::{HttpResponseHeaders, HttpResponseOk};
144
+
145
+
pub type OkCorsResponse<T> = Result<HttpResponseHeaders<HttpResponseOk<T>>, HttpError>;
146
+
147
+
/// Helper for constructing Ok responses: return OkCors(T).into()
148
+
/// (not happy with this yet)
149
+
pub struct OkCors<T: Serialize + JsonSchema + Send + Sync>(pub T);
150
+
151
+
impl<T> From<OkCors<T>> for OkCorsResponse<T>
152
+
where
153
+
T: Serialize + JsonSchema + Send + Sync,
154
+
{
155
+
fn from(ok: OkCors<T>) -> OkCorsResponse<T> {
156
+
let mut res = HttpResponseHeaders::new_unnamed(HttpResponseOk(ok.0));
157
+
res.headers_mut()
158
+
.insert("access-control-allow-origin", "*".parse().unwrap());
159
+
Ok(res)
160
+
}
161
+
}
162
+
163
+
// TODO: cors for HttpError
164
+
165
+
166
+
/// Serve index page as html
167
+
#[endpoint {
168
+
method = GET,
169
+
path = "/",
170
+
/*
171
+
* not useful to have this in openapi
172
+
*/
173
+
unpublished = true,
174
+
}]
175
+
async fn index(ctx: RequestContext<Context>) -> Result<Response<Body>, HttpError> {
176
+
instrument_handler(&ctx, async {
177
+
Ok(Response::builder()
178
+
.status(StatusCode::OK)
179
+
.header(http::header::CONTENT_TYPE, "text/html")
180
+
.body(INDEX_HTML.into())?)
181
+
})
182
+
.await
183
+
}
184
+
185
+
/// Serve index page as html
186
+
#[endpoint {
187
+
method = GET,
188
+
path = "/favicon.ico",
189
+
/*
190
+
* not useful to have this in openapi
191
+
*/
192
+
unpublished = true,
193
+
}]
194
+
async fn favicon(ctx: RequestContext<Context>) -> Result<Response<Body>, HttpError> {
195
+
instrument_handler(&ctx, async {
196
+
Ok(Response::builder()
197
+
.status(StatusCode::OK)
198
+
.header(http::header::CONTENT_TYPE, "image/x-icon")
199
+
.body(FAVICON.to_vec().into())?)
200
+
})
201
+
.await
202
+
}
203
+
204
+
/// Meta: get the openapi spec for this api
205
+
#[endpoint {
206
+
method = GET,
207
+
path = "/openapi",
208
+
/*
209
+
* not useful to have this in openapi
210
+
*/
211
+
unpublished = true,
212
+
}]
213
+
async fn openapi(ctx: RequestContext<Context>) -> OkCorsResponse<serde_json::Value> {
214
+
instrument_handler(&ctx, async {
215
+
let spec = (*ctx.context().spec).clone();
216
+
OkCors(spec).into()
217
+
})
218
+
.await
219
+
}
220
+
221
+
/// The real type that gets deserialized
222
+
#[derive(Debug, Deserialize, JsonSchema)]
223
+
#[serde(rename_all = "camelCase")]
224
+
pub struct MultiSubscribeQuery {
225
+
#[serde(default)]
226
+
pub wanted_subjects: HashSet<String>,
227
+
#[serde(default)]
228
+
pub wanted_subject_dids: HashSet<String>,
229
+
#[serde(default)]
230
+
pub wanted_sources: HashSet<String>,
231
+
}
232
+
/// The fake corresponding type for docs that dropshot won't freak out about a
233
+
/// vec for
234
+
#[derive(Deserialize, JsonSchema)]
235
+
#[allow(dead_code)]
236
+
#[serde(rename_all = "camelCase")]
237
+
struct MultiSubscribeQueryForDocs {
238
+
/// One or more at-uris to receive links about
239
+
///
240
+
/// The at-uri must be url-encoded
241
+
///
242
+
/// Pass this parameter multiple times to specify multiple collections, like
243
+
/// `wantedSubjects=[...]&wantedSubjects=[...]`
244
+
pub wanted_subjects: String,
245
+
/// One or more DIDs to receive links about
246
+
///
247
+
/// Pass this parameter multiple times to specify multiple collections
248
+
pub wanted_subject_dids: String,
249
+
/// One or more link sources to receive links about
250
+
///
251
+
/// TODO: docs about link sources
252
+
///
253
+
/// eg, a bluesky like's link source: `app.bsky.feed.like:subject.uri`
254
+
///
255
+
/// Pass this parameter multiple times to specify multiple sources
256
+
pub wanted_sources: String,
257
+
}
258
+
259
+
// The `SharedExtractor` implementation for Query<QueryType> describes how to
260
+
// construct an instance of `Query<QueryType>` from an HTTP request: namely, by
261
+
// parsing the query string to an instance of `QueryType`.
262
+
#[async_trait]
263
+
impl SharedExtractor for MultiSubscribeQuery {
264
+
async fn from_request<Context: ServerContext>(
265
+
ctx: &RequestContext<Context>,
266
+
) -> Result<MultiSubscribeQuery, HttpError> {
267
+
let raw_query = ctx.request.uri().query().unwrap_or("");
268
+
let q = serde_qs::from_str(raw_query).map_err(|e| {
269
+
HttpError::for_bad_request(None, format!("unable to parse query string: {}", e))
270
+
})?;
271
+
Ok(q)
272
+
}
273
+
274
+
fn metadata(body_content_type: ApiEndpointBodyContentType) -> ExtractorMetadata {
275
+
// HACK: query type switcheroo: passing MultiSubscribeQuery to
276
+
// `metadata` would "helpfully" panic because dropshot believes we can
277
+
// only have scalar types in a query.
278
+
//
279
+
// so instead we have a fake second type whose only job is to look the
280
+
// same as MultiSubscribeQuery exept that it has `String` instead of
281
+
// `Vec<String>`, which dropshot will accept, and generate ~close-enough
282
+
// docs for.
283
+
<Query<MultiSubscribeQueryForDocs> as SharedExtractor>::metadata(body_content_type)
284
+
}
285
+
}
286
+
287
+
#[derive(Deserialize, JsonSchema)]
288
+
#[serde(rename_all = "camelCase")]
289
+
struct ScalarSubscribeQuery {
290
+
#[serde(default)]
291
+
pub instant: bool,
292
+
}
293
+
294
+
#[channel {
295
+
protocol = WEBSOCKETS,
296
+
path = "/subscribe",
297
+
}]
298
+
async fn subscribe(
299
+
reqctx: RequestContext<Context>,
300
+
query: MultiSubscribeQuery,
301
+
scalar_query: Query<ScalarSubscribeQuery>,
302
+
upgraded: WebsocketConnection,
303
+
) -> dropshot::WebsocketChannelResult {
304
+
let ws = tokio_tungstenite::WebSocketStream::from_raw_socket(
305
+
upgraded.into_inner(),
306
+
Role::Server,
307
+
None,
308
+
)
309
+
.await;
310
+
311
+
let Context { b, d, shutdown, .. } = reqctx.context();
312
+
let sub_token = shutdown.child_token();
313
+
314
+
let q = scalar_query.into_inner();
315
+
let subscription = if q.instant { b } else { d }.subscribe();
316
+
log::info!("starting subscriber with broadcast: instant={}", q.instant);
317
+
318
+
Subscriber::new(query, sub_token)
319
+
.start(ws, subscription)
320
+
.await
321
+
.map_err(|e| format!("boo: {e:?}"))?;
322
+
323
+
Ok(())
324
+
}
+163
spacedust/src/subscriber.rs
+163
spacedust/src/subscriber.rs
···
1
+
use tokio::time::interval;
2
+
use std::time::Duration;
3
+
use futures::StreamExt;
4
+
use crate::ClientEvent;
5
+
use crate::LinkEvent;
6
+
use crate::server::MultiSubscribeQuery;
7
+
use futures::SinkExt;
8
+
use std::error::Error;
9
+
use tokio::sync::broadcast::{self, error::RecvError};
10
+
use tokio_tungstenite::{WebSocketStream, tungstenite::Message};
11
+
use tokio_util::sync::CancellationToken;
12
+
use dropshot::WebsocketConnectionRaw;
13
+
14
+
const PING_PERIOD: Duration = Duration::from_secs(30);
15
+
16
+
pub struct Subscriber {
17
+
query: MultiSubscribeQuery,
18
+
shutdown: CancellationToken,
19
+
}
20
+
21
+
impl Subscriber {
22
+
pub fn new(
23
+
query: MultiSubscribeQuery,
24
+
shutdown: CancellationToken,
25
+
) -> Self {
26
+
Self { query, shutdown }
27
+
}
28
+
29
+
pub async fn start(
30
+
self,
31
+
ws: WebSocketStream<WebsocketConnectionRaw>,
32
+
mut receiver: broadcast::Receiver<LinkEvent>
33
+
) -> Result<(), Box<dyn Error>> {
34
+
let mut ping_state = None;
35
+
let (mut ws_sender, mut ws_receiver) = ws.split();
36
+
let mut ping_interval = interval(PING_PERIOD);
37
+
let _guard = self.shutdown.clone().drop_guard();
38
+
39
+
// TODO: do we need to timeout ws sends??
40
+
41
+
metrics::counter!("subscribers_connected_total").increment(1);
42
+
metrics::gauge!("subscribers_connected").increment(1);
43
+
44
+
loop {
45
+
tokio::select! {
46
+
l = receiver.recv() => match l {
47
+
Ok(link) => if let Some(message) = self.filter(link) {
48
+
if let Err(e) = ws_sender.send(message).await {
49
+
log::warn!("failed to send link, dropping subscriber: {e:?}");
50
+
break;
51
+
}
52
+
},
53
+
Err(RecvError::Closed) => self.shutdown.cancel(),
54
+
Err(RecvError::Lagged(n)) => {
55
+
log::warn!("dropping lagging subscriber (missed {n} messages already)");
56
+
self.shutdown.cancel();
57
+
}
58
+
},
59
+
cm = ws_receiver.next() => match cm {
60
+
Some(Ok(Message::Ping(state))) => {
61
+
if let Err(e) = ws_sender.send(Message::Pong(state)).await {
62
+
log::error!("failed to reply pong to subscriber: {e:?}");
63
+
break;
64
+
}
65
+
}
66
+
Some(Ok(Message::Pong(state))) => {
67
+
if let Some(expected_state) = ping_state {
68
+
if *state == expected_state {
69
+
ping_state = None; // good
70
+
} else {
71
+
log::error!("subscriber returned a pong with the wrong state, dropping");
72
+
self.shutdown.cancel();
73
+
}
74
+
} else {
75
+
log::error!("subscriber sent a pong when none was expected");
76
+
self.shutdown.cancel();
77
+
}
78
+
}
79
+
Some(Ok(m)) => log::trace!("subscriber sent an unexpected message: {m:?}"),
80
+
Some(Err(e)) => {
81
+
log::error!("failed to receive subscriber message: {e:?}");
82
+
break;
83
+
}
84
+
None => {
85
+
log::trace!("end of subscriber messages. bye!");
86
+
break;
87
+
}
88
+
},
89
+
_ = ping_interval.tick() => {
90
+
if ping_state.is_some() {
91
+
log::warn!("did not recieve pong within {PING_PERIOD:?}, dropping subscriber");
92
+
self.shutdown.cancel();
93
+
} else {
94
+
let new_state: [u8; 8] = rand::random();
95
+
let ping = new_state.to_vec().into();
96
+
ping_state = Some(new_state);
97
+
if let Err(e) = ws_sender.send(Message::Ping(ping)).await {
98
+
log::error!("failed to send ping to subscriber, dropping: {e:?}");
99
+
self.shutdown.cancel();
100
+
}
101
+
}
102
+
}
103
+
_ = self.shutdown.cancelled() => {
104
+
log::info!("subscriber shutdown requested, bye!");
105
+
if let Err(e) = ws_sender.close().await {
106
+
log::warn!("failed to close subscriber: {e:?}");
107
+
}
108
+
break;
109
+
},
110
+
}
111
+
}
112
+
log::trace!("end of subscriber. bye!");
113
+
metrics::gauge!("subscribers_connected").decrement(1);
114
+
Ok(())
115
+
}
116
+
117
+
fn filter(
118
+
&self,
119
+
link: LinkEvent,
120
+
// mut sender: impl Sink<Message> + Unpin
121
+
) -> Option<Message> {
122
+
let query = &self.query;
123
+
124
+
// subject + subject DIDs are logical OR
125
+
let target_did = if link.target.starts_with("did:") {
126
+
link.target.clone()
127
+
} else {
128
+
let rest = link.target.strip_prefix("at://")?;
129
+
if let Some((did, _)) = rest.split_once("/") {
130
+
did
131
+
} else {
132
+
rest
133
+
}.to_string()
134
+
};
135
+
if !(query.wanted_subjects.contains(&link.target) || query.wanted_subject_dids.contains(&target_did) || query.wanted_subjects.is_empty() && query.wanted_subject_dids.is_empty()) {
136
+
// wowwww ^^ fix that
137
+
return None
138
+
}
139
+
140
+
// subjects together with sources are logical AND
141
+
142
+
if !query.wanted_sources.is_empty() {
143
+
let undotted = link.path.strip_prefix('.').unwrap_or_else(|| {
144
+
eprintln!("link path did not have expected '.' prefix: {}", link.path);
145
+
""
146
+
});
147
+
let source = format!("{}:{undotted}", link.collection);
148
+
if !query.wanted_sources.contains(&source) {
149
+
return None
150
+
}
151
+
}
152
+
153
+
let ev = ClientEvent {
154
+
kind: "link".to_string(),
155
+
origin: "live".to_string(),
156
+
link: link.into(),
157
+
};
158
+
159
+
let json = serde_json::to_string(&ev).unwrap();
160
+
161
+
Some(Message::Text(json.into()))
162
+
}
163
+
}
spacedust/static/favicon.ico
spacedust/static/favicon.ico
This is a binary file and will not be displayed.
+54
spacedust/static/index.html
+54
spacedust/static/index.html
···
1
+
<!doctype html>
2
+
<html lang="en">
3
+
<head>
4
+
<meta charset="utf-8" />
5
+
<title>Spacedust documentation</title>
6
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
7
+
<meta name="description" content="API Documentation for Spacedust, a configurable ATProto notifications firehose" />
8
+
<style>
9
+
.custom-header {
10
+
height: 42px;
11
+
background-color: #221828;
12
+
box-shadow: inset 0 -1px 0 var(--scalar-border-color);
13
+
color: var(--scalar-color-1);
14
+
font-size: var(--scalar-font-size-3);
15
+
font-family: 'Iowan Old Style', 'Palatino Linotype', 'URW Palladio L', P052, serif;
16
+
padding: 0 18px;
17
+
justify-content: space-between;
18
+
}
19
+
.custom-header,
20
+
.custom-header nav {
21
+
display: flex;
22
+
align-items: center;
23
+
gap: 18px;
24
+
}
25
+
.custom-header a:hover {
26
+
color: var(--scalar-color-2);
27
+
}
28
+
</style>
29
+
</head>
30
+
<body>
31
+
<header class="custom-header scalar-app">
32
+
<p>
33
+
TODO: pdsls jetstream link
34
+
<a href="https://ufos.microcosm.blue">Launch 🛸 UFOs app</a>: Explore lexicons
35
+
</p>
36
+
<nav>
37
+
<b>a <a href="https://microcosm.blue">microcosm</a> project</b>
38
+
<a href="https://bsky.app/profile/microcosm.blue">@microcosm.blue</a>
39
+
<a href="https://github.com/at-microcosm">github</a>
40
+
</nav>
41
+
</header>
42
+
43
+
<script id="api-reference" type="application/json" data-url="/openapi""></script>
44
+
45
+
<script>
46
+
var configuration = {
47
+
theme: 'purple',
48
+
}
49
+
document.getElementById('api-reference').dataset.configuration = JSON.stringify(configuration)
50
+
</script>
51
+
52
+
<script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"></script>
53
+
</body>
54
+
</html>
+1
-1
ufos/src/index_html.rs
+1
-1
ufos/src/index_html.rs
···
2
2
<html lang="en">
3
3
<head>
4
4
<meta charset="utf-8" />
5
-
<title>UFOs API Documentation</title>
5
+
<title>UFOs API documentation</title>
6
6
<meta name="viewport" content="width=device-width, initial-scale=1" />
7
7
<meta name="description" content="API Documentation for UFOs: Samples and stats for all atproto lexicons." />
8
8
<style>