diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index 796dc6f..268bd0b 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -14,21 +14,30 @@ jobs: steps: - uses: actions/checkout@v2 - - name: Build + + - name: Build telemetry executables (in debug mode) working-directory: ./backend - run: cargo build --verbose + run: cargo build --bins --verbose + - name: Run tests working-directory: ./backend run: cargo test --verbose - - name: Build release and call executable + + - name: Build, release and call telemetry executable working-directory: ./backend - run: cargo run --release -- --help + run: cargo run --bin telemetry_core --release -- --help + + - name: Build, release and call shard executable + working-directory: ./backend + run: cargo run --bin telemetry_shard --release -- --help + - name: Login to Dockerhub uses: docker/login-action@v1 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - - name: Build and Push template image + + - name: Build and push template image for tagged commit uses: docker/build-push-action@v2 # https://github.com/docker/build-push-action with: context: './backend' diff --git a/backend/.dockerignore b/backend/.dockerignore index 8f6db69..af65baa 100644 --- a/backend/.dockerignore +++ b/backend/.dockerignore @@ -1,3 +1,3 @@ target -Dockerfile +*.Dockerfile .git diff --git a/backend/Cargo.lock b/backend/Cargo.lock index f3fca55..d8e0ca9 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -1,289 +1,34 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 - -[[package]] -name = "actix" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "543c47e7827f8fcc9d1445bd98ba402137bfce80ee2187429de49c52b5131bd3" -dependencies = [ - "actix-rt", - "actix_derive", - "bitflags", - "bytes", - "crossbeam-channel", - "futures-core", - "futures-sink", - "futures-task", - "futures-util", - "log", - "once_cell", - "parking_lot", - "pin-project-lite 0.2.6", - "smallvec", - "tokio", - "tokio-util", -] - -[[package]] -name = "actix-codec" -version = "0.4.0-beta.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90673465c6187bd0829116b02be465dc0195a74d7719f76ffff0effef934a92e" -dependencies = [ - "bitflags", - "bytes", - "futures-core", - "futures-sink", - "log", - "pin-project-lite 0.2.6", - "tokio", - "tokio-util", -] - -[[package]] -name = "actix-http" -version = "3.0.0-beta.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a01f9e0681608afa887d4269a0857ac4226f09ba5ceda25939e8391c9da610a" -dependencies = [ - "actix-codec", - "actix-rt", - "actix-service", - "actix-tls", - "actix-utils", - "ahash", - "base64", - "bitflags", - "bytes", - "bytestring", - "cfg-if 1.0.0", - "derive_more", - "encoding_rs", - "futures-core", - "futures-util", - "h2", - "http", - "httparse", - "itoa", - "language-tags", - "log", - "mime", - "once_cell", - "percent-encoding", - "pin-project", - "rand 0.8.3", - "regex", - "serde", - "serde_json", - "serde_urlencoded", - "sha-1", - "smallvec", - "time 0.2.26", - "tokio", -] - -[[package]] -name = "actix-macros" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbcb2b608f0accc2f5bcf3dd872194ce13d94ee45b571487035864cf966b04ef" -dependencies = [ - "quote", - "syn", -] - -[[package]] -name = "actix-router" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ad299af73649e1fc893e333ccf86f377751eb95ff875d095131574c6f43452c" -dependencies = [ - "bytestring", - "http", - "log", - "regex", - "serde", -] - -[[package]] -name = "actix-rt" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b4e57bc1a3915e71526d128baf4323700bd1580bc676239e2298a4c5b001f18" -dependencies = [ - "actix-macros", - "futures-core", - "tokio", -] - -[[package]] -name = "actix-server" -version = "2.0.0-beta.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a99198727204a48f82559c18e4b0ba3197b97d5f4576a32bdbef371f3b4599c1" -dependencies = [ - "actix-codec", - "actix-rt", - "actix-service", - "actix-utils", - "futures-core", - "log", - "mio", - "num_cpus", - "slab", - "tokio", -] - -[[package]] -name = "actix-service" -version = "2.0.0-beta.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf82340ad9f4e4caf43737fd3bbc999778a268015cdc54675f60af6240bd2b05" -dependencies = [ - "futures-core", - "pin-project-lite 0.2.6", -] - -[[package]] -name = "actix-tls" -version = "3.0.0-beta.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2b1455e3f7a26d40cfc1080b571f41e8165e5a88e937ed579f7a4b3d55b0370" -dependencies = [ - "actix-codec", - "actix-rt", - "actix-service", - "actix-utils", - "derive_more", - "futures-core", - "http", - "log", - "tokio-util", -] - -[[package]] -name = "actix-utils" -version = "3.0.0-beta.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "458795e09a29bc5557604f9ff6f32236fd0ee457d631672e4ec8f6a0103bb292" -dependencies = [ - "actix-codec", - "actix-rt", - "actix-service", - "futures-core", - "futures-sink", - "log", - "pin-project-lite 0.2.6", -] - -[[package]] -name = "actix-web" -version = "4.0.0-beta.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d95e50c9e32e8456220b5804867de76e97a86ab8c38b51c9edcccc0f0fddca7" -dependencies = [ - "actix-codec", - "actix-http", - "actix-macros", - "actix-router", - "actix-rt", - "actix-server", - "actix-service", - "actix-utils", - "actix-web-codegen", - "ahash", - "awc", - "bytes", - "derive_more", - "either", - "encoding_rs", - "futures-core", - "futures-util", - "log", - "mime", - "pin-project", - "regex", - "serde", - "serde_json", - "serde_urlencoded", - "smallvec", - "socket2", - "time 0.2.26", - "url", -] - -[[package]] -name = "actix-web-actors" -version = "4.0.0-beta.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cd978e384657c95bc7391b68de75d970b2789350ca384dc1fdb30f2473c74da" -dependencies = [ - "actix", - "actix-codec", - "actix-http", - "actix-web", - "bytes", - "bytestring", - "futures-core", - "pin-project", - "tokio", -] - -[[package]] -name = "actix-web-codegen" -version = "0.5.0-beta.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f138ac357a674c3b480ddb7bbd894b13c1b6e8927d728bc9ea5e17eee2f8fc9" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "actix_derive" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d44b8fee1ced9671ba043476deddef739dd0959bf77030b26b738cc591737a7" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "ahash" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f200cbb1e856866d9eade941cf3aa0c5d7dd36f74311c4273b494f4ef036957" -dependencies = [ - "getrandom 0.2.2", - "once_cell", - "version_check", -] - [[package]] name = "aho-corasick" -version = "0.7.14" +version = "0.7.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b476ce7103678b0c6d3d395dbbae31d48ff910bd28be979ba5d48c6351131d0d" +checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" dependencies = [ "memchr", ] [[package]] -name = "arc-swap" -version = "0.4.7" +name = "ansi_term" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d25d88fd6b8041580a654f9d0c581a047baee2b3efee13275f2fc392fc75034" +checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" +dependencies = [ + "winapi", +] + +[[package]] +name = "anyhow" +version = "1.0.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15af2628f6890fe2609a3b91bef4c83450512802e59489f9c1cb1fa5df064a61" [[package]] name = "arrayvec" -version = "0.5.2" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" +checksum = "be4dc07131ffa69b8072d35f5007352af944213cde02545e2103680baed38fcd" [[package]] name = "atty" @@ -302,44 +47,18 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" -[[package]] -name = "awc" -version = "3.0.0-beta.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09aecd8728f6491a62b27454ea4b36fb7e50faf32928b0369b644e402c651f4e" -dependencies = [ - "actix-codec", - "actix-http", - "actix-rt", - "actix-service", - "base64", - "bytes", - "cfg-if 1.0.0", - "derive_more", - "futures-core", - "itoa", - "log", - "mime", - "percent-encoding", - "pin-project-lite 0.2.6", - "rand 0.8.3", - "serde", - "serde_json", - "serde_urlencoded", -] - -[[package]] -name = "base-x" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b20b618342cf9891c292c4f5ac2cde7287cc5c87e87e9c769d617793607dec1" - [[package]] name = "base64" version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" +[[package]] +name = "bimap" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50ae17cabbc8a38a1e3e4c1a6a664e9a09672dc14d0896fa8d865d3a5a446b07" + [[package]] name = "bincode" version = "1.3.3" @@ -357,9 +76,9 @@ checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" [[package]] name = "bitvec" -version = "0.20.2" +version = "0.20.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f682656975d3a682daff957be4ddeb65d6ad656737cd821f2d00685ae466af1" +checksum = "7774144344a4faa177370406a7ff5f1da24303817368584c6206c8303eb07848" dependencies = [ "funty", "radium", @@ -377,10 +96,32 @@ dependencies = [ ] [[package]] -name = "bumpalo" -version = "3.4.0" +name = "bstr" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e8c087f005730276d1096a652e92a8bacee2e2472bcc9715a74d2bec38b5820" +checksum = "90682c8d613ad3373e66de8c6411e0ae2ab2571e879d2efbf73558cc66f21279" +dependencies = [ + "lazy_static", + "memchr", + "regex-automata", + "serde", +] + +[[package]] +name = "buf_redux" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b953a6887648bb07a535631f2bc00fbdb2a2216f135552cb3f534ed136b9c07f" +dependencies = [ + "memchr", + "safemem", +] + +[[package]] +name = "bumpalo" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c59e7af012c713f529e7a3ee57ce9b31ddd858d4b512923602f74608b009631" [[package]] name = "byte-slice-cast" @@ -390,9 +131,9 @@ checksum = "65c1bf4a04a88c54f589125563643d773f3254b5c38571395e2b591c693bbc81" [[package]] name = "byteorder" -version = "1.3.4" +version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" [[package]] name = "bytes" @@ -401,25 +142,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b700ce4376041dcd0a327fd0097c41095743c4c8af8887265942faf1100bd040" [[package]] -name = "bytestring" -version = "1.0.0" +name = "cast" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90706ba19e97b90786e19dc0d5e2abd80008d99d4c0c5d1ad0b5e72cec7c494d" +checksum = "4c24dab4283a142afa2fdca129b80ad2c6284e073930f964c3a1293c225ee39a" dependencies = [ - "bytes", + "rustc_version", ] [[package]] name = "cc" -version = "1.0.61" +version = "1.0.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed67cbde08356238e75fc4656be4749481eeffb09e19f320a25237d5221c985d" - -[[package]] -name = "cfg-if" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" +checksum = "4a72c244c1ff497a746a7e1fb3d14bd08420ecda70c8f25c7112f2781652d787" [[package]] name = "cfg-if" @@ -436,52 +171,25 @@ dependencies = [ "libc", "num-integer", "num-traits", - "serde", - "time 0.1.44", + "time", "winapi", ] [[package]] name = "clap" -version = "3.0.0-beta.2" +version = "2.33.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bd1061998a501ee7d4b6d449020df3266ca3124b941ec56cf2005c3779ca142" +checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002" dependencies = [ + "ansi_term", "atty", "bitflags", - "clap_derive", - "indexmap", - "lazy_static", - "os_str_bytes", "strsim", - "termcolor", "textwrap", "unicode-width", "vec_map", ] -[[package]] -name = "clap_derive" -version = "3.0.0-beta.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "370f715b81112975b1b69db93e0b56ea4cd4e5002ac43b2da8474106a54096a1" -dependencies = [ - "heck", - "proc-macro-error", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "cloudabi" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4344512281c643ae7638bbabc3af17a11307803ec8f0fcad9fae512a8bf36467" -dependencies = [ - "bitflags", -] - [[package]] name = "colored" version = "1.9.3" @@ -494,10 +202,28 @@ dependencies = [ ] [[package]] -name = "const_fn" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce90df4c658c62f12d78f7508cf92f9173e5184a539c10bfe54a3107b3ffd0f2" +name = "common" +version = "0.1.0" +dependencies = [ + "bimap", + "bincode", + "bytes", + "fnv", + "futures", + "hex", + "http", + "log", + "num-traits", + "pin-project-lite", + "primitive-types", + "rustc-hash", + "serde", + "serde_json", + "soketto", + "thiserror", + "tokio", + "tokio-util", +] [[package]] name = "core-foundation" @@ -516,29 +242,93 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea221b5284a47e40033bf9b66f35f984ec0ea2931eb03505246cd27a963f981b" [[package]] -name = "cpuid-bool" -version = "0.1.2" +name = "cpufeatures" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8aebca1129a03dc6dc2b127edd729435bbc4a37e1d5f4d7513165089ceb02634" +checksum = "ed00c67cb5d0a7d64a44f6ad2668db7e7530311dd53ea79bcd4fb022c64911c8" +dependencies = [ + "libc", +] + +[[package]] +name = "criterion" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab327ed7354547cc2ef43cbe20ef68b988e70b4b593cbd66a2a61733123a3d23" +dependencies = [ + "atty", + "cast", + "clap", + "criterion-plot", + "csv", + "futures", + "itertools 0.10.1", + "lazy_static", + "num-traits", + "oorandom", + "plotters", + "rayon", + "regex", + "serde", + "serde_cbor", + "serde_derive", + "serde_json", + "tinytemplate", + "tokio", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022feadec601fba1649cfa83586381a4ad31c6bf3a9ab7d408118b05dd9889d" +dependencies = [ + "cast", + "itertools 0.9.0", +] [[package]] name = "crossbeam-channel" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dca26ee1f8d361640700bde38b2c37d8c22b3ce2d360e1fc1c74ea4b0aa7d775" +checksum = "06ed27e177f16d65f0f0c22a213e17c696ace5dd64b14258b52f9417ccb52db4" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "crossbeam-utils", ] [[package]] -name = "crossbeam-utils" -version = "0.8.3" +name = "crossbeam-deque" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7e9d99fa91428effe99c5c6d4634cdeba32b8cf784fc428a2a687f61a952c49" +checksum = "94af6efb46fef72616855b036a624cf27ba656ffc9be1b9a3c931cfc7749a9a9" dependencies = [ - "autocfg", - "cfg-if 1.0.0", + "cfg-if", + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ec02e091aa634e2c3ada4a392989e7c3116673ef0ac5b72232439094d73b7fd" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "lazy_static", + "memoffset", + "scopeguard", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d82cfc11ce7f2c3faef78d8a684447b40d503d9681acebed6cb728d45940c4db" +dependencies = [ + "cfg-if", "lazy_static", ] @@ -549,24 +339,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" [[package]] -name = "ctor" -version = "0.1.20" +name = "csv" +version = "1.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e98e2ad1a782e33928b96fc3948e7c355e5af34ba4de7670fe8bac2a3b2006d" +checksum = "22813a6dc45b335f9bade10bf7271dc477e81113e89eb251a0bc2a8a81c536e1" dependencies = [ - "quote", - "syn", + "bstr", + "csv-core", + "itoa", + "ryu", + "serde", ] [[package]] -name = "derive_more" -version = "0.99.11" +name = "csv-core" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41cb0e6161ad61ed084a36ba71fbba9e3ac5aee3606fb607fe08da6acbcf3d8c" +checksum = "2b2466559f260f48ad25fe6317b3c8dac77b5bdb5763ac7d9d6103530663bc90" dependencies = [ - "proc-macro2", - "quote", - "syn", + "memchr", ] [[package]] @@ -578,12 +369,6 @@ dependencies = [ "generic-array", ] -[[package]] -name = "discard" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "212d0f5754cb6769937f4501cc0e67f4f4483c8d2c3e1e922ee9edbe4ab4c7c0" - [[package]] name = "either" version = "1.6.1" @@ -592,11 +377,11 @@ checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" [[package]] name = "encoding_rs" -version = "0.8.24" +version = "0.8.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a51b8cf747471cb9499b6d59e59b0444f4c90eba8968c4e44874e92b5b64ace2" +checksum = "80df024fbc5ac80f87dfef0d9f5209a252f2a497f7f42944cff24d8253cac065" dependencies = [ - "cfg-if 0.1.10", + "cfg-if", ] [[package]] @@ -606,7 +391,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfcf0ed7fe52a17a03854ec54a9f76d6d84508d1c0e66bc1793301c73fc8493c" dependencies = [ "byteorder", - "rand 0.8.3", + "rand 0.8.4", "rustc-hex", "static_assertions", ] @@ -649,54 +434,96 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fed34cd105917e91daa4da6b3728c47b068749d6a62c59811f06ed2ac71d9da7" [[package]] -name = "futures-channel" -version = "0.3.7" +name = "futures" +version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0448174b01148032eed37ac4aed28963aaaa8cfa93569a08e5b479bbc6c2c151" +checksum = "0e7e43a803dae2fa37c1f6a8fe121e1f7bf9548b4dfc0522a42f34145dadfc27" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e682a68b29a882df0545c143dc3646daefe80ba479bcdede94d5a703de2871e2" dependencies = [ "futures-core", + "futures-sink", ] [[package]] name = "futures-core" -version = "0.3.7" +version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18eaa56102984bed2c88ea39026cff3ce3b4c7f508ca970cedf2450ea10d4e46" +checksum = "0402f765d8a89a26043b889b26ce3c4679d268fa6bb22cd7c6aad98340e179d1" [[package]] -name = "futures-io" -version = "0.3.13" +name = "futures-executor" +version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71c2c65c57704c32f5241c1223167c2c3294fd34ac020c807ddbe6db287ba59" - -[[package]] -name = "futures-sink" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e3ca3f17d6e8804ae5d3df7a7d35b2b3a6fe89dac84b31872720fc3060a0b11" - -[[package]] -name = "futures-task" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96d502af37186c4fef99453df03e374683f8a1eec9dcc1e66b3b82dc8278ce3c" +checksum = "badaa6a909fac9e7236d0620a2f57f7664640c56575b71a7552fbd68deafab79" dependencies = [ - "once_cell", + "futures-core", + "futures-task", + "futures-util", ] [[package]] -name = "futures-util" -version = "0.3.7" +name = "futures-io" +version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abcb44342f62e6f3e8ac427b8aa815f724fd705dfad060b18ac7866c15bb8e34" +checksum = "acc499defb3b348f8d8f3f66415835a9131856ff7714bf10dadfc4ec4bdb29a1" + +[[package]] +name = "futures-macro" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4c40298486cdf52cc00cd6d6987892ba502c7656a16a4192a9992b1ccedd121" dependencies = [ + "autocfg", + "proc-macro-hack", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a57bead0ceff0d6dde8f465ecd96c9338121bb7717d3e7b108059531870c4282" + +[[package]] +name = "futures-task" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a16bef9fc1a4dddb5bee51c989e3fbba26569cbb0e31f5b303c184e3dd33dae" + +[[package]] +name = "futures-util" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "feb5c238d27e2bf94ffdfd27b2c29e3df4a68c4193bb6427384259e2bf191967" +dependencies = [ + "autocfg", + "futures-channel", "futures-core", "futures-io", + "futures-macro", "futures-sink", "futures-task", "memchr", - "pin-project", + "pin-project-lite", "pin-utils", + "proc-macro-hack", + "proc-macro-nested", "slab", ] @@ -712,31 +539,31 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.1.15" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc587bc0ec293155d5bfa6b9891ec18a1e330c234f896ea47fbada4cadbe47e6" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" dependencies = [ - "cfg-if 0.1.10", + "cfg-if", "libc", "wasi 0.9.0+wasi-snapshot-preview1", ] [[package]] name = "getrandom" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9495705279e7140bf035dde1f6e750c162df8b625267cd52cc44e0b156732c8" +checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "libc", - "wasi 0.10.0+wasi-snapshot-preview1", + "wasi 0.10.2+wasi-snapshot-preview1", ] [[package]] name = "h2" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc018e188373e2777d0ef2467ebff62a08e66c3f5857b23c8fbec3018210dc00" +checksum = "825343c4eef0b63f541f8903f395dc5beb362a979b5799a84062527ef1e37726" dependencies = [ "bytes", "fnv", @@ -751,6 +578,12 @@ dependencies = [ "tracing", ] +[[package]] +name = "half" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62aca2aba2d62b4a7f5b33f3712cb1b0692779a56fb510499d5c0aa594daeaf3" + [[package]] name = "hashbrown" version = "0.9.1" @@ -758,19 +591,44 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04" [[package]] -name = "heck" -version = "0.3.1" +name = "headers" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205" +checksum = "f0b7591fb62902706ae8e7aaff416b1b0fa2c0fd0878b46dc13baa3712d8a855" +dependencies = [ + "base64", + "bitflags", + "bytes", + "headers-core", + "http", + "mime", + "sha-1", + "time", +] + +[[package]] +name = "headers-core" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429" +dependencies = [ + "http", +] + +[[package]] +name = "heck" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" dependencies = [ "unicode-segmentation", ] [[package]] name = "hermit-abi" -version = "0.1.17" +version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aca5565f760fb5b220e499d72710ed156fdb74e631659e99377d9ebfbd13ae8" +checksum = "322f4de77956e22ed0e5032c359a0f1273f1f7f0d79bfa3b8ffbc730d7fbcc5c" dependencies = [ "libc", ] @@ -783,9 +641,9 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "http" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7245cd7449cc792608c3c8a9eaf69bd4eabbabf802713748fd739c98b82f0747" +checksum = "527e8c9ac747e28542699a951517aa9a6945af506cd1f2e1b53a576c17b6cc11" dependencies = [ "bytes", "fnv", @@ -794,32 +652,32 @@ dependencies = [ [[package]] name = "http-body" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dfb77c123b4e2f72a2069aeae0b4b4949cc7e966df277813fc16347e7549737" +checksum = "60daa14be0e0786db0f03a9e57cb404c9d756eed2b6c62b9ea98ec5743ec75a9" dependencies = [ "bytes", "http", - "pin-project-lite 0.2.6", + "pin-project-lite", ] [[package]] name = "httparse" -version = "1.3.4" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd179ae861f0c2e53da70d892f5f3029f9594be0c41dc5269cd371691b1dc2f9" +checksum = "f3a87b616e37e93c22fb19bcd386f02f3af5ea98a25670ad0fce773de23c5e68" [[package]] name = "httpdate" -version = "0.3.2" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "494b4d60369511e7dea41cf646832512a94e542f68bb9c49e54518e0f468eb47" +checksum = "6456b8a6c8f33fee7d958fcd1b60d55b11940a79e63ae87013e6d22e26034440" [[package]] name = "hyper" -version = "0.14.4" +version = "0.14.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8e946c2b1349055e0b72ae281b238baf1a3ea7307c7e9f9d64673bdd9c26ac7" +checksum = "07d6baa1b441335f3ce5098ac421fb6547c46dda735ca1bc6d0153c838f9dd83" dependencies = [ "bytes", "futures-channel", @@ -831,7 +689,7 @@ dependencies = [ "httparse", "httpdate", "itoa", - "pin-project", + "pin-project-lite", "socket2", "tokio", "tower-service", @@ -854,9 +712,9 @@ dependencies = [ [[package]] name = "idna" -version = "0.2.0" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02e2673c30ee86b5b96a9cb52ad15718aa1f966f5ab9ad54a8b95d5ca33120a9" +checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" dependencies = [ "matches", "unicode-bidi", @@ -883,49 +741,70 @@ dependencies = [ [[package]] name = "indexmap" -version = "1.6.0" +version = "1.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55e2e4c765aa53a0424761bf9f41aa7a6ac1efa87238f59560640e27fca028f2" +checksum = "824845a0bf897a9042383849b02c1bc219c2383772efcd5c6f9766fa4b81aef3" dependencies = [ "autocfg", "hashbrown", ] [[package]] -name = "instant" -version = "0.1.8" +name = "input_buffer" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb1fc4429a33e1f80d41dc9fea4d108a88bec1de8053878898ae448a0b52f613" +checksum = "f97967975f448f1a7ddb12b0bc41069d09ed6a1c161a92687e057325db35d413" dependencies = [ - "cfg-if 1.0.0", + "bytes", +] + +[[package]] +name = "instant" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61124eeebbd69b8190558df225adf7e4caafce0d743919e5d6b19652314ec5ec" +dependencies = [ + "cfg-if", ] [[package]] name = "ipnet" -version = "2.3.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47be2f14c678be2fdcab04ab1171db51b2762ce6f0a8ee87c8dd4a04ed216135" +checksum = "68f2d64f2edebec4ce84ad108148e67e1064789bee435edc5b60ad398714a3a9" [[package]] -name = "itoa" -version = "0.4.6" +name = "itertools" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc6f3ad7b9d11a0c00842ff8de1b60ee58661048eb8049ed33c73594f359d7e6" - -[[package]] -name = "js-sys" -version = "0.3.49" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc15e39392125075f60c95ba416f5381ff6c3a948ff02ab12464715adf56c821" +checksum = "284f18f85651fe11e8a991b2adb42cb078325c996ed026d994719efcfca1d54b" dependencies = [ - "wasm-bindgen", + "either", ] [[package]] -name = "language-tags" -version = "0.2.2" +name = "itertools" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a91d884b6667cd606bb5a69aa0c99ba811a115fc68915e7056ec08a46e93199a" +checksum = "69ddb889f9d0d08a67338271fa9b62996bc788c7796a5c18cf057420aaed5eaf" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736" + +[[package]] +name = "js-sys" +version = "0.3.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83bdfbace3a0e81a4253f73b49e960b053e396a11012cbd49b9b74d6a2b67062" +dependencies = [ + "wasm-bindgen", +] [[package]] name = "lazy_static" @@ -935,26 +814,26 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.91" +version = "0.2.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8916b1f6ca17130ec6568feccee27c156ad12037880833a3b842a823236502e7" +checksum = "12b8adadd720df158f4d70dfe7ccc6adb0472d7c55ca83445f6a5ab3e36f8fb6" [[package]] name = "lock_api" -version = "0.4.1" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28247cc5a5be2f05fbcd76dd0cf2c7d3b5400cb978a28042abcd4fa0b3f8261c" +checksum = "0382880606dff6d15c9476c416d18690b72742aa7b605bb6dd6ec9030fbf07eb" dependencies = [ "scopeguard", ] [[package]] name = "log" -version = "0.4.11" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fabed175da42fed1fa0746b0ea71f412aa9d35e76e95e59b192c64b9dc2bf8b" +checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" dependencies = [ - "cfg-if 0.1.10", + "cfg-if", ] [[package]] @@ -965,9 +844,18 @@ checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" [[package]] name = "memchr" -version = "2.3.3" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400" +checksum = "b16bd47d9e329435e309c58469fe0791c2d0d1ba96ec0954152a5ae2b04387dc" + +[[package]] +name = "memoffset" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59accc507f1338036a0477ef61afdae33cde60840f4dfe481319ce3ad116ddf9" +dependencies = [ + "autocfg", +] [[package]] name = "mime" @@ -976,10 +864,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" [[package]] -name = "mio" -version = "0.7.11" +name = "mime_guess" +version = "2.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf80d3e903b34e0bd7282b218398aec54e082c840d9baf8339e0080a0c542956" +checksum = "2684d4c2e97d99848d30b324b00c8fcc7e5c897b7cbb5819b09e7c90e8baf212" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "mio" +version = "0.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c2bdb6314ec10835cd3293dd268473a835c02b7b352e788be788b3c6ca6bb16" dependencies = [ "libc", "log", @@ -997,6 +895,24 @@ dependencies = [ "winapi", ] +[[package]] +name = "multipart" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d050aeedc89243f5347c3e237e3e13dc76fbe4ae3742a57b94dc14f69acf76d4" +dependencies = [ + "buf_redux", + "httparse", + "log", + "mime", + "mime_guess", + "quick-error", + "rand 0.7.3", + "safemem", + "tempfile", + "twoway", +] + [[package]] name = "native-tls" version = "0.2.7" @@ -1026,9 +942,9 @@ dependencies = [ [[package]] name = "num-integer" -version = "0.1.43" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d59457e662d541ba17869cf51cf177c0b5f0cbf476c66bdc90bf1edac4f875b" +checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" dependencies = [ "autocfg", "num-traits", @@ -1036,9 +952,9 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.12" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac267bcc07f48ee5f8935ab0d24f316fb722d7a1292e2913f0cc196b29ffd611" +checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" dependencies = [ "autocfg", ] @@ -1055,9 +971,15 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.7.2" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af8b08b04175473088b46763e51ee54da5f9a164bc162f615b91bc179dbf15a3" +checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56" + +[[package]] +name = "oorandom" +version = "11.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575" [[package]] name = "opaque-debug" @@ -1067,29 +989,29 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" [[package]] name = "openssl" -version = "0.10.30" +version = "0.10.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d575eff3665419f9b83678ff2815858ad9d11567e082f5ac1814baba4e2bcb4" +checksum = "549430950c79ae24e6d02e0b7404534ecf311d94cc9f861e9e4020187d13d885" dependencies = [ "bitflags", - "cfg-if 0.1.10", + "cfg-if", "foreign-types", - "lazy_static", "libc", + "once_cell", "openssl-sys", ] [[package]] name = "openssl-probe" -version = "0.1.2" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77af24da69f9d9341038eba93a073b1fdaaa1b788221b00a69bce9e762cb32de" +checksum = "28988d872ab76095a6e6ac88d99b54fd267702734fd7ffe610ca27f533ddb95a" [[package]] name = "openssl-sys" -version = "0.9.58" +version = "0.9.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a842db4709b604f0fe5d1170ae3565899be2ad3d9cbc72dedc789ac0511f78de" +checksum = "7a7907e3bfa08bb85105209cdfcb6c63d109f8f6c1ed6ca318fff5c1853fbc1d" dependencies = [ "autocfg", "cc", @@ -1098,17 +1020,11 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "os_str_bytes" -version = "2.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ac6fe3538f701e339953a3ebbe4f39941aababa8a3f6964635b24ab526daeac" - [[package]] name = "parity-scale-codec" -version = "2.0.1" +version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cd3dab59b5cf4bc81069ade0fc470341a1ef3ad5fa73e5a8943bed2ec12b2e8" +checksum = "b310f220c335f9df1b3d2e9fbe3890bbfeef5030dad771620f48c5c229877cd3" dependencies = [ "arrayvec", "bitvec", @@ -1118,9 +1034,9 @@ dependencies = [ [[package]] name = "parking_lot" -version = "0.11.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4893845fa2ca272e647da5d0e46660a314ead9c2fdd9a883aabc32e481a8733" +checksum = "6d7744ac029df22dca6284efe4e898991d28e3085c706c972bcd7da4a27a15eb" dependencies = [ "instant", "lock_api", @@ -1129,12 +1045,11 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.8.0" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c361aa727dd08437f2f1447be8b59a33b0edd15e0fcee698f935613d9efbca9b" +checksum = "fa7a782938e745763fe6907fc6ba86946d72f49fe7e21de074e08128a99fb018" dependencies = [ - "cfg-if 0.1.10", - "cloudabi", + "cfg-if", "instant", "libc", "redox_syscall", @@ -1150,18 +1065,18 @@ checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" [[package]] name = "pin-project" -version = "1.0.1" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee41d838744f60d959d7074e3afb6b35c7456d0f61cad38a24e35e6553f73841" +checksum = "c7509cc106041c40a4518d2af7a61530e1eed0e6285296a3d8c5472806ccc4a4" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.0.1" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81a4ffa594b66bff340084d4081df649a7dc049ac8d7fc458d8e628bfbbb2f86" +checksum = "48c950132583b500556b1efd71d45b319029f2b71518d979fcc208e16b42426f" dependencies = [ "proc-macro2", "quote", @@ -1170,15 +1085,9 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.1.11" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c917123afa01924fc84bb20c4c03f004d9c38e5127e3c039bbf7f4b9c76a2f6b" - -[[package]] -name = "pin-project-lite" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc0e1f259c92177c30a4c9d177246edd0a3568b25756a977d0632cf8fa37e905" +checksum = "8d31d11c69a6b52a174b42bdc0c30e5e11670f90788b2c471c31c1d17d449443" [[package]] name = "pin-utils" @@ -1193,10 +1102,38 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3831453b3449ceb48b6d9c7ad7c96d5ea673e9b470a1dc578c2ce6521230884c" [[package]] -name = "ppv-lite86" -version = "0.2.9" +name = "plotters" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c36fa947111f5c62a733b652544dd0016a43ce89619538a8ef92724a6f501a20" +checksum = "32a3fd9ec30b9749ce28cd91f255d569591cdf937fe280c312143e3c4bad6f2a" +dependencies = [ + "num-traits", + "plotters-backend", + "plotters-svg", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plotters-backend" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d88417318da0eaf0fdcdb51a0ee6c3bed624333bff8f946733049380be67ac1c" + +[[package]] +name = "plotters-svg" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521fa9638fa597e1dc53e9412a4f9cefb01187ee1f7413076f9e6749e2885ba9" +dependencies = [ + "plotters-backend", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" [[package]] name = "primitive-types" @@ -1236,19 +1173,31 @@ dependencies = [ [[package]] name = "proc-macro-hack" -version = "0.5.18" +version = "0.5.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99c605b9a0adc77b7211c6b1f722dcb613d68d66859a44f3d485a6da332b0598" +checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" + +[[package]] +name = "proc-macro-nested" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc881b2c22681370c6a780e47af9840ef841837bc98118431d4e1868bd0c1086" [[package]] name = "proc-macro2" -version = "1.0.24" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71" +checksum = "f0d8caf72986c1a598726adc988bb5984792ef84f5ee5aa50209145ee8077038" dependencies = [ "unicode-xid", ] +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + [[package]] name = "quote" version = "1.0.9" @@ -1270,7 +1219,7 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" dependencies = [ - "getrandom 0.1.15", + "getrandom 0.1.16", "libc", "rand_chacha 0.2.2", "rand_core 0.5.1", @@ -1279,14 +1228,14 @@ dependencies = [ [[package]] name = "rand" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ef9e7e66b4468674bfcb0c81af8b7fa0bb154fa9f28eb840da5c447baeb8d7e" +checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8" dependencies = [ "libc", - "rand_chacha 0.3.0", - "rand_core 0.6.2", - "rand_hc 0.3.0", + "rand_chacha 0.3.1", + "rand_core 0.6.3", + "rand_hc 0.3.1", ] [[package]] @@ -1301,12 +1250,12 @@ dependencies = [ [[package]] name = "rand_chacha" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e12735cf05c9e10bf21534da50a147b924d555dc7a547c42e6bb2d5b6017ae0d" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core 0.6.2", + "rand_core 0.6.3", ] [[package]] @@ -1315,16 +1264,16 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" dependencies = [ - "getrandom 0.1.15", + "getrandom 0.1.16", ] [[package]] name = "rand_core" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34cf66eb183df1c5876e2dcf6b13d57340741e8dc255b48e40a26de954d06ae7" +checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" dependencies = [ - "getrandom 0.2.2", + "getrandom 0.2.3", ] [[package]] @@ -1338,36 +1287,69 @@ dependencies = [ [[package]] name = "rand_hc" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3190ef7066a446f2e7f42e239d161e905420ccab01eb967c9eb27d21b2322a73" +checksum = "d51e9f596de227fda2ea6c84607f5558e196eeaf43c986b724ba4fb8fdf497e7" dependencies = [ - "rand_core 0.6.2", + "rand_core 0.6.3", +] + +[[package]] +name = "rayon" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06aca804d41dbc8ba42dfd964f0d01334eceb64314b9ecf7c5fad5188a06d90" +dependencies = [ + "autocfg", + "crossbeam-deque", + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d78120e2c850279833f1dd3582f730c4ab53ed95aeaaaa862a2a5c71b1656d8e" +dependencies = [ + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-utils", + "lazy_static", + "num_cpus", ] [[package]] name = "redox_syscall" -version = "0.1.57" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" +checksum = "5ab49abadf3f9e1c4bc499e8845e152ad87d2ad2d30371841171169e9d75feee" +dependencies = [ + "bitflags", +] [[package]] name = "regex" -version = "1.4.1" +version = "1.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8963b85b8ce3074fecffde43b4b0dded83ce2f367dc8d363afc56679f3ee820b" +checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" dependencies = [ "aho-corasick", "memchr", "regex-syntax", - "thread_local", ] [[package]] -name = "regex-syntax" -version = "0.6.20" +name = "regex-automata" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cab7a364d15cde1e505267766a2d3c4e22a843e1a601f0fa7564c0f82ced11c" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" + +[[package]] +name = "regex-syntax" +version = "0.6.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" [[package]] name = "remove_dir_all" @@ -1380,9 +1362,9 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.11.2" +version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf12057f289428dbf5c591c74bf10392e4a8003f993405a902f20117019022d4" +checksum = "246e9f61b9bb77df069a947682be06e31ac43ea37862e244a69f177694ea6d22" dependencies = [ "base64", "bytes", @@ -1400,7 +1382,7 @@ dependencies = [ "mime", "native-tls", "percent-encoding", - "pin-project-lite 0.2.6", + "pin-project-lite", "serde", "serde_json", "serde_urlencoded", @@ -1427,9 +1409,9 @@ checksum = "3e75f6a532d0fd9f7f13144f392b6ad56a32696bfcd9c78f797f16bbb6f072d6" [[package]] name = "rustc_version" -version = "0.2.3" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" dependencies = [ "semver", ] @@ -1440,6 +1422,21 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" +[[package]] +name = "safemem" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "schannel" version = "0.1.19" @@ -1450,6 +1447,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "scoped-tls" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6a9290e3c9cf0f18145ef7ffa62d68ee0bf5fcd651017e586dc7fd5da448c2" + [[package]] name = "scopeguard" version = "1.1.0" @@ -1458,9 +1461,9 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" [[package]] name = "security-framework" -version = "2.1.2" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d493c5f39e02dfb062cd8f33301f90f9b13b650e8c1b1d0fd75c19dd64bff69d" +checksum = "23a2ac85147a3a11d77ecf1bc7166ec0b92febfa4461c37944e180f319ece467" dependencies = [ "bitflags", "core-foundation", @@ -1471,9 +1474,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.1.1" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dee48cdde5ed250b0d3252818f646e174ab414036edb884dde62d80a3ac6082d" +checksum = "7e4effb91b4b8b6fb7732e670b6cee160278ff8e6bf485c7805d9e319d76e284" dependencies = [ "core-foundation-sys", "libc", @@ -1481,33 +1484,34 @@ dependencies = [ [[package]] name = "semver" -version = "0.9.0" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" -dependencies = [ - "semver-parser", -] - -[[package]] -name = "semver-parser" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" +checksum = "5f3aac57ee7f3272d8395c6e4f502f434f0e289fcd62876f70daa008c20dcabe" [[package]] name = "serde" -version = "1.0.117" +version = "1.0.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b88fa983de7720629c9387e9f517353ed404164b1e482c970a90c1a4aaf7dc1a" +checksum = "ec7505abeacaec74ae4778d9d9328fe5a5d04253220a85c4ee022239fc996d03" dependencies = [ "serde_derive", ] [[package]] -name = "serde_derive" -version = "1.0.117" +name = "serde_cbor" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbd1ae72adb44aab48f325a02444a5fc079349a8d804c1fc922aed3f7454c74e" +checksum = "1e18acfa2f90e8b735b2836ab8d538de304cbb6729a7360729ea5a895d15a622" +dependencies = [ + "half", + "serde", +] + +[[package]] +name = "serde_derive" +version = "1.0.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "963a7dbc9895aeac7ac90e74f34a5d5261828f79df35cbed41e10189d3804d43" dependencies = [ "proc-macro2", "quote", @@ -1516,9 +1520,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.59" +version = "1.0.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcac07dbffa1c65e7f816ab9eba78eb142c6d44410f4eeba1e26e4f5dfa56b95" +checksum = "799e97dc9fdae36a5c8b8f2cae9ce2ee9fdce2058c57a93e6099d919fd982f79" dependencies = [ "itoa", "ryu", @@ -1539,30 +1543,33 @@ dependencies = [ [[package]] name = "sha-1" -version = "0.9.1" +version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "170a36ea86c864a3f16dd2687712dd6646f7019f301e57537c7f4dc9f5916770" +checksum = "8c4cfa741c5832d0ef7fab46cabed29c2aae926db0b11bb2069edd8db5e64e16" dependencies = [ "block-buffer", - "cfg-if 0.1.10", - "cpuid-bool", + "cfg-if", + "cpufeatures", "digest", "opaque-debug", ] [[package]] -name = "sha1" -version = "0.6.0" +name = "shellwords" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2579985fda508104f7587689507983eadd6a6e84dd35d6d115361f530916fa0d" +checksum = "89e515aa4699a88148ed5ef96413ceef0048ce95b43fbc955a33bde0a70fcae6" +dependencies = [ + "lazy_static", + "regex", +] [[package]] name = "signal-hook-registry" -version = "1.2.1" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e12110bc539e657a646068aaf5eb5b63af9d0c1f7b29c97113fad80e15f035" +checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" dependencies = [ - "arc-swap", "libc", ] @@ -1581,9 +1588,9 @@ dependencies = [ [[package]] name = "slab" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8" +checksum = "f173ac3d1a7e3b28003f40de0b5ce7fe2710f9b9dc3fc38664cebee46b3b6527" [[package]] name = "smallvec" @@ -1593,22 +1600,27 @@ checksum = "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e" [[package]] name = "socket2" -version = "0.3.19" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "122e570113d28d773067fab24266b66753f6ea915758651696b6e35e49f88d6e" +checksum = "9e3dfc207c526015c632472a77be09cf1b6e46866581aecae5cc38fb4235dea2" dependencies = [ - "cfg-if 1.0.0", "libc", "winapi", ] [[package]] -name = "standback" -version = "0.2.17" +name = "soketto" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e113fb6f3de07a243d434a56ec6f186dfd51cb08448239fe7bcae73f87ff28ff" +checksum = "a74e48087dbeed4833785c2f3352b59140095dc192dce966a3bfc155020a439f" dependencies = [ - "version_check", + "base64", + "bytes", + "futures", + "httparse", + "log", + "rand 0.8.4", + "sha-1", ] [[package]] @@ -1617,66 +1629,41 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" -[[package]] -name = "stdweb" -version = "0.4.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d022496b16281348b52d0e30ae99e01a73d737b2f45d38fed4edf79f9325a1d5" -dependencies = [ - "discard", - "rustc_version", - "stdweb-derive", - "stdweb-internal-macros", - "stdweb-internal-runtime", - "wasm-bindgen", -] - -[[package]] -name = "stdweb-derive" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c87a60a40fccc84bef0652345bbbbbe20a605bf5d0ce81719fc476f5c03b50ef" -dependencies = [ - "proc-macro2", - "quote", - "serde", - "serde_derive", - "syn", -] - -[[package]] -name = "stdweb-internal-macros" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58fa5ff6ad0d98d1ffa8cb115892b6e69d67799f6763e162a1c9db421dc22e11" -dependencies = [ - "base-x", - "proc-macro2", - "quote", - "serde", - "serde_derive", - "serde_json", - "sha1", - "syn", -] - -[[package]] -name = "stdweb-internal-runtime" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "213701ba3370744dcd1a12960caa4843b3d68b4d1c0a5d575e0d65b2ee9d16c0" - [[package]] name = "strsim" -version = "0.10.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" + +[[package]] +name = "structopt" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5277acd7ee46e63e5168a80734c9f6ee81b1367a7d8772a2d765df2a3705d28c" +dependencies = [ + "clap", + "lazy_static", + "structopt-derive", +] + +[[package]] +name = "structopt-derive" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ba9cdfda491b814720b6b06e0cac513d922fc407582032e8706e9f137976f90" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] [[package]] name = "syn" -version = "1.0.65" +version = "1.0.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3a1d708c221c5a612956ef9f75b37e454e88d1f7b899fbd3a18d4252012d663" +checksum = "f71489ff30030d2ae598524f61326b902466f72a0fb1a8564c001cc63425bcc7" dependencies = [ "proc-macro2", "quote", @@ -1690,153 +1677,160 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] -name = "telemetry" -version = "0.3.0" +name = "telemetry_core" +version = "0.1.0" dependencies = [ - "actix", - "actix-http", - "actix-web", - "actix-web-actors", + "anyhow", + "bimap", "bincode", "bytes", - "chrono", - "clap", - "ctor", - "fnv", + "common", + "criterion", + "futures", "hex", + "http", "log", - "num-traits", + "once_cell", "parking_lot", "primitive-types", "reqwest", "rustc-hash", "serde", "serde_json", + "shellwords", "simple_logger", + "smallvec", + "soketto", + "structopt", + "test_utils", "thiserror", + "tokio", + "tokio-util", + "warp", +] + +[[package]] +name = "telemetry_shard" +version = "0.1.0" +dependencies = [ + "anyhow", + "bincode", + "common", + "futures", + "hex", + "http", + "log", + "primitive-types", + "serde", + "serde_json", + "simple_logger", + "soketto", + "structopt", + "thiserror", + "tokio", + "tokio-util", + "warp", ] [[package]] name = "tempfile" -version = "3.1.0" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a6e24d9338a0a5be79593e2fa15a648add6138caa803e2d5bc782c371732ca9" +checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22" dependencies = [ - "cfg-if 0.1.10", + "cfg-if", "libc", - "rand 0.7.3", + "rand 0.8.4", "redox_syscall", "remove_dir_all", "winapi", ] [[package]] -name = "termcolor" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb6bfa289a4d7c5766392812c0a1f4c1ba45afa1ad47803c11e1f407d846d75f" +name = "test_utils" +version = "0.1.0" dependencies = [ - "winapi-util", + "anyhow", + "common", + "futures", + "http", + "log", + "serde_json", + "soketto", + "thiserror", + "tokio", + "tokio-util", ] [[package]] name = "textwrap" -version = "0.12.1" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "203008d98caf094106cfaba70acfed15e18ed3ddb7d94e49baec153a2b462789" +checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" dependencies = [ "unicode-width", ] [[package]] name = "thiserror" -version = "1.0.24" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0f4a65597094d4483ddaed134f409b2cb7c1beccf25201a9f73c719254fa98e" +checksum = "fa6f76457f59514c7eeb4e59d891395fab0b2fd1d40723ae737d64153392e9c6" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.24" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7765189610d8241a44529806d6fd1f2e0a08734313a35d5b3a556f92b381f3c0" +checksum = "8a36768c0fbf1bb15eca10defa29526bda730a2376c2ab4393ccfa16fb1a318d" dependencies = [ "proc-macro2", "quote", "syn", ] -[[package]] -name = "thread_local" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d40c6d1b69745a6ec6fb1ca717914848da4b44ae29d9b3080cbee91d72a69b14" -dependencies = [ - "lazy_static", -] - [[package]] name = "time" -version = "0.1.44" +version = "0.1.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" +checksum = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438" dependencies = [ "libc", - "wasi 0.10.0+wasi-snapshot-preview1", "winapi", ] [[package]] -name = "time" -version = "0.2.26" +name = "tinytemplate" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08a8cbfbf47955132d0202d1662f49b2423ae35862aee471f3ba4b133358f372" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" dependencies = [ - "const_fn", - "libc", - "standback", - "stdweb", - "time-macros", - "version_check", - "winapi", -] - -[[package]] -name = "time-macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "957e9c6e26f12cb6d0dd7fc776bb67a706312e7299aed74c8dd5b17ebb27e2f1" -dependencies = [ - "proc-macro-hack", - "time-macros-impl", -] - -[[package]] -name = "time-macros-impl" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5c3be1edfad6027c69f5491cf4cb310d1a71ecd6af742788c6ff8bced86b8fa" -dependencies = [ - "proc-macro-hack", - "proc-macro2", - "quote", - "standback", - "syn", + "serde", + "serde_json", ] [[package]] name = "tinyvec" -version = "0.3.4" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "238ce071d267c5710f9d31451efec16c5ee22de34df17cc05e56cbc92e967117" +checksum = "5b5220f05bb7de7f3f53c7c065e1199b3172696fe2db9f9c4d8ad9b4ee74c342" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.4.0" +version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "134af885d758d645f0f0505c9a8b3f9bf8a348fd822e112ab5248138348f1722" +checksum = "c2602b8af3767c285202012822834005f596c811042315fa7e9f5b12b2a43207" dependencies = [ "autocfg", "bytes", @@ -1846,11 +1840,23 @@ dependencies = [ "num_cpus", "once_cell", "parking_lot", - "pin-project-lite 0.2.6", + "pin-project-lite", "signal-hook-registry", + "tokio-macros", "winapi", ] +[[package]] +name = "tokio-macros" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c49e3df43841dafb86046472506755d8501c5615673955f6aa17181125d13c37" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "tokio-native-tls" version = "0.3.0" @@ -1862,16 +1868,42 @@ dependencies = [ ] [[package]] -name = "tokio-util" -version = "0.6.5" +name = "tokio-stream" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5143d049e85af7fbc36f5454d990e62c2df705b3589f123b71f441b6b59f443f" +checksum = "f8864d706fdb3cc0843a49647ac892720dac98a6eeb818b77190592cf4994066" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1a5f475f1b9d077ea1017ecbc60890fda8e54942d680ca0b1d2b47cfa2d861b" +dependencies = [ + "futures-util", + "log", + "pin-project", + "tokio", + "tungstenite", +] + +[[package]] +name = "tokio-util" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1caa0b0c8d94a049db56b5acf8cba99dc0623aab1b26d5b5f5e2d945846b3592" dependencies = [ "bytes", "futures-core", + "futures-io", "futures-sink", "log", - "pin-project-lite 0.2.6", + "pin-project-lite", + "slab", "tokio", ] @@ -1883,20 +1915,21 @@ checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6" [[package]] name = "tracing" -version = "0.1.21" +version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0987850db3733619253fe60e17cb59b82d37c7e6c0236bb81e4d6b87c879f27" +checksum = "09adeb8c97449311ccd28a427f96fb563e7fd31aabf994189879d9da2394b89d" dependencies = [ - "cfg-if 0.1.10", - "pin-project-lite 0.1.11", + "cfg-if", + "log", + "pin-project-lite", "tracing-core", ] [[package]] name = "tracing-core" -version = "0.1.17" +version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f50de3927f93d202783f4513cda820ab47ef17f624b03c096e86ef00c67e6b5f" +checksum = "a9ff14f98b1a4b289c6248a023c1c2fa1491062964e9fed67ab29c4e4da4a052" dependencies = [ "lazy_static", ] @@ -1908,10 +1941,38 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" [[package]] -name = "typenum" -version = "1.12.0" +name = "tungstenite" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "373c8a200f9e67a0c95e62a4f52fbf80c23b4381c05a17845531982fa99e6b33" +checksum = "8ada8297e8d70872fa9a551d93250a9f407beb9f37ef86494eb20012a2ff7c24" +dependencies = [ + "base64", + "byteorder", + "bytes", + "http", + "httparse", + "input_buffer", + "log", + "rand 0.8.4", + "sha-1", + "url", + "utf-8", +] + +[[package]] +name = "twoway" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59b11b2b5241ba34be09c3cc85a36e56e48f9888862e19cedf23336d35316ed1" +dependencies = [ + "memchr", +] + +[[package]] +name = "typenum" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f6906492a7cd215bfa4cf595b600146ccfac0c79bcbd1f3000162af5e8b06" [[package]] name = "uint" @@ -1926,28 +1987,37 @@ dependencies = [ ] [[package]] -name = "unicode-bidi" -version = "0.3.4" +name = "unicase" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f2bd0c6468a8230e1db229cff8029217cf623c767ea5d60bfbd42729ea54d5" +checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" +dependencies = [ + "version_check", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eeb8be209bb1c96b7c177c7420d26e04eccacb0eeae6b980e35fcb74678107e0" dependencies = [ "matches", ] [[package]] name = "unicode-normalization" -version = "0.1.13" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fb19cf769fa8c6a80a162df694621ebeb4dafb606470b2b2fce0be40a98a977" +checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9" dependencies = [ "tinyvec", ] [[package]] name = "unicode-segmentation" -version = "1.6.0" +version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e83e153d1053cbb5a118eeff7fd5be06ed99153f00dbcd8ae310c5fb2b22edc0" +checksum = "bb0d2e7be6ae3a5fa87eed5fb451aff96f2573d2694942e40543ae0bbe19c796" [[package]] name = "unicode-width" @@ -1957,15 +2027,15 @@ checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3" [[package]] name = "unicode-xid" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" +checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" [[package]] name = "url" -version = "2.2.1" +version = "2.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ccd964113622c8e9322cfac19eb1004a07e636c545f325da085d5cdde6f1f8b" +checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c" dependencies = [ "form_urlencoded", "idna", @@ -1974,10 +2044,16 @@ dependencies = [ ] [[package]] -name = "vcpkg" -version = "0.2.10" +name = "utf-8" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6454029bf181f092ad1b853286f23e2c507d8e8194d01d92da4a55c274a5508c" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] name = "vec_map" @@ -1987,9 +2063,20 @@ checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" [[package]] name = "version_check" -version = "0.9.2" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed" +checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" + +[[package]] +name = "walkdir" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" +dependencies = [ + "same-file", + "winapi", + "winapi-util", +] [[package]] name = "want" @@ -2001,6 +2088,35 @@ dependencies = [ "try-lock", ] +[[package]] +name = "warp" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "332d47745e9a0c38636dbd454729b147d16bd1ed08ae67b3ab281c4506771054" +dependencies = [ + "bytes", + "futures", + "headers", + "http", + "hyper", + "log", + "mime", + "mime_guess", + "multipart", + "percent-encoding", + "pin-project", + "scoped-tls", + "serde", + "serde_json", + "serde_urlencoded", + "tokio", + "tokio-stream", + "tokio-tungstenite", + "tokio-util", + "tower-service", + "tracing", +] + [[package]] name = "wasi" version = "0.9.0+wasi-snapshot-preview1" @@ -2009,17 +2125,17 @@ checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" [[package]] name = "wasi" -version = "0.10.0+wasi-snapshot-preview1" +version = "0.10.2+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" +checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" [[package]] name = "wasm-bindgen" -version = "0.2.72" +version = "0.2.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fe8f61dba8e5d645a4d8132dc7a0a66861ed5e1045d2c0ed940fab33bac0fbe" +checksum = "d54ee1d4ed486f78874278e63e4069fc1ab9f6a18ca492076ffb90c5eb2997fd" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "serde", "serde_json", "wasm-bindgen-macro", @@ -2027,9 +2143,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.72" +version = "0.2.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "046ceba58ff062da072c7cb4ba5b22a37f00a302483f7e2a6cdc18fedbdc1fd3" +checksum = "3b33f6a0694ccfea53d94db8b2ed1c3a8a4c86dd936b13b9f0a15ec4a451b900" dependencies = [ "bumpalo", "lazy_static", @@ -2042,11 +2158,11 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.22" +version = "0.4.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73157efb9af26fb564bb59a009afd1c7c334a44db171d280690d0c3faaec3468" +checksum = "5fba7978c679d53ce2d0ac80c8c175840feb849a161664365d1287b41f2e67f1" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "js-sys", "wasm-bindgen", "web-sys", @@ -2054,9 +2170,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.72" +version = "0.2.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ef9aa01d36cda046f797c57959ff5f3c615c9cc63997a8d545831ec7976819b" +checksum = "088169ca61430fe1e58b8096c24975251700e7b1f6fd91cc9d59b04fb9b18bd4" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2064,9 +2180,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.72" +version = "0.2.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96eb45c1b2ee33545a813a92dbb53856418bf7eb54ab34f7f7ff1448a5b3735d" +checksum = "be2241542ff3d9f241f5e2cb6dd09b37efe786df8851c54957683a49f0987a97" dependencies = [ "proc-macro2", "quote", @@ -2077,15 +2193,15 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.72" +version = "0.2.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7148f4696fb4960a346eaa60bbfb42a1ac4ebba21f750f75fc1375b098d5ffa" +checksum = "d7cff876b8f18eed75a66cf49b65e7f967cb354a7aa16003fb55dbfd25b44b4f" [[package]] name = "web-sys" -version = "0.3.49" +version = "0.3.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59fe19d70f5dacc03f6e46777213facae5ac3801575d56ca6cbd4c93dcd12310" +checksum = "e828417b379f3df7111d3a2a9e5753706cae29c41f7c4029ee9fd77f3e09e582" dependencies = [ "js-sys", "wasm-bindgen", diff --git a/backend/Cargo.toml b/backend/Cargo.toml index 2a6589f..518633a 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -1,6 +1,9 @@ [workspace] members = [ - "core", + "common", + "telemetry_core", + "telemetry_shard", + "test_utils" ] [profile.dev] @@ -9,3 +12,7 @@ opt-level = 3 [profile.release] lto = true panic = "abort" +## Enabling these seems necessary to get +## good debug info in Instruments: +# debug = true +# codegen-units = 1 \ No newline at end of file diff --git a/backend/Dockerfile b/backend/Dockerfile index 52b5760..0ffd8c6 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -10,15 +10,15 @@ RUN cargo build --${PROFILE} --bins # MAIN IMAGE FOR PEOPLE TO PULL --- small one# FROM docker.io/debian:buster-slim LABEL maintainer="Parity Technologies" -LABEL description="Polkadot Telemetry backend, static build" +LABEL description="Polkadot Telemetry backend shard/core binaries, static build" ARG PROFILE=release WORKDIR /usr/local/bin COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ -COPY --from=builder /app/target/$PROFILE/telemetry /usr/local/bin +COPY --from=builder /app/target/$PROFILE/telemetry_shard /usr/local/bin +COPY --from=builder /app/target/$PROFILE/telemetry_core /usr/local/bin RUN apt-get -y update && apt-get -y install openssl && apt-get autoremove -y && apt-get clean && rm -rf /var/lib/apt/lists/ EXPOSE 8000 -ENTRYPOINT ["telemetry"] diff --git a/backend/README.md b/backend/README.md new file mode 100644 index 0000000..984faed --- /dev/null +++ b/backend/README.md @@ -0,0 +1,19 @@ +# Backend Crates + +This folder contains the rust crates and documentation specific to the telemetry backend. A description of the folders: + +- [telemetry_core](./telemetry_core): The Telemetry Core. This aggregates data received from shards and allows UI feeds to connect and receive this information. +- [telemetry_shard](./telemetry_shard): A Shard. It's expected that multiple of these will run. Nodes will connect to Shard instances and send JSON telemetry to them, and Shard instances will each connect to the Telemetry Core and relay on relevant data to it. +- [common](./common): common code shared between the telemetry shard and core +- [test_utils](./test_utils): Test utilities, primarily focused around making it easy to run end-to-end tests. +- [docs](./docs): Material supporting the documentation lives here + +# Architecture + +As we move to a sharded version of this telemetry server, this set of architecture diagrams may be useful in helping to understand the current setup (middle diagram), previous setup (first diagram) and possible future setup if we need to scale further (last diagram): + +![Architecture Diagram](./docs/architecture.svg) + +# Deployment + +A `Dockerfile` exists which builds the Shard and Telemetry Core binaries into an image. A `docker-compose.yaml` in the root of the repository can serve as an example of these services, along with the UI, running together. \ No newline at end of file diff --git a/backend/core/Cargo.toml b/backend/common/Cargo.toml similarity index 52% rename from backend/core/Cargo.toml rename to backend/common/Cargo.toml index 8ea9c50..1b38b09 100644 --- a/backend/core/Cargo.toml +++ b/backend/common/Cargo.toml @@ -1,29 +1,28 @@ [package] -name = "telemetry" -version = "0.3.0" +name = "common" +version = "0.1.0" authors = ["Parity Technologies Ltd. "] edition = "2018" license = "GPL-3.0" [dependencies] -actix = "0.11.1" -actix-web = { version = "4.0.0-beta.4", default-features = false } -actix-web-actors = "4.0.0-beta.3" -actix-http = "3.0.0-beta.4" -bincode = "1.3.3" +bimap = "0.6.1" bytes = "1.0.1" -chrono = { version = "0.4", features = ["serde"] } fnv = "1.0.7" +futures = "0.3.15" hex = "0.4.3" +http = "0.2.4" +log = "0.4" +num-traits = "0.2" +pin-project-lite = "0.2.7" +primitive-types = { version = "0.9.0", features = ["serde"] } +rustc-hash = "1.1.0" serde = { version = "1.0", features = ["derive"] } serde_json = { version = "1.0", features = ["raw_value"] } +soketto = "0.6.0" thiserror = "1.0.24" -primitive-types = { version = "0.9.0", features = ["serde"] } -log = "0.4" -simple_logger = "1.11.0" -num-traits = "0.2" -parking_lot = "0.11" -reqwest = { version = "0.11.1", features = ["blocking", "json"] } -rustc-hash = "1.1.0" -clap = "3.0.0-beta.2" -ctor = "0.1.20" +tokio = { version = "1.8.2", features = ["full"] } +tokio-util = { version = "0.6", features = ["compat"] } + +[dev-dependencies] +bincode = "1.3.3" diff --git a/backend/common/src/assign_id.rs b/backend/common/src/assign_id.rs new file mode 100644 index 0000000..1bcd42f --- /dev/null +++ b/backend/common/src/assign_id.rs @@ -0,0 +1,68 @@ +use bimap::BiMap; +use std::hash::Hash; + +/// A struct that allows you to assign an Id to an arbitrary set of +/// details (so long as they are Eq+Hash+Clone), and then access +/// the assigned Id given those details or access the details given +/// the Id. +/// +/// The Id can be any type that's convertible to/from a `usize`. Using +/// a custom type is recommended for increased type safety. +#[derive(Debug)] +pub struct AssignId { + current_id: usize, + mapping: BiMap, + _id_type: std::marker::PhantomData, +} + +impl AssignId +where + Details: Eq + Hash, + Id: From + Copy, + usize: From, +{ + pub fn new() -> Self { + Self { + current_id: 0, + mapping: BiMap::new(), + _id_type: std::marker::PhantomData, + } + } + + pub fn assign_id(&mut self, details: Details) -> Id { + let this_id = self.current_id; + self.current_id += 1; + self.mapping.insert(this_id, details); + this_id.into() + } + + pub fn get_details(&mut self, id: Id) -> Option<&Details> { + self.mapping.get_by_left(&id.into()) + } + + pub fn get_id(&mut self, details: &Details) -> Option { + self.mapping.get_by_right(details).map(|&id| id.into()) + } + + pub fn remove_by_id(&mut self, id: Id) -> Option
{ + self.mapping + .remove_by_left(&id.into()) + .map(|(_, details)| details) + } + + pub fn remove_by_details(&mut self, details: &Details) -> Option { + self.mapping + .remove_by_right(&details) + .map(|(id, _)| id.into()) + } + + pub fn clear(&mut self) { + *self = AssignId::new(); + } + + pub fn iter(&self) -> impl Iterator { + self.mapping + .iter() + .map(|(&id, details)| (id.into(), details)) + } +} diff --git a/backend/core/src/util/dense_map.rs b/backend/common/src/dense_map.rs similarity index 51% rename from backend/core/src/util/dense_map.rs rename to backend/common/src/dense_map.rs index 8c06af5..a631cff 100644 --- a/backend/core/src/util/dense_map.rs +++ b/backend/common/src/dense_map.rs @@ -1,17 +1,31 @@ -pub type Id = usize; - -pub struct DenseMap { +/// This stores items in contiguous memory, making a note of free +/// slots when items are removed again so that they can be reused. +/// +/// This is particularly efficient when items are often added and +/// seldom removed. +/// +/// Items are keyed by an Id, which can be any type you wish, but +/// must be convertible to/from a `usize`. This promotes using a +/// custom Id type to talk about items in the map. +pub struct DenseMap { /// List of retired indexes that can be re-used - retired: Vec, + retired: Vec, /// All items items: Vec>, + /// Our ID type + _id_type: std::marker::PhantomData, } -impl DenseMap { +impl DenseMap +where + Id: From + Copy, + usize: From, +{ pub fn new() -> Self { DenseMap { retired: Vec::new(), items: Vec::new(), + _id_type: std::marker::PhantomData, } } @@ -25,11 +39,12 @@ impl DenseMap { { match self.retired.pop() { Some(id) => { - self.items[id] = Some(f(id)); - id + let id_out = id.into(); + self.items[id] = Some(f(id_out)); + id_out } None => { - let id = self.items.len(); + let id = self.items.len().into(); self.items.push(Some(f(id))); id } @@ -37,14 +52,17 @@ impl DenseMap { } pub fn get(&self, id: Id) -> Option<&T> { + let id: usize = id.into(); self.items.get(id).and_then(|item| item.as_ref()) } pub fn get_mut(&mut self, id: Id) -> Option<&mut T> { + let id: usize = id.into(); self.items.get_mut(id).and_then(|item| item.as_mut()) } pub fn remove(&mut self, id: Id) -> Option { + let id: usize = id.into(); let old = self.items.get_mut(id).and_then(|item| item.take()); if old.is_some() { @@ -60,14 +78,21 @@ impl DenseMap { self.items .iter() .enumerate() - .filter_map(|(id, item)| Some((id, item.as_ref()?))) + .filter_map(|(id, item)| Some((id.into(), item.as_ref()?))) } pub fn iter_mut(&mut self) -> impl Iterator + '_ { self.items .iter_mut() .enumerate() - .filter_map(|(id, item)| Some((id, item.as_mut()?))) + .filter_map(|(id, item)| Some((id.into(), item.as_mut()?))) + } + + pub fn into_iter(self) -> impl Iterator { + self.items + .into_iter() + .enumerate() + .filter_map(|(id, item)| Some((id.into(), item?))) } pub fn len(&self) -> usize { @@ -77,4 +102,12 @@ impl DenseMap { pub fn is_empty(&self) -> bool { self.len() == 0 } + + /// Return the next Id that will be assigned. + pub fn next_id(&self) -> usize { + match self.retired.last() { + Some(id) => *id, + None => self.items.len(), + } + } } diff --git a/backend/common/src/id_type.rs b/backend/common/src/id_type.rs new file mode 100644 index 0000000..d3165a5 --- /dev/null +++ b/backend/common/src/id_type.rs @@ -0,0 +1,74 @@ +/// Define a type that can be used as an ID, be converted from/to the inner type, +/// and serialized/deserialized transparently into the inner type. +#[macro_export] +macro_rules! id_type { + ($( #[$attrs:meta] )* $vis:vis struct $ty:ident ( $inner:ident ) $(;)? ) => { + #[derive(Debug,Clone,Copy,PartialEq,Eq,Hash)] + $( #[$attrs] )* + $vis struct $ty($inner); + + impl $ty { + #[allow(dead_code)] + pub fn new(inner: $inner) -> Self { + Self(inner) + } + } + + impl From<$inner> for $ty { + fn from(inner: $inner) -> Self { + Self(inner) + } + } + + impl From<$ty> for $inner { + fn from(ty: $ty) -> Self { + ty.0 + } + } + } +} + +#[cfg(test)] +mod test { + //! Mostly we're just checking that everything compiles OK + //! when the macro is used as expected.. + + // A basic definition is possible: + id_type! { + struct Foo(usize) + } + + // We can add a ';' on the end: + id_type! { + struct Bar(usize); + } + + // Visibility qualifiers are allowed: + id_type! { + pub struct Wibble(u64) + } + + // Doc strings are possible + id_type! { + /// We can have doc strings, too + pub(crate) struct Wobble(u16) + } + + // In fact, any attributes can be added (common + // derives are added already): + id_type! { + /// We can have doc strings, too + #[derive(serde::Serialize)] + #[serde(transparent)] + pub(crate) struct Lark(u16) + } + + #[test] + fn create_and_use_new_id_type() { + let _ = Foo::new(123); + let id = Foo::from(123); + let id_num: usize = id.into(); + + assert_eq!(id_num, 123); + } +} diff --git a/backend/common/src/internal_messages.rs b/backend/common/src/internal_messages.rs new file mode 100644 index 0000000..941dfeb --- /dev/null +++ b/backend/common/src/internal_messages.rs @@ -0,0 +1,51 @@ +//! Internal messages passed between the shard and telemetry core. + +use std::net::IpAddr; + +use crate::id_type; +use crate::node_message::Payload; +use crate::node_types::{BlockHash, NodeDetails}; +use serde::{Deserialize, Serialize}; + +id_type! { + /// The shard-local ID of a given node, where a single connection + /// might send data on behalf of more than one chain. + #[derive(serde::Serialize, serde::Deserialize)] + pub struct ShardNodeId(usize); +} + +/// Message sent from a telemetry shard to the telemetry core +#[derive(Deserialize, Serialize, Debug, Clone)] +pub enum FromShardAggregator { + /// Get information about a new node, including it's IP + /// address and chain genesis hash. + AddNode { + ip: Option, + node: NodeDetails, + local_id: ShardNodeId, + genesis_hash: BlockHash, + }, + /// A message payload with updated details for a node + UpdateNode { + local_id: ShardNodeId, + payload: Payload, + }, + /// Inform the telemetry core that a node has been removed + RemoveNode { local_id: ShardNodeId }, +} + +/// Message sent form the telemetry core to a telemetry shard +#[derive(Deserialize, Serialize, Debug, Clone)] +pub enum FromTelemetryCore { + Mute { + local_id: ShardNodeId, + reason: MuteReason, + }, +} + +/// Why is the thing being muted? +#[derive(Deserialize, Serialize, Debug, Clone)] +pub enum MuteReason { + Overquota, + ChainNotAllowed, +} diff --git a/backend/common/src/lib.rs b/backend/common/src/lib.rs new file mode 100644 index 0000000..4eaab74 --- /dev/null +++ b/backend/common/src/lib.rs @@ -0,0 +1,20 @@ +pub mod id_type; +pub mod internal_messages; +pub mod node_message; +pub mod node_types; +pub mod time; +pub mod ws_client; +pub mod ready_chunks_all; + +mod assign_id; +mod dense_map; +mod mean_list; +mod most_seen; +mod num_stats; + +// Export a bunch of common bits at the top level for ease of import: +pub use assign_id::AssignId; +pub use dense_map::DenseMap; +pub use mean_list::MeanList; +pub use most_seen::MostSeen; +pub use num_stats::NumStats; diff --git a/backend/core/src/util/mean_list.rs b/backend/common/src/mean_list.rs similarity index 100% rename from backend/core/src/util/mean_list.rs rename to backend/common/src/mean_list.rs diff --git a/backend/common/src/most_seen.rs b/backend/common/src/most_seen.rs new file mode 100644 index 0000000..393022d --- /dev/null +++ b/backend/common/src/most_seen.rs @@ -0,0 +1,231 @@ +use std::collections::HashMap; +use std::hash::Hash; + +/// Add items to this, and it will keep track of what the item +/// seen the most is. +#[derive(Debug)] +pub struct MostSeen { + current_best: T, + current_count: usize, + others: HashMap, +} + +impl Default for MostSeen { + fn default() -> Self { + Self { + current_best: T::default(), + current_count: 0, + others: HashMap::new(), + } + } +} + +impl MostSeen { + pub fn new(item: T) -> Self { + Self { + current_best: item, + current_count: 1, + others: HashMap::new(), + } + } + pub fn best(&self) -> &T { + &self.current_best + } + pub fn best_count(&self) -> usize { + self.current_count + } +} + +impl MostSeen { + pub fn insert(&mut self, item: &T) -> ChangeResult { + if &self.current_best == item { + // Item already the best one; bump count. + self.current_count += 1; + return ChangeResult::NoChange; + } + + // Item not the best; increment count in map + let item_count = self.others.entry(item.clone()).or_default(); + *item_count += 1; + + // Is item now the best? + if *item_count > self.current_count { + let (mut item, mut count) = self.others.remove_entry(item).expect("item added above"); + + // Swap the current best for the new best: + std::mem::swap(&mut item, &mut self.current_best); + std::mem::swap(&mut count, &mut self.current_count); + + // Insert the old best back into the map: + self.others.insert(item, count); + + ChangeResult::NewMostSeenItem + } else { + ChangeResult::NoChange + } + } + pub fn remove(&mut self, item: &T) -> ChangeResult { + if &self.current_best == item { + // Item already the best one; reduce count (don't allow to drop below 0) + self.current_count = self.current_count.saturating_sub(1); + + // Is there a new best? + let other_best = self.others.iter().max_by_key(|f| f.1); + + let (other_item, &other_count) = match other_best { + Some(item) => item, + None => return ChangeResult::NoChange, + }; + + if other_count > self.current_count { + // Clone item to unborrow self.others so that we can remove + // the item from it. We could pre-emptively remove and reinsert + // instead, but most of the time there is no change, so I'm + // aiming to keep that path cheaper. + let other_item = other_item.clone(); + let (mut other_item, mut other_count) = self + .others + .remove_entry(&other_item) + .expect("item returned above, so def exists"); + + // Swap the current best for the new best: + std::mem::swap(&mut other_item, &mut self.current_best); + std::mem::swap(&mut other_count, &mut self.current_count); + + // Insert the old best back into the map: + self.others.insert(other_item, other_count); + + return ChangeResult::NewMostSeenItem; + } else { + return ChangeResult::NoChange; + } + } + + // Item is in the map; not the best anyway. decrement count. + if let Some(count) = self.others.get_mut(item) { + *count += 1; + } + ChangeResult::NoChange + } +} + +/// Record the result of adding/removing an entry +#[derive(Clone, Copy)] +pub enum ChangeResult { + /// The best item has remained the same. + NoChange, + /// There is a new best item now. + NewMostSeenItem, +} + +impl ChangeResult { + pub fn has_changed(self) -> bool { + match self { + ChangeResult::NewMostSeenItem => true, + ChangeResult::NoChange => false, + } + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn default_renames_instantly() { + let mut a: MostSeen<&str> = MostSeen::default(); + let res = a.insert(&"Hello"); + assert_eq!(*a.best(), "Hello"); + assert!(res.has_changed()); + } + + #[test] + fn new_renames_on_second_change() { + let mut a: MostSeen<&str> = MostSeen::new("First"); + a.insert(&"Second"); + assert_eq!(*a.best(), "First"); + a.insert(&"Second"); + assert_eq!(*a.best(), "Second"); + } + + #[test] + fn removing_doesnt_underflow() { + let mut a: MostSeen<&str> = MostSeen::new("First"); + a.remove(&"First"); + a.remove(&"First"); + a.remove(&"Second"); + a.remove(&"Third"); + } + + #[test] + fn keeps_track_of_best_count() { + let mut a: MostSeen<&str> = MostSeen::default(); + a.insert(&"First"); + assert_eq!(a.best_count(), 1); + + a.insert(&"First"); + assert_eq!(a.best_count(), 2); + + a.insert(&"First"); + assert_eq!(a.best_count(), 3); + + a.remove(&"First"); + assert_eq!(a.best_count(), 2); + + a.remove(&"First"); + assert_eq!(a.best_count(), 1); + + a.remove(&"First"); + assert_eq!(a.best_count(), 0); + + a.remove(&"First"); + assert_eq!(a.best_count(), 0); + } + + #[test] + fn it_tracks_best_on_insert() { + let mut a: MostSeen<&str> = MostSeen::default(); + + a.insert(&"First"); + assert_eq!(*a.best(), "First", "1"); + + a.insert(&"Second"); + assert_eq!(*a.best(), "First", "2"); + + a.insert(&"Second"); + assert_eq!(*a.best(), "Second", "3"); + + a.insert(&"First"); + assert_eq!(*a.best(), "Second", "4"); + + a.insert(&"First"); + assert_eq!(*a.best(), "First", "5"); + } + + #[test] + fn it_tracks_best() { + let mut a: MostSeen<&str> = MostSeen::default(); + a.insert(&"First"); + a.insert(&"Second"); + a.insert(&"Third"); // 1 + + a.insert(&"Second"); + a.insert(&"Second"); // 3 + a.insert(&"First"); // 2 + + assert_eq!(*a.best(), "Second"); + assert_eq!(a.best_count(), 3); + + let res = a.remove(&"Second"); + + assert!(!res.has_changed()); + assert_eq!(a.best_count(), 2); + assert_eq!(*a.best(), "Second"); // Tied with "First" + + let res = a.remove(&"Second"); + + assert!(res.has_changed()); + assert_eq!(a.best_count(), 2); + assert_eq!(*a.best(), "First"); // First is now ahead + } +} diff --git a/backend/common/src/node_message.rs b/backend/common/src/node_message.rs new file mode 100644 index 0000000..b8a87fe --- /dev/null +++ b/backend/common/src/node_message.rs @@ -0,0 +1,246 @@ +//! This is the internal represenation of telemetry messages sent from nodes. +//! There is a separate JSON representation of these types, because internally we want to be +//! able to serialize these messages to bincode, and various serde attribtues aren't compatible +//! with this, hence this separate internal representation. + +use crate::node_types::{Block, BlockHash, BlockNumber, NodeDetails}; +use serde::{Deserialize, Serialize}; + +pub type NodeMessageId = u64; + +#[derive(Serialize, Deserialize, Debug)] +pub enum NodeMessage { + V1 { payload: Payload }, + V2 { id: NodeMessageId, payload: Payload }, +} + +impl NodeMessage { + /// Returns the ID associated with the node message, or 0 + /// if the message has no ID. + pub fn id(&self) -> NodeMessageId { + match self { + NodeMessage::V1 { .. } => 0, + NodeMessage::V2 { id, .. } => *id, + } + } + /// Return the payload associated with the message. + pub fn into_payload(self) -> Payload { + match self { + NodeMessage::V1 { payload, .. } | NodeMessage::V2 { payload, .. } => payload, + } + } +} + +impl From for Payload { + fn from(msg: NodeMessage) -> Payload { + msg.into_payload() + } +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub enum Payload { + SystemConnected(SystemConnected), + SystemInterval(SystemInterval), + BlockImport(Block), + NotifyFinalized(Finalized), + TxPoolImport, + AfgFinalized(AfgFinalized), + AfgReceivedPrecommit(AfgReceived), + AfgReceivedPrevote(AfgReceived), + AfgReceivedCommit(AfgReceived), + AfgAuthoritySet(AfgAuthoritySet), + AfgFinalizedBlocksUpTo, + AuraPreSealedBlock, + PreparedBlockForProposing, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct SystemConnected { + pub genesis_hash: BlockHash, + pub node: NodeDetails, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct SystemInterval { + pub peers: Option, + pub txcount: Option, + pub bandwidth_upload: Option, + pub bandwidth_download: Option, + pub finalized_height: Option, + pub finalized_hash: Option, + pub block: Option, + pub used_state_cache_size: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct Finalized { + pub hash: BlockHash, + pub height: Box, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct AfgFinalized { + pub finalized_hash: BlockHash, + pub finalized_number: Box, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct AfgReceived { + pub target_hash: BlockHash, + pub target_number: Box, + pub voter: Option>, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct AfgAuthoritySet { + pub authority_id: Box, + pub authorities: Box, + pub authority_set_id: Box, +} + +impl Payload { + pub fn best_block(&self) -> Option<&Block> { + match self { + Payload::BlockImport(block) => Some(block), + Payload::SystemInterval(SystemInterval { block, .. }) => block.as_ref(), + _ => None, + } + } + + pub fn finalized_block(&self) -> Option { + match self { + Payload::SystemInterval(ref interval) => Some(Block { + hash: interval.finalized_hash?, + height: interval.finalized_height?, + }), + Payload::NotifyFinalized(ref finalized) => Some(Block { + hash: finalized.hash, + height: finalized.height.parse().ok()?, + }), + _ => None, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use bincode::Options; + + // Without adding a derive macro and marker trait (and enforcing their use), we don't really + // know whether things can (de)serialize to bincode or not at runtime without failing unless + // we test the different types we want to (de)serialize ourselves. We just need to test each + // type, not each variant. + fn bincode_can_serialize_and_deserialize<'de, T>(item: T) + where + T: Serialize + serde::de::DeserializeOwned, + { + let bytes = bincode::serialize(&item).expect("Serialization should work"); + let _: T = bincode::deserialize(&bytes).expect("Deserialization should work"); + } + + #[test] + fn bincode_can_serialize_and_deserialize_node_message_system_connected() { + bincode_can_serialize_and_deserialize(NodeMessage::V1 { + payload: Payload::SystemConnected(SystemConnected { + genesis_hash: BlockHash::zero(), + node: NodeDetails { + chain: "foo".into(), + name: "foo".into(), + implementation: "foo".into(), + version: "foo".into(), + validator: None, + network_id: None, + startup_time: None, + }, + }), + }); + } + + #[test] + fn bincode_can_serialize_and_deserialize_node_message_system_interval() { + bincode_can_serialize_and_deserialize(NodeMessage::V1 { + payload: Payload::SystemInterval(SystemInterval { + peers: None, + txcount: None, + bandwidth_upload: None, + bandwidth_download: None, + finalized_height: None, + finalized_hash: None, + block: None, + used_state_cache_size: None, + }), + }); + } + + #[test] + fn bincode_can_serialize_and_deserialize_node_message_block_import() { + bincode_can_serialize_and_deserialize(NodeMessage::V1 { + payload: Payload::BlockImport(Block { + hash: BlockHash([0; 32]), + height: 0, + }), + }); + } + + #[test] + fn bincode_can_serialize_and_deserialize_node_message_notify_finalized() { + bincode_can_serialize_and_deserialize(NodeMessage::V1 { + payload: Payload::NotifyFinalized(Finalized { + hash: BlockHash::zero(), + height: "foo".into(), + }), + }); + } + + #[test] + fn bincode_can_serialize_and_deserialize_node_message_tx_pool_import() { + bincode_can_serialize_and_deserialize(NodeMessage::V1 { + payload: Payload::TxPoolImport, + }); + } + + #[test] + fn bincode_can_serialize_and_deserialize_node_message_afg_finalized() { + bincode_can_serialize_and_deserialize(NodeMessage::V1 { + payload: Payload::AfgFinalized(AfgFinalized { + finalized_hash: BlockHash::zero(), + finalized_number: "foo".into(), + }), + }); + } + + #[test] + fn bincode_can_serialize_and_deserialize_node_message_afg_received() { + bincode_can_serialize_and_deserialize(NodeMessage::V1 { + payload: Payload::AfgReceivedPrecommit(AfgReceived { + target_hash: BlockHash::zero(), + target_number: "foo".into(), + voter: None, + }), + }); + } + + #[test] + fn bincode_can_serialize_and_deserialize_node_message_afg_authority_set() { + bincode_can_serialize_and_deserialize(NodeMessage::V1 { + payload: Payload::AfgAuthoritySet(AfgAuthoritySet { + authority_id: "foo".into(), + authorities: "foo".into(), + authority_set_id: "foo".into(), + }), + }); + } + + #[test] + fn bincode_block_zero() { + let raw = Block::zero(); + + let bytes = bincode::options().serialize(&raw).unwrap(); + + let deserialized: Block = bincode::options().deserialize(&bytes).unwrap(); + + assert_eq!(raw.hash, deserialized.hash); + assert_eq!(raw.height, deserialized.height); + } +} diff --git a/backend/core/src/types.rs b/backend/common/src/node_types.rs similarity index 61% rename from backend/core/src/types.rs rename to backend/common/src/node_types.rs index a93a709..3b7c947 100644 --- a/backend/core/src/types.rs +++ b/backend/common/src/node_types.rs @@ -1,16 +1,17 @@ -use serde::ser::{Serialize, SerializeTuple, Serializer}; -use serde::Deserialize; +//! These types are partly used in [`crate::node_message`], but also stored and used +//! more generally through the application. -use crate::util::{now, MeanList}; +use serde::ser::{SerializeTuple, Serializer}; +use serde::{Deserialize, Serialize}; + +use crate::{time, MeanList}; -pub type NodeId = usize; -pub type ConnId = u64; pub type BlockNumber = u64; pub type Timestamp = u64; -pub type Address = Box; pub use primitive_types::H256 as BlockHash; -#[derive(Deserialize, Debug, Clone)] +/// +#[derive(Serialize, Deserialize, Debug, Clone)] pub struct NodeDetails { pub chain: Box, pub name: Box, @@ -21,75 +22,13 @@ pub struct NodeDetails { pub startup_time: Option>, } -#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq, Default)] +/// +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub struct NodeStats { pub peers: u64, pub txcount: u64, } -#[derive(Default)] -pub struct NodeIO { - pub used_state_cache_size: MeanList, -} - -#[derive(Deserialize, Debug, Clone, Copy)] -pub struct Block { - #[serde(rename = "best")] - pub hash: BlockHash, - pub height: BlockNumber, -} - -#[derive(Debug, Clone, Copy)] -pub struct BlockDetails { - pub block: Block, - pub block_time: u64, - pub block_timestamp: u64, - pub propagation_time: Option, -} - -impl Default for BlockDetails { - fn default() -> Self { - BlockDetails { - block: Block::zero(), - block_timestamp: now(), - block_time: 0, - propagation_time: None, - } - } -} - -#[derive(Default)] -pub struct NodeHardware { - /// Upload uses means - pub upload: MeanList, - /// Download uses means - pub download: MeanList, - /// Stampchange uses means - pub chart_stamps: MeanList, -} - -#[derive(Deserialize, Debug, Clone)] -pub struct NodeLocation { - pub latitude: f32, - pub longitude: f32, - pub city: Box, -} - -impl Serialize for NodeDetails { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - let mut tup = serializer.serialize_tuple(6)?; - tup.serialize_element(&self.name)?; - tup.serialize_element(&self.implementation)?; - tup.serialize_element(&self.version)?; - tup.serialize_element(&self.validator)?; - tup.serialize_element(&self.network_id)?; - tup.end() - } -} - impl Serialize for NodeStats { fn serialize(&self, serializer: S) -> Result where @@ -102,17 +41,130 @@ impl Serialize for NodeStats { } } +impl<'de> Deserialize<'de> for NodeStats { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let (peers, txcount) = <(u64, u64)>::deserialize(deserializer)?; + Ok(NodeStats { peers, txcount }) + } +} + +/// +#[derive(Default)] +pub struct NodeIO { + pub used_state_cache_size: MeanList, +} + impl Serialize for NodeIO { fn serialize(&self, serializer: S) -> Result where S: Serializer, { let mut tup = serializer.serialize_tuple(1)?; + // This is "one-way": we can't deserialize again from this to a MeanList: tup.serialize_element(self.used_state_cache_size.slice())?; tup.end() } } +/// +#[derive(Deserialize, Serialize, Debug, Clone, Copy, PartialEq)] +pub struct Block { + pub hash: BlockHash, + pub height: BlockNumber, +} + +impl Block { + pub fn zero() -> Self { + Block { + hash: BlockHash::from([0; 32]), + height: 0, + } + } +} + +/// +#[derive(Default)] +pub struct NodeHardware { + /// Upload uses means + pub upload: MeanList, + /// Download uses means + pub download: MeanList, + /// Stampchange uses means + pub chart_stamps: MeanList, +} + +impl Serialize for NodeHardware { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut tup = serializer.serialize_tuple(3)?; + // These are "one-way": we can't deserialize again from them to MeanLists: + tup.serialize_element(self.upload.slice())?; + tup.serialize_element(self.download.slice())?; + tup.serialize_element(self.chart_stamps.slice())?; + tup.end() + } +} + +/// +#[derive(Debug, Clone, PartialEq)] +pub struct NodeLocation { + pub latitude: f32, + pub longitude: f32, + pub city: Box, +} + +impl Serialize for NodeLocation { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut tup = serializer.serialize_tuple(3)?; + tup.serialize_element(&self.latitude)?; + tup.serialize_element(&self.longitude)?; + tup.serialize_element(&&*self.city)?; + tup.end() + } +} + +impl<'de> Deserialize<'de> for NodeLocation { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let (latitude, longitude, city) = <(f32, f32, Box)>::deserialize(deserializer)?; + Ok(NodeLocation { + latitude, + longitude, + city, + }) + } +} + +/// +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct BlockDetails { + pub block: Block, + pub block_time: u64, + pub block_timestamp: u64, + pub propagation_time: Option, +} + +impl Default for BlockDetails { + fn default() -> Self { + BlockDetails { + block: Block::zero(), + block_timestamp: time::now(), + block_time: 0, + propagation_time: None, + } + } +} + impl Serialize for BlockDetails { fn serialize(&self, serializer: S) -> Result where @@ -128,28 +180,20 @@ impl Serialize for BlockDetails { } } -impl Serialize for NodeLocation { - fn serialize(&self, serializer: S) -> Result +impl<'de> Deserialize<'de> for BlockDetails { + fn deserialize(deserializer: D) -> Result where - S: Serializer, + D: serde::Deserializer<'de>, { - let mut tup = serializer.serialize_tuple(3)?; - tup.serialize_element(&self.latitude)?; - tup.serialize_element(&self.longitude)?; - tup.serialize_element(&&*self.city)?; - tup.end() - } -} - -impl Serialize for NodeHardware { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - let mut tup = serializer.serialize_tuple(3)?; - tup.serialize_element(self.upload.slice())?; - tup.serialize_element(self.download.slice())?; - tup.serialize_element(self.chart_stamps.slice())?; - tup.end() + let tup = <(u64, BlockHash, u64, u64, Option)>::deserialize(deserializer)?; + Ok(BlockDetails { + block: Block { + height: tup.0, + hash: tup.1, + }, + block_time: tup.2, + block_timestamp: tup.3, + propagation_time: tup.4, + }) } } diff --git a/backend/core/src/util/num_stats.rs b/backend/common/src/num_stats.rs similarity index 100% rename from backend/core/src/util/num_stats.rs rename to backend/common/src/num_stats.rs diff --git a/backend/common/src/ready_chunks_all.rs b/backend/common/src/ready_chunks_all.rs new file mode 100644 index 0000000..f4cc367 --- /dev/null +++ b/backend/common/src/ready_chunks_all.rs @@ -0,0 +1,105 @@ +//! [`futures::StreamExt::ready_chunks()`] internally stores a vec with a certain capacity, and will buffer up +//! up to that many items that are ready from the underlying stream before returning either when we run out of +//! Poll::Ready items, or we hit the capacity. +//! +//! This variation has no fixed capacity, and will buffer everything it can up at each point to return. This is +//! better when the amount of items varies a bunch (and we don't want to allocate a fixed capacity every time), +//! and can help ensure that we process as many items as possible each time (rather than only up to capacity items). +//! +//! Code is adapted from the futures implementation +//! (see [ready_chunks.rs](https://docs.rs/futures-util/0.3.15/src/futures_util/stream/stream/ready_chunks.rs.html)). + +use futures::stream::Fuse; +use futures::StreamExt; +use core::mem; +use core::pin::Pin; +use futures::stream::{FusedStream, Stream}; +use futures::task::{Context, Poll}; +use pin_project_lite::pin_project; + +pin_project! { + /// Buffer up all Ready items in the underlying stream each time + /// we attempt to retrieve items from it, and return a Vec of those + /// items. + #[derive(Debug)] + #[must_use = "streams do nothing unless polled"] + pub struct ReadyChunksAll { + #[pin] + stream: Fuse, + items: Vec, + } +} + +impl ReadyChunksAll +where + St: Stream, +{ + pub fn new(stream: St) -> Self { + Self { + stream: stream.fuse(), + items: Vec::new() + } + } +} + +impl Stream for ReadyChunksAll { + type Item = Vec; + + fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + let mut this = self.project(); + + loop { + match this.stream.as_mut().poll_next(cx) { + // Flush all collected data if underlying stream doesn't contain + // more ready values + Poll::Pending => { + return if this.items.is_empty() { + Poll::Pending + } else { + Poll::Ready(Some(mem::replace(this.items, Vec::new()))) + } + } + + // Push the ready item into the buffer + Poll::Ready(Some(item)) => { + this.items.push(item); + } + + // Since the underlying stream ran out of values, return what we + // have buffered, if we have anything. + Poll::Ready(None) => { + let last = if this.items.is_empty() { + None + } else { + let full_buf = mem::replace(this.items, Vec::new()); + Some(full_buf) + }; + + return Poll::Ready(last); + } + } + } + } + + fn size_hint(&self) -> (usize, Option) { + // Look at the underlying stream's size_hint. If we've + // buffered some items, we'll return at least that Vec, + // giving us a lower bound 1 greater than the underlying. + // The upper bound is, worst case, our vec + each individual + // item in the underlying stream. + let chunk_len = if self.items.is_empty() { 0 } else { 1 }; + let (lower, upper) = self.stream.size_hint(); + let lower = lower.saturating_add(chunk_len); + let upper = match upper { + Some(x) => x.checked_add(chunk_len), + None => None, + }; + (lower, upper) + } +} + +impl FusedStream for ReadyChunksAll { + fn is_terminated(&self) -> bool { + self.stream.is_terminated() && self.items.is_empty() + } +} diff --git a/backend/common/src/time.rs b/backend/common/src/time.rs new file mode 100644 index 0000000..29386bd --- /dev/null +++ b/backend/common/src/time.rs @@ -0,0 +1,9 @@ +/// Returns current unix time in ms (compatible with JS Date.now()) +pub fn now() -> u64 { + use std::time::SystemTime; + + SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .expect("System time must be configured to be post Unix Epoch start; qed") + .as_millis() as u64 +} diff --git a/backend/common/src/ws_client.rs b/backend/common/src/ws_client.rs new file mode 100644 index 0000000..7de9363 --- /dev/null +++ b/backend/common/src/ws_client.rs @@ -0,0 +1,274 @@ +use futures::channel::mpsc; +use futures::{Sink, SinkExt, Stream, StreamExt}; +use soketto::handshake::{Client, ServerResponse}; +use tokio::net::TcpStream; +use tokio_util::compat::TokioAsyncReadCompatExt; + +/// Send messages into the connection +#[derive(Clone)] +pub struct Sender { + inner: mpsc::UnboundedSender, +} + +impl Sender { + /// Ask the underlying Websocket connection to close. + pub async fn close(&mut self) -> Result<(), SendError> { + self.inner.send(SentMessageInternal::Close).await?; + Ok(()) + } + /// Returns whether this channel is closed. + pub fn is_closed(&mut self) -> bool { + self.inner.is_closed() + } + /// Unbounded send will always queue the message and doesn't + /// need to be awaited. + pub fn unbounded_send(&self, msg: SentMessage) -> Result<(), SendError> { + self.inner + .unbounded_send(SentMessageInternal::Message(msg)) + .map_err(|e| e.into_send_error())?; + Ok(()) + } +} + +#[derive(thiserror::Error, Debug, Clone)] +pub enum SendError { + #[error("Failed to send message: {0}")] + ChannelError(#[from] mpsc::SendError) +} + +impl Sink for Sender { + type Error = SendError; + fn poll_ready( + mut self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + self.inner.poll_ready_unpin(cx).map_err(|e| e.into()) + } + fn start_send(mut self: std::pin::Pin<&mut Self>, item: SentMessage) -> Result<(), Self::Error> { + self.inner + .start_send_unpin(SentMessageInternal::Message(item)) + .map_err(|e| e.into()) + } + fn poll_flush( + mut self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + self.inner.poll_flush_unpin(cx).map_err(|e| e.into()) + } + fn poll_close( + mut self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + self.inner.poll_close_unpin(cx).map_err(|e| e.into()) + } +} + +/// Receive messages out of a connection +pub struct Receiver { + inner: mpsc::UnboundedReceiver>, +} + +#[derive(thiserror::Error, Debug)] +pub enum RecvError { + #[error("Text message contains invalid UTF8: {0}")] + InvalidUtf8(#[from] std::string::FromUtf8Error), + #[error("Stream finished")] + StreamFinished, +} + +impl Stream for Receiver { + type Item = Result; + fn poll_next( + mut self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + self.inner.poll_next_unpin(cx).map_err(|e| e.into()) + } +} + +/// A message that can be received from the connection +#[derive(Debug, Clone)] +pub enum RecvMessage { + /// Send an owned string into the socket. + Text(String), + /// Send owned bytes into the socket. + Binary(Vec), +} + +impl RecvMessage { + pub fn len(&self) -> usize { + match self { + RecvMessage::Binary(b) => b.len(), + RecvMessage::Text(s) => s.len(), + } + } +} + +/// A message that can be sent into the connection +#[derive(Debug, Clone)] +pub enum SentMessage { + /// Being able to send static text is primarily useful for benchmarking, + /// so that we can avoid cloning an owned string and pass a static reference + /// (one such option here is using [`Box::leak`] to generate strings with + /// static lifetimes). + StaticText(&'static str), + /// Being able to send static bytes is primarily useful for benchmarking, + /// so that we can avoid cloning an owned string and pass a static reference + /// (one such option here is using [`Box::leak`] to generate bytes with + /// static lifetimes). + StaticBinary(&'static [u8]), + /// Send an owned string into the socket. + Text(String), + /// Send owned bytes into the socket. + Binary(Vec), +} + +/// Sent messages can be anything publically visible, or a close message. +#[derive(Debug, Clone)] +enum SentMessageInternal { + Message(SentMessage), + Close, +} + +#[derive(thiserror::Error, Debug)] +pub enum ConnectError { + #[error("IO error: {0}")] + Io(#[from] std::io::Error), + #[error("Handshake error: {0}")] + Handshake(#[from] soketto::handshake::Error), + #[error("Redirect not supported (status code: {status_code})")] + ConnectionFailedRedirect { status_code: u16 }, + #[error("Connection rejected (status code: {status_code})")] + ConnectionFailedRejected { status_code: u16 }, +} + +/// Establish a websocket connection that you can send and receive messages from. +/// A thin wrapper around Soketto that provides cancel-safe send/receive handles. +/// +/// This must be called within the context of a tokio runtime. +pub async fn connect(uri: &http::Uri) -> Result<(Sender, Receiver), ConnectError> { + let host = uri.host().unwrap_or("127.0.0.1"); + let port = uri.port_u16().unwrap_or(80); + let path = uri.path(); + + let socket = TcpStream::connect((host, port)).await?; + socket.set_nodelay(true).expect("socket set_nodelay failed"); + + // Establish a WS connection: + let mut client = Client::new(socket.compat(), host, &path); + let (mut ws_to_connection, mut ws_from_connection) = match client.handshake().await? { + ServerResponse::Accepted { .. } => client.into_builder().finish(), + ServerResponse::Redirect { status_code, .. } => { + return Err(ConnectError::ConnectionFailedRedirect { status_code }) + } + ServerResponse::Rejected { status_code } => { + return Err(ConnectError::ConnectionFailedRejected { status_code }) + } + }; + + // Soketto sending/receiving isn't cancel safe, so we wrap the message stuff into spawned + // tasks and use channels (which are cancel safe) to send/recv messages atomically.. + + // Receive messages from the socket and post them out: + let (mut tx_to_external, rx_from_ws) = mpsc::unbounded(); + tokio::spawn(async move { + let mut data = Vec::with_capacity(128); + loop { + // Clear the buffer and wait for the next message to arrive: + data.clear(); + + let message_data = match ws_from_connection.receive_data(&mut data).await { + Err(e) => { + // Couldn't receive data may mean all senders are gone, so log + // the error and shut this down: + log::error!( + "Shutting down websocket connection: Failed to receive data: {}", + e + ); + break; + } + Ok(data) => data, + }; + + let msg = match message_data { + soketto::Data::Text(_) => Ok(RecvMessage::Binary(data)), + soketto::Data::Binary(_) => String::from_utf8(data) + .map(|s| RecvMessage::Text(s)) + .map_err(|e| e.into()), + }; + + data = Vec::with_capacity(128); + + if let Err(e) = tx_to_external.send(msg).await { + // Failure to send likely means that the recv has been dropped, + // so let's drop this loop too. + log::error!( + "Shutting down websocket connection: Failed to send data out: {}", + e + ); + break; + } + } + }); + + // Receive messages externally to send to the socket. + let (tx_to_ws, mut rx_from_external) = mpsc::unbounded(); + tokio::spawn(async move { + while let Some(msg) = rx_from_external.next().await { + match msg { + SentMessageInternal::Message(SentMessage::Text(s)) => { + if let Err(e) = ws_to_connection.send_text_owned(s).await { + log::error!( + "Shutting down websocket connection: Failed to send text data: {}", + e + ); + break; + } + } + SentMessageInternal::Message(SentMessage::Binary(bytes)) => { + if let Err(e) = ws_to_connection.send_binary_mut(bytes).await { + log::error!( + "Shutting down websocket connection: Failed to send binary data: {}", + e + ); + break; + } + }, + SentMessageInternal::Message(SentMessage::StaticText(s)) => { + if let Err(e) = ws_to_connection.send_text(s).await { + log::error!( + "Shutting down websocket connection: Failed to send text data: {}", + e + ); + break; + } + } + SentMessageInternal::Message(SentMessage::StaticBinary(bytes)) => { + if let Err(e) = ws_to_connection.send_binary(bytes).await { + log::error!( + "Shutting down websocket connection: Failed to send binary data: {}", + e + ); + break; + } + }, + SentMessageInternal::Close => { + if let Err(e) = ws_to_connection.close().await { + log::error!("Error attempting to close connection: {}", e); + break; + } + } + } + + if let Err(e) = ws_to_connection.flush().await { + log::error!( + "Shutting down websocket connection: Failed to flush data: {}", + e + ); + break; + } + } + }); + + Ok((Sender { inner: tx_to_ws }, Receiver { inner: rx_from_ws })) +} diff --git a/backend/core/src/aggregator.rs b/backend/core/src/aggregator.rs deleted file mode 100644 index 0694c8a..0000000 --- a/backend/core/src/aggregator.rs +++ /dev/null @@ -1,383 +0,0 @@ -use actix::prelude::*; -use actix_web_actors::ws::{CloseCode, CloseReason}; -use ctor::ctor; -use std::collections::{HashMap, HashSet}; - -use crate::chain::{self, Chain, ChainId, Label}; -use crate::feed::connector::{Connected, FeedConnector, FeedId}; -use crate::feed::{self, FeedMessageSerializer}; -use crate::node::connector::{Mute, NodeConnector}; -use crate::types::{ConnId, NodeDetails}; -use crate::util::{DenseMap, Hash}; - -pub struct Aggregator { - genesis_hashes: HashMap, - labels: HashMap, - chains: DenseMap, - feeds: DenseMap>, - serializer: FeedMessageSerializer, - /// Denylist for networks we do not want to allow connecting. - denylist: HashSet, -} - -pub struct ChainEntry { - /// Address to the `Chain` agent - addr: Addr, - /// Genesis [`Hash`] of the chain - genesis_hash: Hash, - /// String name of the chain - label: Label, - /// Node count - nodes: usize, - /// Maximum allowed nodes - max_nodes: usize, -} - -#[ctor] -/// Labels of chains we consider "first party". These chains allow any -/// number of nodes to connect. -static FIRST_PARTY_NETWORKS: HashSet<&'static str> = { - let mut set = HashSet::new(); - set.insert("Polkadot"); - set.insert("Kusama"); - set.insert("Westend"); - set.insert("Rococo"); - set -}; - -/// Max number of nodes allowed to connect to the telemetry server. -const THIRD_PARTY_NETWORKS_MAX_NODES: usize = 500; - -impl Aggregator { - pub fn new(denylist: HashSet) -> Self { - Aggregator { - genesis_hashes: HashMap::new(), - labels: HashMap::new(), - chains: DenseMap::new(), - feeds: DenseMap::new(), - serializer: FeedMessageSerializer::new(), - denylist, - } - } - - /// Get an address to the chain actor by name. If the address is not found, - /// or the address is disconnected (actor dropped), create a new one. - pub fn lazy_chain( - &mut self, - genesis_hash: Hash, - label: &str, - ctx: &mut ::Context, - ) -> ChainId { - let cid = match self.genesis_hashes.get(&genesis_hash).copied() { - Some(cid) => cid, - None => { - self.serializer.push(feed::AddedChain(&label, 1)); - - let addr = ctx.address(); - let max_nodes = max_nodes(label); - let label: Label = label.into(); - let cid = self.chains.add_with(|cid| ChainEntry { - addr: Chain::new(cid, addr, label.clone()).start(), - genesis_hash, - label: label.clone(), - nodes: 1, - max_nodes, - }); - - self.labels.insert(label, cid); - self.genesis_hashes.insert(genesis_hash, cid); - - self.broadcast(); - - cid - } - }; - - cid - } - - fn get_chain(&mut self, label: &str) -> Option<&mut ChainEntry> { - let chains = &mut self.chains; - self.labels - .get(label) - .and_then(move |&cid| chains.get_mut(cid)) - } - - fn broadcast(&mut self) { - if let Some(msg) = self.serializer.finalize() { - for (_, feed) in self.feeds.iter() { - feed.do_send(msg.clone()); - } - } - } -} - -impl Actor for Aggregator { - type Context = Context; -} - -/// Message sent from the NodeConnector to the Aggregator upon getting all node details -#[derive(Message)] -#[rtype(result = "()")] -pub struct AddNode { - /// Details of the node being added to the aggregator - pub node: NodeDetails, - /// Genesis [`Hash`] of the chain the node is being added to. - pub genesis_hash: Hash, - /// Connection id used by the node connector for multiplexing parachains - pub conn_id: ConnId, - /// Address of the NodeConnector actor - pub node_connector: Addr, -} - -/// Message sent from the Chain to the Aggregator when the Chain loses all nodes -#[derive(Message)] -#[rtype(result = "()")] -pub struct DropChain(pub ChainId); - -#[derive(Message)] -#[rtype(result = "()")] -pub struct RenameChain(pub ChainId, pub Label); - -/// Message sent from the FeedConnector to the Aggregator when subscribing to a new chain -#[derive(Message)] -#[rtype(result = "bool")] -pub struct Subscribe { - pub chain: Label, - pub feed: Addr, -} - -/// Message sent from the FeedConnector to the Aggregator consensus requested -#[derive(Message)] -#[rtype(result = "()")] -pub struct SendFinality { - pub chain: Label, - pub fid: FeedId, -} - -/// Message sent from the FeedConnector to the Aggregator no more consensus required -#[derive(Message)] -#[rtype(result = "()")] -pub struct NoMoreFinality { - pub chain: Label, - pub fid: FeedId, -} - -/// Message sent from the FeedConnector to the Aggregator when first connected -#[derive(Message)] -#[rtype(result = "()")] -pub struct Connect(pub Addr); - -/// Message sent from the FeedConnector to the Aggregator when disconnecting -#[derive(Message)] -#[rtype(result = "()")] -pub struct Disconnect(pub FeedId); - -/// Message sent from the Chain to the Aggergator when the node count on the chain changes -#[derive(Message)] -#[rtype(result = "()")] -pub struct NodeCount(pub ChainId, pub usize); - -/// Message sent to the Aggregator to get a health check -#[derive(Message)] -#[rtype(result = "usize")] -pub struct GetHealth; - -impl Handler for Aggregator { - type Result = (); - - fn handle(&mut self, msg: AddNode, ctx: &mut Self::Context) { - if self.denylist.contains(&*msg.node.chain) { - log::warn!(target: "Aggregator::AddNode", "'{}' is on the denylist.", msg.node.chain); - let AddNode { node_connector, .. } = msg; - let reason = CloseReason { - code: CloseCode::Abnormal, - description: Some("Denied".into()), - }; - node_connector.do_send(Mute { reason }); - return; - } - let AddNode { - node, - genesis_hash, - conn_id, - node_connector, - } = msg; - log::trace!(target: "Aggregator::AddNode", "New node connected. Chain '{}'", node.chain); - - let cid = self.lazy_chain(genesis_hash, &node.chain, ctx); - let chain = self - .chains - .get_mut(cid) - .expect("Entry just created above; qed"); - if chain.nodes < chain.max_nodes { - chain.addr.do_send(chain::AddNode { - node, - conn_id, - node_connector, - }); - } else { - log::warn!(target: "Aggregator::AddNode", "Chain {} is over quota ({})", chain.label, chain.max_nodes); - let reason = CloseReason { - code: CloseCode::Again, - description: Some("Overquota".into()), - }; - node_connector.do_send(Mute { reason }); - } - } -} - -impl Handler for Aggregator { - type Result = (); - - fn handle(&mut self, msg: DropChain, _: &mut Self::Context) { - let DropChain(cid) = msg; - - if let Some(entry) = self.chains.remove(cid) { - let label = &entry.label; - self.genesis_hashes.remove(&entry.genesis_hash); - self.labels.remove(label); - self.serializer.push(feed::RemovedChain(label)); - log::info!("Dropped chain [{}] from the aggregator", label); - self.broadcast(); - } - } -} - -impl Handler for Aggregator { - type Result = (); - - fn handle(&mut self, msg: RenameChain, _: &mut Self::Context) { - let RenameChain(cid, new) = msg; - - if let Some(entry) = self.chains.get_mut(cid) { - if entry.label == new { - return; - } - - // Update UI - self.serializer.push(feed::RemovedChain(&entry.label)); - self.serializer.push(feed::AddedChain(&new, entry.nodes)); - - // Update labels -> cid map - self.labels.remove(&entry.label); - self.labels.insert(new.clone(), cid); - - // Update entry - entry.label = new; - - self.broadcast(); - } - } -} - -impl Handler for Aggregator { - type Result = bool; - - fn handle(&mut self, msg: Subscribe, _: &mut Self::Context) -> bool { - let Subscribe { chain, feed } = msg; - - if let Some(chain) = self.get_chain(&chain) { - chain.addr.do_send(chain::Subscribe(feed)); - true - } else { - false - } - } -} - -impl Handler for Aggregator { - type Result = (); - - fn handle(&mut self, msg: SendFinality, _: &mut Self::Context) { - let SendFinality { chain, fid } = msg; - if let Some(chain) = self.get_chain(&chain) { - chain.addr.do_send(chain::SendFinality(fid)); - } - } -} - -impl Handler for Aggregator { - type Result = (); - - fn handle(&mut self, msg: NoMoreFinality, _: &mut Self::Context) { - let NoMoreFinality { chain, fid } = msg; - if let Some(chain) = self.get_chain(&chain) { - chain.addr.do_send(chain::NoMoreFinality(fid)); - } - } -} - -impl Handler for Aggregator { - type Result = (); - - fn handle(&mut self, msg: Connect, _: &mut Self::Context) { - let Connect(connector) = msg; - - let fid = self.feeds.add(connector.clone()); - - log::info!("Feed #{} connected", fid); - - connector.do_send(Connected(fid)); - - self.serializer.push(feed::Version(31)); - - // TODO: keep track on number of nodes connected to each chain - for (_, entry) in self.chains.iter() { - self.serializer - .push(feed::AddedChain(&entry.label, entry.nodes)); - } - - if let Some(msg) = self.serializer.finalize() { - connector.do_send(msg); - } - } -} - -impl Handler for Aggregator { - type Result = (); - - fn handle(&mut self, msg: Disconnect, _: &mut Self::Context) { - let Disconnect(fid) = msg; - - log::info!("Feed #{} disconnected", fid); - - self.feeds.remove(fid); - } -} - -impl Handler for Aggregator { - type Result = (); - - fn handle(&mut self, msg: NodeCount, _: &mut Self::Context) { - let NodeCount(cid, count) = msg; - - if let Some(entry) = self.chains.get_mut(cid) { - entry.nodes = count; - - if count != 0 { - self.serializer.push(feed::AddedChain(&entry.label, count)); - self.broadcast(); - } - } - } -} - -impl Handler for Aggregator { - type Result = usize; - - fn handle(&mut self, _: GetHealth, _: &mut Self::Context) -> Self::Result { - self.chains.len() - } -} - -/// First party networks (Polkadot, Kusama etc) are allowed any number of nodes. -/// Third party networks are allowed `THIRD_PARTY_NETWORKS_MAX_NODES` nodes and -/// no more. -fn max_nodes(label: &str) -> usize { - if FIRST_PARTY_NETWORKS.contains(label) { - usize::MAX - } else { - THIRD_PARTY_NETWORKS_MAX_NODES - } -} diff --git a/backend/core/src/chain.rs b/backend/core/src/chain.rs deleted file mode 100644 index aa5488f..0000000 --- a/backend/core/src/chain.rs +++ /dev/null @@ -1,564 +0,0 @@ -use actix::prelude::*; -use rustc_hash::FxHashMap; -use std::collections::HashMap; -use std::sync::Arc; - -use crate::aggregator::{Aggregator, DropChain, NodeCount, RenameChain}; -use crate::feed::connector::{FeedConnector, FeedId, Subscribed, Unsubscribed}; -use crate::feed::{self, FeedMessageSerializer}; -use crate::node::{ - connector::{Initialize, NodeConnector}, - message::Payload, - Node, -}; -use crate::types::{Block, BlockNumber, ConnId, NodeDetails, NodeId, NodeLocation, Timestamp}; -use crate::util::{now, DenseMap, NumStats}; - -const STALE_TIMEOUT: u64 = 2 * 60 * 1000; // 2 minutes - -pub type ChainId = usize; -pub type Label = Arc; - -pub struct Chain { - cid: ChainId, - /// Who to inform if the Chain drops itself - aggregator: Addr, - /// Label of this chain, along with count of nodes that use this label - label: (Label, usize), - /// Dense mapping of NodeId -> Node - nodes: DenseMap, - /// Dense mapping of FeedId -> Addr, - feeds: DenseMap>, - /// Mapping of FeedId -> Addr for feeds requiring finality info, - finality_feeds: FxHashMap>, - /// Best block - best: Block, - /// Finalized block - finalized: Block, - /// Block times history, stored so we can calculate averages - block_times: NumStats, - /// Calculated average block time - average_block_time: Option, - /// Message serializer - serializer: FeedMessageSerializer, - /// When the best block first arrived - timestamp: Option, - /// Some nodes might manifest a different label, note them here - labels: HashMap, -} - -impl Chain { - pub fn new(cid: ChainId, aggregator: Addr, label: Label) -> Self { - log::info!("[{}] Created", label); - - Chain { - cid, - aggregator, - label: (label, 0), - nodes: DenseMap::new(), - feeds: DenseMap::new(), - finality_feeds: FxHashMap::default(), - best: Block::zero(), - finalized: Block::zero(), - block_times: NumStats::new(50), - average_block_time: None, - serializer: FeedMessageSerializer::new(), - timestamp: None, - labels: HashMap::default(), - } - } - - fn increment_label_count(&mut self, label: &str) { - let count = match self.labels.get_mut(label) { - Some(count) => { - *count += 1; - *count - } - None => { - self.labels.insert(label.into(), 1); - 1 - } - }; - - if &*self.label.0 == label { - self.label.1 += 1; - } else if count > self.label.1 { - self.rename(label.into(), count); - } - } - - fn decrement_label_count(&mut self, label: &str) { - match self.labels.get_mut(label) { - Some(count) => *count -= 1, - None => return, - }; - - if &*self.label.0 == label { - self.label.1 -= 1; - - for (label, &count) in self.labels.iter() { - if count > self.label.1 { - let label: Arc<_> = label.clone(); - self.rename(label, count); - break; - } - } - } - } - - fn rename(&mut self, label: Label, count: usize) { - self.label = (label, count); - - self.aggregator - .do_send(RenameChain(self.cid, self.label.0.clone())); - } - - fn broadcast(&mut self) { - if let Some(msg) = self.serializer.finalize() { - for (_, feed) in self.feeds.iter() { - feed.do_send(msg.clone()); - } - } - } - - fn broadcast_finality(&mut self) { - if let Some(msg) = self.serializer.finalize() { - for feed in self.finality_feeds.values() { - feed.do_send(msg.clone()); - } - } - } - - /// Triggered when the number of nodes in this chain has changed, Aggregator will - /// propagate new counts to all connected feeds - fn update_count(&self) { - self.aggregator - .do_send(NodeCount(self.cid, self.nodes.len())); - } - - /// Check if the chain is stale (has not received a new best block in a while). - /// If so, find a new best block, ignoring any stale nodes and marking them as such. - fn update_stale_nodes(&mut self, now: u64) { - let threshold = now - STALE_TIMEOUT; - let timestamp = match self.timestamp { - Some(ts) => ts, - None => return, - }; - - if timestamp > threshold { - // Timestamp is in range, nothing to do - return; - } - - let mut best = Block::zero(); - let mut finalized = Block::zero(); - let mut timestamp = None; - - for (nid, node) in self.nodes.iter_mut() { - if !node.update_stale(threshold) { - if node.best().height > best.height { - best = *node.best(); - timestamp = Some(node.best_timestamp()); - } - - if node.finalized().height > finalized.height { - finalized = *node.finalized(); - } - } else { - self.serializer.push(feed::StaleNode(nid)); - } - } - - if self.best.height != 0 || self.finalized.height != 0 { - self.best = best; - self.finalized = finalized; - self.block_times.reset(); - self.timestamp = timestamp; - - self.serializer.push(feed::BestBlock( - self.best.height, - timestamp.unwrap_or(now), - None, - )); - self.serializer - .push(feed::BestFinalized(finalized.height, finalized.hash)); - } - } -} - -impl Actor for Chain { - type Context = Context; - - fn stopped(&mut self, _: &mut Self::Context) { - self.aggregator.do_send(DropChain(self.cid)); - - for (_, feed) in self.feeds.iter() { - feed.do_send(Unsubscribed) - } - } -} - -/// Message sent from the Aggregator to the Chain when new Node is connected -#[derive(Message)] -#[rtype(result = "()")] -pub struct AddNode { - /// Details of the node being added to the aggregator - pub node: NodeDetails, - /// Connection id used by the node connector for multiplexing parachains - pub conn_id: ConnId, - /// Address of the NodeConnector actor to which we send [`Initialize`] or [`Mute`] messages. - pub node_connector: Addr, -} - -/// Message sent from the NodeConnector to the Chain when it receives new telemetry data -#[derive(Message)] -#[rtype(result = "()")] -pub struct UpdateNode { - pub nid: NodeId, - pub payload: Payload, -} - -/// Message sent from the NodeConnector to the Chain when the connector disconnects -#[derive(Message)] -#[rtype(result = "()")] -pub struct RemoveNode(pub NodeId); - -/// Message sent from the Aggregator to the Chain when the connector wants to subscribe to that chain -#[derive(Message)] -#[rtype(result = "()")] -pub struct Subscribe(pub Addr); - -/// Message sent from the FeedConnector before it subscribes to a new chain, or if it disconnects -#[derive(Message)] -#[rtype(result = "()")] -pub struct Unsubscribe(pub FeedId); - -#[derive(Message)] -#[rtype(result = "()")] -pub struct SendFinality(pub FeedId); - -#[derive(Message)] -#[rtype(result = "()")] -pub struct NoMoreFinality(pub FeedId); - -/// Message sent from the NodeConnector to the Chain when it receives location data -#[derive(Message)] -#[rtype(result = "()")] -pub struct LocateNode { - pub nid: NodeId, - pub location: Arc, -} - -impl Handler for Chain { - type Result = (); - - fn handle(&mut self, msg: AddNode, ctx: &mut Self::Context) { - let AddNode { - node, - conn_id, - node_connector, - } = msg; - log::trace!(target: "Chain::AddNode", "New node connected. Chain '{}', node count goes from {} to {}", node.chain, self.nodes.len(), self.nodes.len() + 1); - self.increment_label_count(&node.chain); - - let nid = self.nodes.add(Node::new(node)); - let chain = ctx.address(); - - if node_connector - .try_send(Initialize { - nid, - conn_id, - chain, - }) - .is_err() - { - self.nodes.remove(nid); - } else if let Some(node) = self.nodes.get(nid) { - self.serializer.push(feed::AddedNode(nid, node)); - self.broadcast(); - } - - self.update_count(); - } -} - -impl Chain { - fn handle_block(&mut self, block: &Block, nid: NodeId) { - let mut propagation_time = None; - let now = now(); - let nodes_len = self.nodes.len(); - - self.update_stale_nodes(now); - - let node = match self.nodes.get_mut(nid) { - Some(node) => node, - None => return, - }; - - if node.update_block(*block) { - if block.height > self.best.height { - self.best = *block; - log::debug!( - "[{}] [nodes={}/feeds={}] new best block={}/{:?}", - self.label.0, - nodes_len, - self.feeds.len(), - self.best.height, - self.best.hash, - ); - if let Some(timestamp) = self.timestamp { - self.block_times.push(now - timestamp); - self.average_block_time = Some(self.block_times.average()); - } - self.timestamp = Some(now); - self.serializer.push(feed::BestBlock( - self.best.height, - now, - self.average_block_time, - )); - propagation_time = Some(0); - } else if block.height == self.best.height { - if let Some(timestamp) = self.timestamp { - propagation_time = Some(now - timestamp); - } - } - - if let Some(details) = node.update_details(now, propagation_time) { - self.serializer.push(feed::ImportedBlock(nid, details)); - } - } - } -} - -impl Handler for Chain { - type Result = (); - - fn handle(&mut self, msg: UpdateNode, _: &mut Self::Context) { - let UpdateNode { nid, payload } = msg; - - if let Some(block) = payload.best_block() { - self.handle_block(block, nid); - } - - if let Some(node) = self.nodes.get_mut(nid) { - match payload { - Payload::SystemInterval(ref interval) => { - if node.update_hardware(interval) { - self.serializer.push(feed::Hardware(nid, node.hardware())); - } - - if let Some(stats) = node.update_stats(interval) { - self.serializer.push(feed::NodeStatsUpdate(nid, stats)); - } - - if let Some(io) = node.update_io(interval) { - self.serializer.push(feed::NodeIOUpdate(nid, io)); - } - } - Payload::AfgAuthoritySet(authority) => { - node.set_validator_address(authority.authority_id.clone()); - self.broadcast(); - return; - } - Payload::AfgFinalized(finalized) => { - if let Ok(finalized_number) = finalized.finalized_number.parse::() - { - if let Some(addr) = node.details().validator.clone() { - self.serializer.push(feed::AfgFinalized( - addr, - finalized_number, - finalized.finalized_hash, - )); - self.broadcast_finality(); - } - } - return; - } - Payload::AfgReceivedPrecommit(precommit) => { - if let Ok(finalized_number) = - precommit.received.target_number.parse::() - { - if let Some(addr) = node.details().validator.clone() { - let voter = precommit.received.voter.clone(); - self.serializer.push(feed::AfgReceivedPrecommit( - addr, - finalized_number, - precommit.received.target_hash, - voter, - )); - self.broadcast_finality(); - } - } - return; - } - Payload::AfgReceivedPrevote(prevote) => { - if let Ok(finalized_number) = - prevote.received.target_number.parse::() - { - if let Some(addr) = node.details().validator.clone() { - let voter = prevote.received.voter.clone(); - self.serializer.push(feed::AfgReceivedPrevote( - addr, - finalized_number, - prevote.received.target_hash, - voter, - )); - self.broadcast_finality(); - } - } - return; - } - Payload::AfgReceivedCommit(_) => {} - _ => (), - } - - if let Some(block) = payload.finalized_block() { - if let Some(finalized) = node.update_finalized(block) { - self.serializer.push(feed::FinalizedBlock( - nid, - finalized.height, - finalized.hash, - )); - - if finalized.height > self.finalized.height { - self.finalized = *finalized; - self.serializer - .push(feed::BestFinalized(finalized.height, finalized.hash)); - } - } - } - } - - self.broadcast(); - } -} - -impl Handler for Chain { - type Result = (); - - fn handle(&mut self, msg: LocateNode, _: &mut Self::Context) { - let LocateNode { nid, location } = msg; - - if let Some(node) = self.nodes.get_mut(nid) { - self.serializer.push(feed::LocatedNode( - nid, - location.latitude, - location.longitude, - &location.city, - )); - - node.update_location(location); - } - } -} - -impl Handler for Chain { - type Result = (); - - fn handle(&mut self, msg: RemoveNode, ctx: &mut Self::Context) { - let RemoveNode(nid) = msg; - - if let Some(node) = self.nodes.remove(nid) { - self.decrement_label_count(&node.details().chain); - } - - if self.nodes.is_empty() { - log::info!("[{}] Lost all nodes, dropping...", self.label.0); - ctx.stop(); - } - - self.serializer.push(feed::RemovedNode(nid)); - self.broadcast(); - self.update_count(); - } -} - -impl Handler for Chain { - type Result = (); - - fn handle(&mut self, msg: Subscribe, ctx: &mut Self::Context) { - let Subscribe(feed) = msg; - let fid = self.feeds.add(feed.clone()); - - feed.do_send(Subscribed(fid, ctx.address().recipient())); - - self.serializer.push(feed::SubscribedTo(&self.label.0)); - self.serializer.push(feed::TimeSync(now())); - self.serializer.push(feed::BestBlock( - self.best.height, - self.timestamp.unwrap_or(0), - self.average_block_time, - )); - self.serializer.push(feed::BestFinalized( - self.finalized.height, - self.finalized.hash, - )); - - for (idx, (nid, node)) in self.nodes.iter().enumerate() { - // Send subscription confirmation and chain head before doing all the nodes, - // and continue sending batches of 32 nodes a time over the wire subsequently - if idx % 32 == 0 { - if let Some(serialized) = self.serializer.finalize() { - feed.do_send(serialized); - } - } - - self.serializer.push(feed::AddedNode(nid, node)); - self.serializer.push(feed::FinalizedBlock( - nid, - node.finalized().height, - node.finalized().hash, - )); - if node.stale() { - self.serializer.push(feed::StaleNode(nid)); - } - } - - if let Some(serialized) = self.serializer.finalize() { - feed.do_send(serialized); - } - } -} - -impl Handler for Chain { - type Result = (); - - fn handle(&mut self, msg: SendFinality, _ctx: &mut Self::Context) { - let SendFinality(fid) = msg; - if let Some(feed) = self.feeds.get(fid) { - self.finality_feeds.insert(fid, feed.clone()); - } - - // info!("Added new finality feed {}", fid); - } -} - -impl Handler for Chain { - type Result = (); - - fn handle(&mut self, msg: NoMoreFinality, _: &mut Self::Context) { - let NoMoreFinality(fid) = msg; - - // info!("Removed finality feed {}", fid); - self.finality_feeds.remove(&fid); - } -} - -impl Handler for Chain { - type Result = (); - - fn handle(&mut self, msg: Unsubscribe, _: &mut Self::Context) { - let Unsubscribe(fid) = msg; - - if let Some(feed) = self.feeds.get(fid) { - self.serializer.push(feed::UnsubscribedFrom(&self.label.0)); - - if let Some(serialized) = self.serializer.finalize() { - feed.do_send(serialized); - } - } - - self.feeds.remove(fid); - self.finality_feeds.remove(&fid); - } -} diff --git a/backend/core/src/feed.rs b/backend/core/src/feed.rs deleted file mode 100644 index e1915f4..0000000 --- a/backend/core/src/feed.rs +++ /dev/null @@ -1,195 +0,0 @@ -use serde::ser::{SerializeTuple, Serializer}; -use serde::Serialize; -use std::mem; - -use crate::node::Node; -use crate::types::{ - Address, BlockDetails, BlockHash, BlockNumber, NodeHardware, NodeIO, NodeId, NodeStats, - Timestamp, -}; -use serde_json::to_writer; - -pub mod connector; - -use connector::Serialized; - -pub trait FeedMessage: Serialize { - const ACTION: u8; -} - -pub struct FeedMessageSerializer { - /// Current buffer, - buffer: Vec, -} - -const BUFCAP: usize = 128; - -impl FeedMessageSerializer { - pub fn new() -> Self { - Self { - buffer: Vec::with_capacity(BUFCAP), - } - } - - pub fn push(&mut self, msg: Message) - where - Message: FeedMessage, - { - let glue = match self.buffer.len() { - 0 => b'[', - _ => b',', - }; - - self.buffer.push(glue); - let _ = to_writer(&mut self.buffer, &Message::ACTION); - self.buffer.push(b','); - let _ = to_writer(&mut self.buffer, &msg); - } - - pub fn finalize(&mut self) -> Option { - if self.buffer.is_empty() { - return None; - } - - self.buffer.push(b']'); - - let bytes = mem::replace(&mut self.buffer, Vec::with_capacity(BUFCAP)).into(); - - Some(Serialized(bytes)) - } -} - -macro_rules! actions { - ($($action:literal: $t:ty,)*) => { - $( - impl FeedMessage for $t { - const ACTION: u8 = $action; - } - )* - } -} - -actions! { - 0x00: Version, - 0x01: BestBlock, - 0x02: BestFinalized, - 0x03: AddedNode<'_>, - 0x04: RemovedNode, - 0x05: LocatedNode<'_>, - 0x06: ImportedBlock<'_>, - 0x07: FinalizedBlock, - 0x08: NodeStatsUpdate<'_>, - 0x09: Hardware<'_>, - 0x0A: TimeSync, - 0x0B: AddedChain<'_>, - 0x0C: RemovedChain<'_>, - 0x0D: SubscribedTo<'_>, - 0x0E: UnsubscribedFrom<'_>, - 0x0F: Pong<'_>, - 0x10: AfgFinalized, - 0x11: AfgReceivedPrevote, - 0x12: AfgReceivedPrecommit, - 0x13: AfgAuthoritySet, - 0x14: StaleNode, - 0x15: NodeIOUpdate<'_>, -} - -#[derive(Serialize)] -pub struct Version(pub usize); - -#[derive(Serialize)] -pub struct BestBlock(pub BlockNumber, pub Timestamp, pub Option); - -#[derive(Serialize)] -pub struct BestFinalized(pub BlockNumber, pub BlockHash); - -pub struct AddedNode<'a>(pub NodeId, pub &'a Node); - -#[derive(Serialize)] -pub struct RemovedNode(pub NodeId); - -#[derive(Serialize)] -pub struct LocatedNode<'a>(pub NodeId, pub f32, pub f32, pub &'a str); - -#[derive(Serialize)] -pub struct ImportedBlock<'a>(pub NodeId, pub &'a BlockDetails); - -#[derive(Serialize)] -pub struct FinalizedBlock(pub NodeId, pub BlockNumber, pub BlockHash); - -#[derive(Serialize)] -pub struct NodeStatsUpdate<'a>(pub NodeId, pub &'a NodeStats); - -#[derive(Serialize)] -pub struct NodeIOUpdate<'a>(pub NodeId, pub &'a NodeIO); - -#[derive(Serialize)] -pub struct Hardware<'a>(pub NodeId, pub &'a NodeHardware); - -#[derive(Serialize)] -pub struct TimeSync(pub u64); - -#[derive(Serialize)] -pub struct AddedChain<'a>(pub &'a str, pub usize); - -#[derive(Serialize)] -pub struct RemovedChain<'a>(pub &'a str); - -#[derive(Serialize)] -pub struct SubscribedTo<'a>(pub &'a str); - -#[derive(Serialize)] -pub struct UnsubscribedFrom<'a>(pub &'a str); - -#[derive(Serialize)] -pub struct Pong<'a>(pub &'a str); - -#[derive(Serialize)] -pub struct AfgFinalized(pub Address, pub BlockNumber, pub BlockHash); - -#[derive(Serialize)] -pub struct AfgReceivedPrevote( - pub Address, - pub BlockNumber, - pub BlockHash, - pub Option
, -); - -#[derive(Serialize)] -pub struct AfgReceivedPrecommit( - pub Address, - pub BlockNumber, - pub BlockHash, - pub Option
, -); - -#[derive(Serialize)] -pub struct AfgAuthoritySet( - pub Address, - pub Address, - pub Address, - pub BlockNumber, - pub BlockHash, -); - -#[derive(Serialize)] -pub struct StaleNode(pub NodeId); - -impl Serialize for AddedNode<'_> { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - let AddedNode(nid, node) = self; - let mut tup = serializer.serialize_tuple(8)?; - tup.serialize_element(nid)?; - tup.serialize_element(node.details())?; - tup.serialize_element(node.stats())?; - tup.serialize_element(node.io())?; - tup.serialize_element(node.hardware())?; - tup.serialize_element(node.block_details())?; - tup.serialize_element(&node.location())?; - tup.serialize_element(&node.startup_time())?; - tup.end() - } -} diff --git a/backend/core/src/feed/connector.rs b/backend/core/src/feed/connector.rs deleted file mode 100644 index 926a7f9..0000000 --- a/backend/core/src/feed/connector.rs +++ /dev/null @@ -1,217 +0,0 @@ -use crate::aggregator::{Aggregator, Connect, Disconnect, NoMoreFinality, SendFinality, Subscribe}; -use crate::chain::Unsubscribe; -use crate::feed::{FeedMessageSerializer, Pong}; -use crate::util::fnv; -use actix::prelude::*; -use actix_web_actors::ws; -use bytes::Bytes; -use std::time::{Duration, Instant}; - -pub type FeedId = usize; - -/// How often heartbeat pings are sent -const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(20); -/// How long before lack of client response causes a timeout -const CLIENT_TIMEOUT: Duration = Duration::from_secs(60); - -pub struct FeedConnector { - /// FeedId that Aggregator holds of this actor - fid_aggregator: FeedId, - /// FeedId that Chain holds of this actor - fid_chain: FeedId, - /// Client must send ping at least once per 10 seconds (CLIENT_TIMEOUT), - hb: Instant, - /// Aggregator actor address - aggregator: Addr, - /// Chain actor address - chain: Option>, - /// FNV hash of the chain label, optimization to avoid double-subscribing - chain_label_hash: u64, - /// Message serializer - serializer: FeedMessageSerializer, -} - -impl Actor for FeedConnector { - type Context = ws::WebsocketContext; - - fn started(&mut self, ctx: &mut Self::Context) { - self.heartbeat(ctx); - self.aggregator.do_send(Connect(ctx.address())); - } - - fn stopped(&mut self, _: &mut Self::Context) { - if let Some(chain) = self.chain.take() { - let _ = chain.do_send(Unsubscribe(self.fid_chain)); - } - - self.aggregator.do_send(Disconnect(self.fid_aggregator)); - } -} - -impl FeedConnector { - pub fn new(aggregator: Addr) -> Self { - Self { - // Garbage id, will be replaced by the Connected message - fid_aggregator: !0, - // Garbage id, will be replaced by the Subscribed message - fid_chain: !0, - hb: Instant::now(), - aggregator, - chain: None, - chain_label_hash: 0, - serializer: FeedMessageSerializer::new(), - } - } - - fn heartbeat(&self, ctx: &mut ::Context) { - ctx.run_interval(HEARTBEAT_INTERVAL, |act, ctx| { - // check client heartbeats - if Instant::now().duration_since(act.hb) > CLIENT_TIMEOUT { - // stop actor - ctx.stop(); - } else { - ctx.ping(b"") - } - }); - } - - fn handle_cmd(&mut self, cmd: &str, payload: &str, ctx: &mut ::Context) { - match cmd { - "subscribe" => { - match fnv(payload) { - hash if hash == self.chain_label_hash => return, - hash => self.chain_label_hash = hash, - } - - self.aggregator - .send(Subscribe { - chain: payload.into(), - feed: ctx.address(), - }) - .into_actor(self) - .then(|res, actor, _| { - match res { - Ok(true) => (), - // Chain not found, reset hash - _ => actor.chain_label_hash = 0, - } - async {}.into_actor(actor) - }) - .wait(ctx); - } - "send-finality" => { - self.aggregator.do_send(SendFinality { - chain: payload.into(), - fid: self.fid_chain, - }); - } - "no-more-finality" => { - self.aggregator.do_send(NoMoreFinality { - chain: payload.into(), - fid: self.fid_chain, - }); - } - "ping" => { - self.serializer.push(Pong(payload)); - if let Some(serialized) = self.serializer.finalize() { - ctx.binary(serialized.0); - } - } - _ => (), - } - } -} - -/// Message sent form Chain to the FeedConnector upon successful subscription -#[derive(Message)] -#[rtype(result = "()")] -pub struct Subscribed(pub FeedId, pub Recipient); - -#[derive(Message)] -#[rtype(result = "()")] -pub struct Unsubscribed; - -/// Message sent from Aggregator to FeedConnector upon successful connection -#[derive(Message)] -#[rtype(result = "()")] -pub struct Connected(pub FeedId); - -/// Message sent from either Aggregator or Chain to FeedConnector containing -/// serialized message(s) for the frontend -/// -/// Since Bytes is ARC'ed, this is cheap to clone -#[derive(Message, Clone)] -#[rtype(result = "()")] -pub struct Serialized(pub Bytes); - -impl StreamHandler> for FeedConnector { - fn handle(&mut self, msg: Result, ctx: &mut Self::Context) { - match msg { - Ok(ws::Message::Ping(msg)) => { - self.hb = Instant::now(); - ctx.pong(&msg); - } - Ok(ws::Message::Pong(_)) => self.hb = Instant::now(), - Ok(ws::Message::Text(text)) => { - if let Some(idx) = text.find(':') { - let cmd = &text[..idx]; - let payload = &text[idx + 1..]; - - log::info!("New FEED message: {}", cmd); - - self.handle_cmd(cmd, payload, ctx); - } - } - Ok(ws::Message::Close(_)) => ctx.stop(), - Ok(_) => (), - Err(error) => { - log::error!("{:?}", error); - ctx.stop(); - } - } - } -} - -impl Handler for FeedConnector { - type Result = (); - - fn handle(&mut self, msg: Subscribed, _: &mut Self::Context) { - let Subscribed(fid_chain, chain) = msg; - - if let Some(current) = self.chain.take() { - let _ = current.do_send(Unsubscribe(self.fid_chain)); - } - - self.fid_chain = fid_chain; - self.chain = Some(chain); - } -} - -impl Handler for FeedConnector { - type Result = (); - - fn handle(&mut self, _: Unsubscribed, _: &mut Self::Context) { - self.chain = None; - self.chain_label_hash = 0; - } -} - -impl Handler for FeedConnector { - type Result = (); - - fn handle(&mut self, msg: Connected, _: &mut Self::Context) { - let Connected(fid_aggregator) = msg; - - self.fid_aggregator = fid_aggregator; - } -} - -impl Handler for FeedConnector { - type Result = (); - - fn handle(&mut self, msg: Serialized, ctx: &mut Self::Context) { - let Serialized(bytes) = msg; - - ctx.binary(bytes); - } -} diff --git a/backend/core/src/main.rs b/backend/core/src/main.rs deleted file mode 100644 index 9e373f9..0000000 --- a/backend/core/src/main.rs +++ /dev/null @@ -1,187 +0,0 @@ -use std::collections::HashSet; -use std::iter::FromIterator; -use std::net::Ipv4Addr; - -use actix::prelude::*; -use actix_http::ws::Codec; -use actix_web::{get, middleware, web, App, Error, HttpRequest, HttpResponse, HttpServer}; -use actix_web_actors::ws; -use clap::Clap; -use simple_logger::SimpleLogger; - -mod aggregator; -mod chain; -mod feed; -mod node; -mod shard; -mod types; -mod util; - -use aggregator::{Aggregator, GetHealth}; -use feed::connector::FeedConnector; -use node::connector::NodeConnector; -use shard::connector::ShardConnector; -use util::{Locator, LocatorFactory}; - -const VERSION: &str = env!("CARGO_PKG_VERSION"); -const AUTHORS: &str = env!("CARGO_PKG_AUTHORS"); -const NAME: &str = "Substrate Telemetry Backend"; -const ABOUT: &str = "This is the Telemetry Backend that injects and provide the data sent by Substrate/Polkadot nodes"; - -#[derive(Clap, Debug)] -#[clap(name = NAME, version = VERSION, author = AUTHORS, about = ABOUT)] -struct Opts { - #[clap( - short = 'l', - long = "listen", - default_value = "127.0.0.1:8000", - about = "This is the socket address Telemetry is listening to. This is restricted to localhost (127.0.0.1) by default and should be fine for most use cases. If you are using Telemetry in a container, you likely want to set this to '0.0.0.0:8000'" - )] - socket: std::net::SocketAddr, - #[clap( - required = false, - long = "denylist", - about = "Space delimited list of chains that are not allowed to connect to telemetry. Case sensitive." - )] - denylist: Vec, - #[clap( - arg_enum, - required = false, - long = "log", - default_value = "info", - about = "Log level." - )] - log_level: LogLevel, -} - -#[derive(Clap, Debug, PartialEq)] -enum LogLevel { - Error, - Warn, - Info, - Debug, - Trace, -} - -impl From<&LogLevel> for log::LevelFilter { - fn from(log_level: &LogLevel) -> Self { - match log_level { - LogLevel::Error => log::LevelFilter::Error, - LogLevel::Warn => log::LevelFilter::Warn, - LogLevel::Info => log::LevelFilter::Info, - LogLevel::Debug => log::LevelFilter::Debug, - LogLevel::Trace => log::LevelFilter::Trace, - } - } -} - -/// Entry point for connecting nodes -#[get("/submit")] -async fn node_route( - req: HttpRequest, - stream: web::Payload, - aggregator: web::Data>, - locator: web::Data>, -) -> Result { - let ip = req - .connection_info() - .realip_remote_addr() - .and_then(|mut addr| { - if let Some(port_idx) = addr.find(':') { - addr = &addr[..port_idx]; - } - addr.parse::().ok() - }); - - let mut res = ws::handshake(&req)?; - let aggregator = aggregator.get_ref().clone(); - let locator = locator.get_ref().clone().recipient(); - - Ok(res.streaming(ws::WebsocketContext::with_codec( - NodeConnector::new(aggregator, locator, ip), - stream, - Codec::new().max_size(10 * 1024 * 1024), // 10mb frame limit - ))) -} - -#[get("/shard_submit/{chain_hash}")] -async fn shard_route( - req: HttpRequest, - stream: web::Payload, - aggregator: web::Data>, - path: web::Path>, -) -> Result { - let hash_str = path.into_inner(); - let genesis_hash = hash_str.parse()?; - - let mut res = ws::handshake(&req)?; - - let aggregator = aggregator.get_ref().clone(); - - Ok(res.streaming(ws::WebsocketContext::with_codec( - ShardConnector::new(aggregator, genesis_hash), - stream, - Codec::new().max_size(10 * 1024 * 1024), // 10mb frame limit - ))) -} - -/// Entry point for connecting feeds -#[get("/feed")] -async fn feed_route( - req: HttpRequest, - stream: web::Payload, - aggregator: web::Data>, -) -> Result { - ws::start( - FeedConnector::new(aggregator.get_ref().clone()), - &req, - stream, - ) -} - -/// Entry point for health check monitoring bots -#[get("/health")] -async fn health(aggregator: web::Data>) -> Result { - match aggregator.send(GetHealth).await { - Ok(count) => { - let body = format!("Connected chains: {}", count); - - HttpResponse::Ok().body(body).await - } - Err(error) => { - log::error!("Health check mailbox error: {:?}", error); - - HttpResponse::InternalServerError().await - } - } -} - -/// Telemetry entry point. Listening by default on 127.0.0.1:8000. -/// This can be changed using the `PORT` and `BIND` ENV variables. -#[actix_web::main] -async fn main() -> std::io::Result<()> { - let opts = Opts::parse(); - let log_level = &opts.log_level; - SimpleLogger::new() - .with_level(log_level.into()) - .init() - .expect("Must be able to start a logger"); - - let denylist = HashSet::from_iter(opts.denylist); - let aggregator = Aggregator::new(denylist).start(); - let factory = LocatorFactory::new(); - let locator = SyncArbiter::start(4, move || factory.create()); - log::info!("Starting telemetry version: {}", env!("CARGO_PKG_VERSION")); - HttpServer::new(move || { - App::new() - .wrap(middleware::NormalizePath::default()) - .data(aggregator.clone()) - .data(locator.clone()) - .service(node_route) - .service(feed_route) - .service(health) - }) - .bind(opts.socket)? - .run() - .await -} diff --git a/backend/core/src/node/connector.rs b/backend/core/src/node/connector.rs deleted file mode 100644 index 81fddd8..0000000 --- a/backend/core/src/node/connector.rs +++ /dev/null @@ -1,274 +0,0 @@ -use std::collections::BTreeMap; -use std::mem; -use std::net::Ipv4Addr; -use std::time::{Duration, Instant}; - -use crate::aggregator::{AddNode, Aggregator}; -use crate::chain::{Chain, RemoveNode, UpdateNode}; -use crate::node::message::{NodeMessage, Payload}; -use crate::node::NodeId; -use crate::types::ConnId; -use crate::util::LocateRequest; -use actix::prelude::*; -use actix_http::ws::Item; -use actix_web_actors::ws::{self, CloseReason}; -use bytes::{Bytes, BytesMut}; - -/// How often heartbeat pings are sent -const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(20); -/// How long before lack of client response causes a timeout -const CLIENT_TIMEOUT: Duration = Duration::from_secs(60); -/// Continuation buffer limit, 10mb -const CONT_BUF_LIMIT: usize = 10 * 1024 * 1024; - -pub struct NodeConnector { - /// Multiplexing connections by id - multiplex: BTreeMap, - /// Client must send ping at least once every 60 seconds (CLIENT_TIMEOUT), - hb: Instant, - /// Aggregator actor address - aggregator: Addr, - /// IP address of the node this connector is responsible for - ip: Option, - /// Actix address of location services - locator: Recipient, - /// Buffer for constructing continuation messages - contbuf: BytesMut, -} - -enum ConnMultiplex { - Connected { - /// Id of the node this multiplex connector is responsible for handling - nid: NodeId, - /// Chain address to which this multiplex connector is delegating messages - chain: Addr, - }, - Waiting { - /// Backlog of messages to be sent once we get a recipient handle to the chain - backlog: Vec, - }, -} - -impl Default for ConnMultiplex { - fn default() -> Self { - ConnMultiplex::Waiting { - backlog: Vec::new(), - } - } -} - -impl Actor for NodeConnector { - type Context = ws::WebsocketContext; - - fn started(&mut self, ctx: &mut Self::Context) { - self.heartbeat(ctx); - } - - fn stopped(&mut self, _: &mut Self::Context) { - for mx in self.multiplex.values() { - if let ConnMultiplex::Connected { chain, nid } = mx { - chain.do_send(RemoveNode(*nid)); - } - } - } -} - -impl NodeConnector { - pub fn new( - aggregator: Addr, - locator: Recipient, - ip: Option, - ) -> Self { - Self { - multiplex: BTreeMap::new(), - hb: Instant::now(), - aggregator, - ip, - locator, - contbuf: BytesMut::new(), - } - } - - fn heartbeat(&self, ctx: &mut ::Context) { - ctx.run_interval(HEARTBEAT_INTERVAL, |act, ctx| { - // check client heartbeats - if Instant::now().duration_since(act.hb) > CLIENT_TIMEOUT { - // stop actor - ctx.close(Some(CloseReason { - code: ws::CloseCode::Abnormal, - description: Some("Missed heartbeat".into()), - })); - ctx.stop(); - } - }); - } - - fn handle_message( - &mut self, - msg: NodeMessage, - ctx: &mut ::Context, - ) { - let conn_id = msg.id(); - let payload = msg.into(); - - match self.multiplex.entry(conn_id).or_default() { - ConnMultiplex::Connected { nid, chain } => { - chain.do_send(UpdateNode { - nid: *nid, - payload, - }); - } - ConnMultiplex::Waiting { backlog } => { - if let Payload::SystemConnected(connected) = payload { - self.aggregator.do_send(AddNode { - node: connected.node, - genesis_hash: connected.genesis_hash, - conn_id, - node_connector: ctx.address(), - }); - } else { - if backlog.len() >= 10 { - backlog.remove(0); - } - - backlog.push(payload); - } - } - } - } - - fn start_frame(&mut self, bytes: &[u8]) { - if !self.contbuf.is_empty() { - log::error!("Unused continuation buffer"); - self.contbuf.clear(); - } - self.continue_frame(bytes); - } - - fn continue_frame(&mut self, bytes: &[u8]) { - if self.contbuf.len() + bytes.len() <= CONT_BUF_LIMIT { - self.contbuf.extend_from_slice(&bytes); - } else { - log::error!("Continuation buffer overflow"); - self.contbuf = BytesMut::new(); - } - } - - fn finish_frame(&mut self) -> Bytes { - mem::replace(&mut self.contbuf, BytesMut::new()).freeze() - } -} - -#[derive(Message)] -#[rtype(result = "()")] -pub struct Mute { - pub reason: CloseReason, -} - -impl Handler for NodeConnector { - type Result = (); - fn handle(&mut self, msg: Mute, ctx: &mut Self::Context) { - let Mute { reason } = msg; - log::debug!(target: "NodeConnector::Mute", "Muting a node. Reason: {:?}", reason.description); - - ctx.close(Some(reason)); - ctx.stop(); - } -} - -#[derive(Message)] -#[rtype(result = "()")] -pub struct Initialize { - pub nid: NodeId, - pub conn_id: ConnId, - pub chain: Addr, -} - -impl Handler for NodeConnector { - type Result = (); - - fn handle(&mut self, msg: Initialize, _: &mut Self::Context) { - let Initialize { - nid, - conn_id, - chain, - } = msg; - log::trace!(target: "NodeConnector::Initialize", "Initializing a node, nid={}, on conn_id={}", nid, conn_id); - let mx = self.multiplex.entry(conn_id).or_default(); - - if let ConnMultiplex::Waiting { backlog } = mx { - for payload in backlog.drain(..) { - chain.do_send(UpdateNode { - nid, - payload, - }); - } - - *mx = ConnMultiplex::Connected { - nid, - chain: chain.clone(), - }; - }; - - // Acquire the node's physical location - if let Some(ip) = self.ip { - let _ = self.locator.do_send(LocateRequest { ip, nid, chain }); - } - } -} - -impl StreamHandler> for NodeConnector { - fn handle(&mut self, msg: Result, ctx: &mut Self::Context) { - self.hb = Instant::now(); - - let data = match msg { - Ok(ws::Message::Ping(msg)) => { - ctx.pong(&msg); - return; - } - Ok(ws::Message::Pong(_)) => return, - Ok(ws::Message::Text(text)) => text.into_bytes(), - Ok(ws::Message::Binary(data)) => data, - Ok(ws::Message::Close(reason)) => { - ctx.close(reason); - ctx.stop(); - return; - } - Ok(ws::Message::Nop) => return, - Ok(ws::Message::Continuation(cont)) => match cont { - Item::FirstText(bytes) | Item::FirstBinary(bytes) => { - self.start_frame(&bytes); - return; - } - Item::Continue(bytes) => { - self.continue_frame(&bytes); - return; - } - Item::Last(bytes) => { - self.continue_frame(&bytes); - self.finish_frame() - } - }, - Err(error) => { - log::error!("{:?}", error); - ctx.stop(); - return; - } - }; - - match serde_json::from_slice(&data) { - Ok(msg) => self.handle_message(msg, ctx), - #[cfg(debug)] - Err(err) => { - let data: &[u8] = data.get(..512).unwrap_or_else(|| &data); - log::warn!( - "Failed to parse node message: {} {}", - err, - std::str::from_utf8(data).unwrap_or_else(|_| "INVALID UTF8") - ) - } - #[cfg(not(debug))] - Err(_) => (), - } - } -} diff --git a/backend/core/src/node/message.rs b/backend/core/src/node/message.rs deleted file mode 100644 index 64cd8a5..0000000 --- a/backend/core/src/node/message.rs +++ /dev/null @@ -1,196 +0,0 @@ -use crate::node::NodeDetails; -use crate::types::{Block, BlockHash, BlockNumber, ConnId}; -use crate::util::Hash; -use actix::prelude::*; -use serde::de::IgnoredAny; -use serde::Deserialize; - -#[derive(Deserialize, Debug, Message)] -#[rtype(result = "()")] -#[serde(untagged)] -pub enum NodeMessage { - V1 { - #[serde(flatten)] - payload: Payload, - }, - V2 { - id: ConnId, - payload: Payload, - }, -} - -impl NodeMessage { - /// Returns the connection ID or 0 if there is no ID. - pub fn id(&self) -> ConnId { - match self { - NodeMessage::V1 { .. } => 0, - NodeMessage::V2 { id, .. } => *id, - } - } -} - -impl From for Payload { - fn from(msg: NodeMessage) -> Payload { - match msg { - NodeMessage::V1 { payload, .. } | NodeMessage::V2 { payload, .. } => payload, - } - } -} - -#[derive(Deserialize, Debug)] -#[serde(tag = "msg")] -pub enum Payload { - #[serde(rename = "system.connected")] - SystemConnected(SystemConnected), - #[serde(rename = "system.interval")] - SystemInterval(SystemInterval), - #[serde(rename = "block.import")] - BlockImport(Block), - #[serde(rename = "notify.finalized")] - NotifyFinalized(Finalized), - #[serde(rename = "txpool.import")] - TxPoolImport(IgnoredAny), - #[serde(rename = "afg.finalized")] - AfgFinalized(AfgFinalized), - #[serde(rename = "afg.received_precommit")] - AfgReceivedPrecommit(AfgReceivedPrecommit), - #[serde(rename = "afg.received_prevote")] - AfgReceivedPrevote(AfgReceivedPrevote), - #[serde(rename = "afg.received_commit")] - AfgReceivedCommit(AfgReceivedCommit), - #[serde(rename = "afg.authority_set")] - AfgAuthoritySet(AfgAuthoritySet), - #[serde(rename = "afg.finalized_blocks_up_to")] - AfgFinalizedBlocksUpTo(IgnoredAny), - #[serde(rename = "aura.pre_sealed_block")] - AuraPreSealedBlock(IgnoredAny), - #[serde(rename = "prepared_block_for_proposing")] - PreparedBlockForProposing(IgnoredAny), -} - -#[derive(Deserialize, Debug)] -pub struct SystemConnected { - pub genesis_hash: Hash, - #[serde(flatten)] - pub node: NodeDetails, -} - -#[derive(Deserialize, Debug)] -pub struct SystemInterval { - pub peers: Option, - pub txcount: Option, - pub bandwidth_upload: Option, - pub bandwidth_download: Option, - pub finalized_height: Option, - pub finalized_hash: Option, - #[serde(flatten)] - pub block: Option, - pub used_state_cache_size: Option, -} - -#[derive(Deserialize, Debug)] -pub struct Finalized { - #[serde(rename = "best")] - pub hash: BlockHash, - pub height: Box, -} - -#[derive(Deserialize, Debug)] -pub struct AfgAuthoritySet { - pub authority_id: Box, - pub authorities: Box, - pub authority_set_id: Box, -} - -#[derive(Deserialize, Debug, Clone)] -pub struct AfgFinalized { - pub finalized_hash: BlockHash, - pub finalized_number: Box, -} - -#[derive(Deserialize, Debug, Clone)] -pub struct AfgReceived { - pub target_hash: BlockHash, - pub target_number: Box, - pub voter: Option>, -} - -#[derive(Deserialize, Debug, Clone)] -pub struct AfgReceivedPrecommit { - #[serde(flatten)] - pub received: AfgReceived, -} - -#[derive(Deserialize, Debug, Clone)] -pub struct AfgReceivedPrevote { - #[serde(flatten)] - pub received: AfgReceived, -} - -#[derive(Deserialize, Debug, Clone)] -pub struct AfgReceivedCommit { - #[serde(flatten)] - pub received: AfgReceived, -} - -impl Block { - pub fn zero() -> Self { - Block { - hash: BlockHash::from([0; 32]), - height: 0, - } - } -} - -impl Payload { - pub fn best_block(&self) -> Option<&Block> { - match self { - Payload::BlockImport(block) => Some(block), - Payload::SystemInterval(SystemInterval { block, .. }) => block.as_ref(), - _ => None, - } - } - - pub fn finalized_block(&self) -> Option { - match self { - Payload::SystemInterval(ref interval) => Some(Block { - hash: interval.finalized_hash?, - height: interval.finalized_height?, - }), - Payload::NotifyFinalized(ref finalized) => Some(Block { - hash: finalized.hash, - height: finalized.height.parse().ok()?, - }), - _ => None, - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn message_v1() { - let json = r#"{"msg":"notify.finalized","level":"INFO","ts":"2021-01-13T12:38:25.410794650+01:00","best":"0x031c3521ca2f9c673812d692fc330b9a18e18a2781e3f9976992f861fd3ea0cb","height":"50"}"#; - assert!( - matches!( - serde_json::from_str::(json).unwrap(), - NodeMessage::V1 { .. }, - ), - "message did not match variant V1", - ); - } - - #[test] - fn message_v2() { - let json = r#"{"id":1,"ts":"2021-01-13T12:22:20.053527101+01:00","payload":{"best":"0xcc41708573f2acaded9dd75e07dac2d4163d136ca35b3061c558d7a35a09dd8d","height":"209","msg":"notify.finalized"}}"#; - assert!( - matches!( - serde_json::from_str::(json).unwrap(), - NodeMessage::V2 { .. }, - ), - "message did not match variant V2", - ); - } -} diff --git a/backend/core/src/shard.rs b/backend/core/src/shard.rs deleted file mode 100644 index 30c7678..0000000 --- a/backend/core/src/shard.rs +++ /dev/null @@ -1,13 +0,0 @@ -use crate::node::message::Payload; -use serde::Deserialize; - -pub mod connector; - -/// Alias for the ID of the node connection -type ShardConnId = usize; - -#[derive(Deserialize)] -pub struct ShardMessage { - pub conn_id: ShardConnId, - pub payload: Payload, -} diff --git a/backend/core/src/shard/connector.rs b/backend/core/src/shard/connector.rs deleted file mode 100644 index 111ad1f..0000000 --- a/backend/core/src/shard/connector.rs +++ /dev/null @@ -1,156 +0,0 @@ -use std::mem; -use std::time::{Duration, Instant}; - -use crate::aggregator::{AddNode, Aggregator}; -use crate::chain::{Chain, RemoveNode, UpdateNode}; -use crate::shard::ShardMessage; -use crate::types::NodeId; -use crate::util::{DenseMap, Hash}; -use actix::prelude::*; -use actix_http::ws::Item; -use actix_web_actors::ws::{self, CloseReason}; -use bincode::Options; -use bytes::{Bytes, BytesMut}; - -/// How often heartbeat pings are sent -const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(20); -/// How long before lack of client response causes a timeout -const CLIENT_TIMEOUT: Duration = Duration::from_secs(60); -/// Continuation buffer limit, 10mb -const CONT_BUF_LIMIT: usize = 10 * 1024 * 1024; - -pub struct ShardConnector { - /// Client must send ping at least once every 60 seconds (CLIENT_TIMEOUT), - hb: Instant, - /// Aggregator actor address - aggregator: Addr, - /// Genesis hash of the chain this connection will be submitting data for - genesis_hash: Hash, - /// Chain address to which this multiplex connector is delegating messages - chain: Option>, - /// Mapping `ShardConnId` to `NodeId` - nodes: DenseMap, - /// Buffer for constructing continuation messages - contbuf: BytesMut, -} - -impl Actor for ShardConnector { - type Context = ws::WebsocketContext; - - fn started(&mut self, ctx: &mut Self::Context) { - self.heartbeat(ctx); - } - - fn stopped(&mut self, _: &mut Self::Context) { - if let Some(ref chain) = self.chain { - for (_, nid) in self.nodes.iter() { - chain.do_send(RemoveNode(*nid)) - } - } - } -} - -impl ShardConnector { - pub fn new(aggregator: Addr, genesis_hash: Hash) -> Self { - Self { - hb: Instant::now(), - aggregator, - genesis_hash, - chain: None, - nodes: DenseMap::new(), - contbuf: BytesMut::new(), - } - } - - fn heartbeat(&self, ctx: &mut ::Context) { - ctx.run_interval(HEARTBEAT_INTERVAL, |act, ctx| { - // check client heartbeats - if Instant::now().duration_since(act.hb) > CLIENT_TIMEOUT { - // stop actor - ctx.close(Some(CloseReason { - code: ws::CloseCode::Abnormal, - description: Some("Missed heartbeat".into()), - })); - ctx.stop(); - } - }); - } - - fn handle_message(&mut self, msg: ShardMessage, ctx: &mut ::Context) { - let ShardMessage { conn_id, payload } = msg; - - // TODO: get `NodeId` for `ShardConnId` and proxy payload to `self.chain`. - } - - fn start_frame(&mut self, bytes: &[u8]) { - if !self.contbuf.is_empty() { - log::error!("Unused continuation buffer"); - self.contbuf.clear(); - } - self.continue_frame(bytes); - } - - fn continue_frame(&mut self, bytes: &[u8]) { - if self.contbuf.len() + bytes.len() <= CONT_BUF_LIMIT { - self.contbuf.extend_from_slice(&bytes); - } else { - log::error!("Continuation buffer overflow"); - self.contbuf = BytesMut::new(); - } - } - - fn finish_frame(&mut self) -> Bytes { - mem::replace(&mut self.contbuf, BytesMut::new()).freeze() - } -} - -impl StreamHandler> for ShardConnector { - fn handle(&mut self, msg: Result, ctx: &mut Self::Context) { - self.hb = Instant::now(); - - let data = match msg { - Ok(ws::Message::Ping(msg)) => { - ctx.pong(&msg); - return; - } - Ok(ws::Message::Pong(_)) => return, - Ok(ws::Message::Text(text)) => text.into_bytes(), - Ok(ws::Message::Binary(data)) => data, - Ok(ws::Message::Close(reason)) => { - ctx.close(reason); - ctx.stop(); - return; - } - Ok(ws::Message::Nop) => return, - Ok(ws::Message::Continuation(cont)) => match cont { - Item::FirstText(bytes) | Item::FirstBinary(bytes) => { - self.start_frame(&bytes); - return; - } - Item::Continue(bytes) => { - self.continue_frame(&bytes); - return; - } - Item::Last(bytes) => { - self.continue_frame(&bytes); - self.finish_frame() - } - }, - Err(error) => { - log::error!("{:?}", error); - ctx.stop(); - return; - } - }; - - match bincode::options().deserialize(&data) { - Ok(msg) => self.handle_message(msg, ctx), - #[cfg(debug)] - Err(err) => { - log::warn!("Failed to parse shard message: {}", err,) - } - #[cfg(not(debug))] - Err(_) => (), - } - } -} diff --git a/backend/core/src/util.rs b/backend/core/src/util.rs deleted file mode 100644 index 374ec78..0000000 --- a/backend/core/src/util.rs +++ /dev/null @@ -1,31 +0,0 @@ -mod dense_map; -mod hash; -mod location; -mod mean_list; -mod num_stats; - -pub use dense_map::DenseMap; -pub use hash::Hash; -pub use location::{LocateRequest, Locator, LocatorFactory}; -pub use mean_list::MeanList; -pub use num_stats::NumStats; - -pub fn fnv>(data: D) -> u64 { - use fnv::FnvHasher; - use std::hash::Hasher; - - let mut hasher = FnvHasher::default(); - - hasher.write(data.as_ref()); - hasher.finish() -} - -/// Returns current unix time in ms (compatible with JS Date.now()) -pub fn now() -> u64 { - use std::time::SystemTime; - - SystemTime::now() - .duration_since(SystemTime::UNIX_EPOCH) - .expect("System time must be configured to be post Unix Epoch start; qed") - .as_millis() as u64 -} diff --git a/backend/core/src/util/hash.rs b/backend/core/src/util/hash.rs deleted file mode 100644 index 727602d..0000000 --- a/backend/core/src/util/hash.rs +++ /dev/null @@ -1,89 +0,0 @@ -use std::fmt::{self, Debug, Display}; -use std::str::FromStr; - -use actix_web::error::ResponseError; -use serde::de::{self, Deserialize, Deserializer, Unexpected, Visitor}; - -const HASH_BYTES: usize = 32; - -/// Newtype wrapper for 32-byte hash values, implementing readable `Debug` and `serde::Deserialize`. -// We could use primitive_types::H256 here, but opted for a custom type to avoid more dependencies. -#[derive(Hash, PartialEq, Eq, Clone, Copy)] -pub struct Hash([u8; HASH_BYTES]); - -struct HashVisitor; - -impl<'de> Visitor<'de> for HashVisitor { - type Value = Hash; - - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.write_str("hexidecimal string of 32 bytes beginning with 0x") - } - - fn visit_str(self, value: &str) -> Result - where - E: de::Error, - { - value - .parse() - .map_err(|_| de::Error::invalid_value(Unexpected::Str(value), &self)) - } -} - -impl FromStr for Hash { - type Err = HashParseError; - - fn from_str(value: &str) -> Result { - if !value.starts_with("0x") { - return Err(HashParseError::InvalidPrefix); - } - - let mut hash = [0; HASH_BYTES]; - - hex::decode_to_slice(&value[2..], &mut hash).map_err(HashParseError::HexError)?; - - Ok(Hash(hash)) - } -} - -impl<'de> Deserialize<'de> for Hash { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - deserializer.deserialize_str(HashVisitor) - } -} - -impl Display for Hash { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.write_str("0x")?; - - let mut ascii = [0; HASH_BYTES * 2]; - - hex::encode_to_slice(self.0, &mut ascii) - .expect("Encoding 32 bytes into 64 bytes of ascii; qed"); - - f.write_str(std::str::from_utf8(&ascii).expect("ASCII hex encoded bytes canot fail; qed")) - } -} - -impl Debug for Hash { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - Display::fmt(self, f) - } -} - -#[derive(thiserror::Error, Debug)] -pub enum HashParseError { - HexError(hex::FromHexError), - InvalidPrefix, -} - -impl Display for HashParseError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - Debug::fmt(self, f) - } -} - -impl ResponseError for HashParseError {} diff --git a/backend/core/src/util/location.rs b/backend/core/src/util/location.rs deleted file mode 100644 index 777e0c1..0000000 --- a/backend/core/src/util/location.rs +++ /dev/null @@ -1,191 +0,0 @@ -use std::net::Ipv4Addr; -use std::sync::Arc; - -use actix::prelude::*; -use parking_lot::RwLock; -use rustc_hash::FxHashMap; -use serde::Deserialize; - -use crate::chain::{Chain, LocateNode}; -use crate::types::{NodeId, NodeLocation}; - -#[derive(Clone)] -pub struct Locator { - client: reqwest::blocking::Client, - cache: Arc>>>>, -} - -pub struct LocatorFactory { - cache: Arc>>>>, -} - -impl LocatorFactory { - pub fn new() -> Self { - let mut cache = FxHashMap::default(); - - // Default entry for localhost - cache.insert( - Ipv4Addr::new(127, 0, 0, 1), - Some(Arc::new(NodeLocation { - latitude: 52.516_6667, - longitude: 13.4, - city: "Berlin".into(), - })), - ); - - LocatorFactory { - cache: Arc::new(RwLock::new(cache)), - } - } - - pub fn create(&self) -> Locator { - Locator { - client: reqwest::blocking::Client::new(), - cache: self.cache.clone(), - } - } -} - -impl Actor for Locator { - type Context = SyncContext; -} - -#[derive(Message)] -#[rtype(result = "()")] -pub struct LocateRequest { - pub ip: Ipv4Addr, - pub nid: NodeId, - pub chain: Addr, -} - -#[derive(Deserialize)] -pub struct IPApiLocate { - city: Box, - loc: Box, -} - -impl IPApiLocate { - fn into_node_location(self) -> Option { - let IPApiLocate { city, loc } = self; - - let mut loc = loc.split(',').map(|n| n.parse()); - - let latitude = loc.next()?.ok()?; - let longitude = loc.next()?.ok()?; - - // Guarantee that the iterator has been exhausted - if loc.next().is_some() { - return None; - } - - Some(NodeLocation { - latitude, - longitude, - city, - }) - } -} - -impl Handler for Locator { - type Result = (); - - fn handle(&mut self, msg: LocateRequest, _: &mut Self::Context) { - let LocateRequest { ip, nid, chain } = msg; - - if let Some(item) = self.cache.read().get(&ip) { - if let Some(location) = item { - return chain.do_send(LocateNode { - nid, - location: location.clone(), - }); - } - - return; - } - - let location = match self.iplocate(ip) { - Ok(location) => location, - Err(err) => return log::debug!("GET error for ip location: {:?}", err), - }; - - self.cache.write().insert(ip, location.clone()); - - if let Some(location) = location { - chain.do_send(LocateNode { nid, location }); - } - } -} - -impl Locator { - fn iplocate(&self, ip: Ipv4Addr) -> Result>, reqwest::Error> { - let location = self.iplocate_ipapi_co(ip)?; - - match location { - Some(location) => Ok(Some(location)), - None => self.iplocate_ipinfo_io(ip), - } - } - - fn iplocate_ipapi_co(&self, ip: Ipv4Addr) -> Result>, reqwest::Error> { - let location = self - .query(&format!("https://ipapi.co/{}/json", ip))? - .map(Arc::new); - - Ok(location) - } - - fn iplocate_ipinfo_io( - &self, - ip: Ipv4Addr, - ) -> Result>, reqwest::Error> { - let location = self - .query(&format!("https://ipinfo.io/{}/json", ip))? - .and_then(|loc: IPApiLocate| loc.into_node_location().map(Arc::new)); - - Ok(location) - } - - fn query(&self, url: &str) -> Result, reqwest::Error> - where - for<'de> T: Deserialize<'de>, - { - match self.client.get(url).send()?.json::() { - Ok(result) => Ok(Some(result)), - Err(err) => { - log::debug!("JSON error for ip location: {:?}", err); - Ok(None) - } - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn ipapi_locate_to_node_location() { - let ipapi = IPApiLocate { - loc: "12.5,56.25".into(), - city: "Foobar".into(), - }; - - let location = ipapi.into_node_location().unwrap(); - - assert_eq!(location.latitude, 12.5); - assert_eq!(location.longitude, 56.25); - assert_eq!(&*location.city, "Foobar"); - } - - #[test] - fn ipapi_locate_to_node_location_too_many() { - let ipapi = IPApiLocate { - loc: "12.5,56.25,1.0".into(), - city: "Foobar".into(), - }; - - let location = ipapi.into_node_location(); - - assert!(location.is_none()); - } -} diff --git a/backend/docs/architecture.svg b/backend/docs/architecture.svg new file mode 100644 index 0000000..f82280e --- /dev/null +++ b/backend/docs/architecture.svg @@ -0,0 +1,3 @@ + + +
Telemetry Core
Telemetry Core
Feed 1
Feed 1
Feed 2
Feed 2
Feed 3
Feed 3
Feed N
Feed N
Node
Node
Node
Node
Node
Node
Node
Node

Nodes connect to the telemetry address and their connection is routed to the telemetry core process.

The core process stores chain-specific state, and routes messages to the relevant chain to update that state and send updates to feeds subscribed to that chain.

Feeds subscribe to chains and are routed to the relevant chain actor to receive updates from it.


Nodes connect to the telemetry address and...
JSON
JSON
JSON
(compact format)
JSON...
The current architecture
The current architecture
Shard 1
Shard 1
Shard 2
Shard 2
Shard 3
Shard 3
Shard N
Shard N
Telemetry Core
Telemetry Core
Feed 1
Feed 1
Feed 2
Feed 2
Feed 3
Feed 3
Feed N
Feed N
Node
Node
Node
Node
Node
Node
Node
Node
Load balancer
Load balancer
k8s
k8s

Nodes connect to the telemetry address and their connection is routed to one of the available shards. On connection, they send "system connected" messages for each set of node information they wish to send out and then start sending updates about that node.
Nodes connect to the telemetry address and...

One way or another, we'd like to distribute incoming connections across shards. Nodes will try to reconnect in a few seconds if they are disconnected.
One way or another, we'd like to distribut...

Shards receive JSON telemetry from nodes. Their main task is to deserialize it, and send along to the Telemetry Core only data that it cares about, ignoring the rest. It re-serializes to a more compact format (bincode currently) to try and minimise bandwidth to the core.
Shards receive JSON telemetry from nodes....

The core process keeps track of the state of connected nodes as information comes in, and broadcasts relevant updates out to feeds. If tells shards to "mute" messages from nodes when there are too many nodes on a chain, or a chain on our deny list.

If this core process dies and restarts, shards will force nodes to reconnect after reconnecting to the core, so that they send out their "system connected" message again.

If a shard connection is lost, the core removes all knowledge of nodes connected via that shard, and broadcasts relevant updates to feeds.
The core process keeps track of the state...

Feeds are browsers that have connected to the telemetry core. Feeds get sent basic information about the chains that exist, and then tell the core what chain they want to subscribe to. The core keeps track of this and sends chain-specific messages only to interested feeds.
Feeds are browsers that have connected to...
bincode
bincode
JSON
JSON
JSON
(compact format)
JSON...
The new architecture ("sharded")
The new architecture ("sharded")
Shard 1
Shard 1
Shard 2
Shard 2
Shard 3
Shard 3
Shard N
Shard N
Telemetry Core
Telemetry Core
Feed 1
Feed 1
Feed 2
Feed 2
Feed 3
Feed 3
Feed N
Feed N
Node
Node
Node
Node
Node
Node
Node
Node
Load balancer
Load balancer
k8s
k8s

Shards connect to multiple telemetry core processes and clone messages to each of them. Potentially, we could "bucket" things based on genesis hashes, but an easy first step is to send all messages to every core.

If any of the connected "core" processes go down, we force nodes to reconnect when it comes back up, to get new uptodate "system connected" messages.
Shards connect to multiple telemetry core...

Each telemetry core process would have a complete state of the world, and work in the same way that it does in the current sharded approach.
Each telemetry core process would have a c...

Because we have multiple "core" processes with a complete state of the world, we can load balance connected feeds across them.
Because we have multiple "core" processes...
bincode
bincode
JSON
JSON
JSON
(compact format)
JSON...
One possible future from this
One possible future from this
Telemetry Core
Telemetry Core
Telemetry Core
Telemetry Core
Load balancer
Load balancer

With shards, we remove the bottleneck on incoming node traffic. We could also "shard" the telemetry core process to help reduce the load from connected feeds if we find it is necessary (either by completely separate processes as this diagram shows, or multiple "loops" running inside a single telemetry core process to internally split feed connections across.)
With shards, we remove the bottleneck on incoming node traffic. We could also "shard" the tele...

With a single telemetry service, we have a bunch of data coming in to a single process, which pushed bandwidth limits and puts more pressure on the single process to handle deserialization and such. We can add shards to spread the load of handling the incoming message deserialization and bandwidth, and they can each strip away all unneeded info before passing it on to the core process.
With a single telemetry service, we have a bunch of data coming in to a single process, which...
JSON
(compact format)
JSON...
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/backend/telemetry_core/Cargo.toml b/backend/telemetry_core/Cargo.toml new file mode 100644 index 0000000..c751052 --- /dev/null +++ b/backend/telemetry_core/Cargo.toml @@ -0,0 +1,41 @@ +[package] +name = "telemetry_core" +version = "0.1.0" +authors = ["Parity Technologies Ltd. "] +edition = "2018" +license = "GPL-3.0" + +[dependencies] +anyhow = "1.0.41" +bimap = "0.6.1" +bincode = "1.3.3" +bytes = "1.0.1" +common = { path = "../common" } +futures = "0.3.15" +hex = "0.4.3" +http = "0.2.4" +log = "0.4.14" +once_cell = "1.8.0" +parking_lot = "0.11.1" +primitive-types = { version = "0.9.0", features = ["serde"] } +reqwest = { version = "0.11.4", features = ["json"] } +rustc-hash = "1.1.0" +serde = { version = "1.0.126", features = ["derive"] } +serde_json = "1.0.64" +simple_logger = "1.11.0" +smallvec = "1.6.1" +soketto = "0.6.0" +structopt = "0.3.21" +thiserror = "1.0.25" +tokio = { version = "1.7.0", features = ["full"] } +tokio-util = { version = "0.6", features = ["compat"] } +warp = "0.3.1" + +[dev-dependencies] +criterion = { version = "0.3.4", features = ["async", "async_tokio"] } +shellwords = "1.1.0" +test_utils = { path = "../test_utils" } + +[[bench]] +name = "throughput" +harness = false diff --git a/backend/telemetry_core/benches/throughput.rs b/backend/telemetry_core/benches/throughput.rs new file mode 100644 index 0000000..1b796a4 --- /dev/null +++ b/backend/telemetry_core/benches/throughput.rs @@ -0,0 +1,98 @@ +use std::iter::FromIterator; + +use futures::StreamExt; +use test_utils::workspace::start_server_release; +use criterion::{criterion_group, criterion_main, Criterion}; +use tokio::runtime::Runtime; +use serde_json::json; +use common::node_types::BlockHash; + +pub fn benchmark_throughput_single_shard(c: &mut Criterion) { + /* + let rt = Runtime::new().expect("tokio runtime should start"); + + // Setup our server and node/feed connections first: + let (nodes, feeds) = rt.block_on(async { + let mut server = start_server_release().await; + let shard_id = server.add_shard().await.unwrap(); + + // Connect 1000 nodes to the shard: + let mut nodes = server + .get_shard(shard_id) + .unwrap() + .connect_multiple(1000) + .await + .expect("nodes can connect"); + + // Every node announces itself on the same chain: + for (idx, (node_tx, _)) in nodes.iter_mut().enumerate() { + node_tx.send_json_text(json!({ + "id":1, // message ID, not node ID. Can be the same for all. + "ts":"2021-07-12T10:37:47.714666+01:00", + "payload": { + "authority":true, + "chain":"Local Testnet", + "config":"", + "genesis_hash": BlockHash::from_low_u64_ne(1), + "implementation":"Substrate Node", + "msg":"system.connected", + "name": format!("Alice {}", idx), + "network_id":"12D3KooWEyoppNCUx8Yx66oV9fJnriXwCcXwDDUA2kj6vnc6iDEp", + "startup_time":"1625565542717", + "version":"2.0.0-07a1af348-aarch64-macos" + } + })).await.unwrap(); + } +tokio::time::sleep(std::time::Duration::from_millis(500)).await; + // Start 1000 feeds: + let mut feeds = server + .get_core() + .connect_multiple(1) + .await + .expect("feeds can connect"); + + // // Subscribe all feeds to the chain: + // for (feed_tx, _) in feeds.iter_mut() { + // feed_tx.send_command("subscribe", "Local Testnet").await.unwrap(); + // } + +println!("consuming feed"); +{ + + let mut msgs = futures::stream::FuturesUnordered::from_iter( + feeds + .iter_mut() + .map(|(_,rx)| rx.recv_feed_messages()) + ); + + let mut n = 0; + while let Some(Ok(msg)) = msgs.next().await { + n += 1; + println!("Message {}: {:?}", n, msg); + } +} + + // // Consume any messages feeds have received so far (every feed should havea few at least): + // let feed_consumers = feeds + // .iter_mut() + // .map(|(_,rx)| rx.next()); + // futures::future::join_all(feed_consumers).await; +println!("feed consumed"); + (nodes, feeds) + }); + + // Next, run criterion using the same tokio runtime to benchmark time taken to send + // messages to nodes and receive them from feeds. + c.bench_function( + "throughput time", + |b| b.to_async(&rt).iter(|| async { + + // TODO: Actually implement the benchmark. + + }) + ); + */ +} + +criterion_group!(benches, benchmark_throughput_single_shard); +criterion_main!(benches); \ No newline at end of file diff --git a/backend/telemetry_core/src/aggregator/aggregator.rs b/backend/telemetry_core/src/aggregator/aggregator.rs new file mode 100644 index 0000000..7e9a4e1 --- /dev/null +++ b/backend/telemetry_core/src/aggregator/aggregator.rs @@ -0,0 +1,117 @@ +use super::inner_loop; +use crate::find_location::find_location; +use crate::state::NodeId; +use common::id_type; +use futures::channel::mpsc; +use futures::{future, Sink, SinkExt}; +use std::net::Ipv4Addr; +use std::sync::atomic::AtomicU64; +use std::sync::Arc; + +id_type! { + /// A unique Id is assigned per websocket connection (or more accurately, + /// per feed socket and per shard socket). This can be combined with the + /// [`LocalId`] of messages to give us a global ID. + pub struct ConnId(u64) +} + +#[derive(Clone)] +pub struct Aggregator(Arc); + +struct AggregatorInternal { + /// Shards that connect are each assigned a unique connection ID. + /// This helps us know who to send messages back to (especially in + /// conjunction with the [`LocalId`] that messages will come with). + shard_conn_id: AtomicU64, + /// Feeds that connect have their own unique connection ID, too. + feed_conn_id: AtomicU64, + /// Send messages in to the aggregator from the outside via this. This is + /// stored here so that anybody holding an `Aggregator` handle can + /// make use of it. + tx_to_aggregator: mpsc::UnboundedSender, +} + +impl Aggregator { + /// Spawn a new Aggregator. This connects to the telemetry backend + pub async fn spawn(denylist: Vec) -> anyhow::Result { + let (tx_to_aggregator, rx_from_external) = mpsc::unbounded(); + + // Kick off a locator task to locate nodes, which hands back a channel to make location requests + let tx_to_locator = find_location(tx_to_aggregator.clone().with(|(node_id, msg)| { + future::ok::<_, mpsc::SendError>(inner_loop::ToAggregator::FromFindLocation( + node_id, msg, + )) + })); + + // Handle any incoming messages in our handler loop: + tokio::spawn(Aggregator::handle_messages( + rx_from_external, + tx_to_locator, + denylist, + )); + + // Return a handle to our aggregator: + Ok(Aggregator(Arc::new(AggregatorInternal { + shard_conn_id: AtomicU64::new(1), + feed_conn_id: AtomicU64::new(1), + tx_to_aggregator, + }))) + } + + // This is spawned into a separate task and handles any messages coming + // in to the aggregator. If nobody is tolding the tx side of the channel + // any more, this task will gracefully end. + async fn handle_messages( + rx_from_external: mpsc::UnboundedReceiver, + tx_to_aggregator: mpsc::UnboundedSender<(NodeId, Ipv4Addr)>, + denylist: Vec, + ) { + inner_loop::InnerLoop::new(rx_from_external, tx_to_aggregator, denylist) + .handle() + .await; + } + + /// Return a sink that a shard can send messages into to be handled by the aggregator. + pub fn subscribe_shard( + &self, + ) -> impl Sink + Unpin { + // Assign a unique aggregator-local ID to each connection that subscribes, and pass + // that along with every message to the aggregator loop: + let shard_conn_id = self + .0 + .shard_conn_id + .fetch_add(1, std::sync::atomic::Ordering::Relaxed); + let tx_to_aggregator = self.0.tx_to_aggregator.clone(); + + // Calling `send` on this Sink requires Unpin. There may be a nicer way than this, + // but pinning by boxing is the easy solution for now: + Box::pin(tx_to_aggregator.with(move |msg| async move { + Ok(inner_loop::ToAggregator::FromShardWebsocket( + shard_conn_id.into(), + msg, + )) + })) + } + + /// Return a sink that a feed can send messages into to be handled by the aggregator. + pub fn subscribe_feed( + &self, + ) -> impl Sink + Unpin { + // Assign a unique aggregator-local ID to each connection that subscribes, and pass + // that along with every message to the aggregator loop: + let feed_conn_id = self + .0 + .feed_conn_id + .fetch_add(1, std::sync::atomic::Ordering::Relaxed); + let tx_to_aggregator = self.0.tx_to_aggregator.clone(); + + // Calling `send` on this Sink requires Unpin. There may be a nicer way than this, + // but pinning by boxing is the easy solution for now: + Box::pin(tx_to_aggregator.with(move |msg| async move { + Ok(inner_loop::ToAggregator::FromFeedWebsocket( + feed_conn_id.into(), + msg, + )) + })) + } +} diff --git a/backend/telemetry_core/src/aggregator/inner_loop.rs b/backend/telemetry_core/src/aggregator/inner_loop.rs new file mode 100644 index 0000000..da20e2a --- /dev/null +++ b/backend/telemetry_core/src/aggregator/inner_loop.rs @@ -0,0 +1,611 @@ +use super::aggregator::ConnId; +use crate::feed_message::{self, FeedMessageSerializer}; +use crate::find_location; +use crate::state::{self, NodeId, State}; +use bimap::BiMap; +use common::{ + internal_messages::{self, MuteReason, ShardNodeId}, + node_message, + node_types::BlockHash, + time, +}; +use futures::channel::mpsc; +use futures::StreamExt; +use std::collections::{HashMap, HashSet}; +use std::{ + net::{IpAddr, Ipv4Addr}, + str::FromStr, +}; + +/// Incoming messages come via subscriptions, and end up looking like this. +#[derive(Clone, Debug)] +pub enum ToAggregator { + FromShardWebsocket(ConnId, FromShardWebsocket), + FromFeedWebsocket(ConnId, FromFeedWebsocket), + FromFindLocation(NodeId, find_location::Location), +} + +/// An incoming shard connection can send these messages to the aggregator. +#[derive(Clone, Debug)] +pub enum FromShardWebsocket { + /// When the socket is opened, it'll send this first + /// so that we have a way to communicate back to it. + Initialize { + channel: mpsc::UnboundedSender, + }, + /// Tell the aggregator about a new node. + Add { + local_id: ShardNodeId, + ip: Option, + node: common::node_types::NodeDetails, + genesis_hash: common::node_types::BlockHash, + }, + /// Update/pass through details about a node. + Update { + local_id: ShardNodeId, + payload: node_message::Payload, + }, + /// Tell the aggregator that a node has been removed when it disconnects. + Remove { local_id: ShardNodeId }, + /// The shard is disconnected. + Disconnected, +} + +/// The aggregator can these messages back to a shard connection. +#[derive(Debug)] +pub enum ToShardWebsocket { + /// Mute messages to the core by passing the shard-local ID of them. + Mute { + local_id: ShardNodeId, + reason: internal_messages::MuteReason, + }, +} + +/// An incoming feed connection can send these messages to the aggregator. +#[derive(Clone, Debug)] +pub enum FromFeedWebsocket { + /// When the socket is opened, it'll send this first + /// so that we have a way to communicate back to it. + /// Unbounded so that slow feeds don't block aggregato + /// progress. + Initialize { + channel: mpsc::UnboundedSender, + }, + /// The feed can subscribe to a chain to receive + /// messages relating to it. + Subscribe { chain: Box }, + /// The feed wants finality info for the chain, too. + SendFinality, + /// The feed doesn't want any more finality info for the chain. + NoMoreFinality, + /// An explicit ping message. + Ping { value: Box }, + /// The feed is disconnected. + Disconnected, +} + +// The frontend sends text based commands; parse them into these messages: +impl FromStr for FromFeedWebsocket { + type Err = anyhow::Error; + fn from_str(s: &str) -> Result { + let (cmd, value) = match s.find(':') { + Some(idx) => (&s[..idx], s[idx + 1..].into()), + None => return Err(anyhow::anyhow!("Expecting format `CMD:CHAIN_NAME`")), + }; + match cmd { + "ping" => Ok(FromFeedWebsocket::Ping { value }), + "subscribe" => Ok(FromFeedWebsocket::Subscribe { chain: value }), + "send-finality" => Ok(FromFeedWebsocket::SendFinality), + "no-more-finality" => Ok(FromFeedWebsocket::NoMoreFinality), + _ => return Err(anyhow::anyhow!("Command {} not recognised", cmd)), + } + } +} + +/// The aggregator can these messages back to a feed connection. +#[derive(Clone, Debug)] +pub enum ToFeedWebsocket { + Bytes(bytes::Bytes), +} + +/// Instances of this are responsible for handling incoming and +/// outgoing messages in the main aggregator loop. +pub struct InnerLoop { + /// Messages from the outside world come into this: + rx_from_external: mpsc::UnboundedReceiver, + + /// The state of our chains and nodes lives here: + node_state: State, + /// We maintain a mapping between NodeId and ConnId+LocalId, so that we know + /// which messages are about which nodes. + node_ids: BiMap, + + /// Keep track of how to send messages out to feeds. + feed_channels: HashMap>, + /// Keep track of how to send messages out to shards. + shard_channels: HashMap>, + + /// Which chain is a feed subscribed to? + /// Feed Connection ID -> Chain Genesis Hash + feed_conn_id_to_chain: HashMap, + /// Which feeds are subscribed to a given chain (needs to stay in sync with above)? + /// Chain Genesis Hash -> Feed Connection IDs + chain_to_feed_conn_ids: HashMap>, + + /// These feeds want finality info, too. + feed_conn_id_finality: HashSet, + + /// Send messages here to make geographical location requests. + tx_to_locator: mpsc::UnboundedSender<(NodeId, Ipv4Addr)>, +} + +impl InnerLoop { + /// Create a new inner loop handler with the various state it needs. + pub fn new( + rx_from_external: mpsc::UnboundedReceiver, + tx_to_locator: mpsc::UnboundedSender<(NodeId, Ipv4Addr)>, + denylist: Vec, + ) -> Self { + InnerLoop { + rx_from_external, + node_state: State::new(denylist), + node_ids: BiMap::new(), + feed_channels: HashMap::new(), + shard_channels: HashMap::new(), + feed_conn_id_to_chain: HashMap::new(), + chain_to_feed_conn_ids: HashMap::new(), + feed_conn_id_finality: HashSet::new(), + tx_to_locator, + } + } + + /// Start handling and responding to incoming messages. Owing to unbounded channels, we actually + /// only have a single `.await` (in this function). This helps to make it clear that the aggregator loop + /// will be able to make progress quickly without any potential yield points. + pub async fn handle(mut self) { + while let Some(msg) = self.rx_from_external.next().await { + match msg { + ToAggregator::FromFeedWebsocket(feed_conn_id, msg) => { + self.handle_from_feed(feed_conn_id, msg) + } + ToAggregator::FromShardWebsocket(shard_conn_id, msg) => { + self.handle_from_shard(shard_conn_id, msg) + } + ToAggregator::FromFindLocation(node_id, location) => { + self.handle_from_find_location(node_id, location) + } + } + } + } + + /// Handle messages that come from the node geographical locator. + fn handle_from_find_location( + &mut self, + node_id: NodeId, + location: find_location::Location, + ) { + self.node_state + .update_node_location(node_id, location.clone()); + + if let Some(loc) = location { + let mut feed_message_serializer = FeedMessageSerializer::new(); + feed_message_serializer.push(feed_message::LocatedNode( + node_id.get_chain_node_id().into(), + loc.latitude, + loc.longitude, + &loc.city, + )); + + let chain_genesis_hash = self + .node_state + .get_chain_by_node_id(node_id) + .map(|chain| *chain.genesis_hash()); + + if let Some(chain_genesis_hash) = chain_genesis_hash { + self.finalize_and_broadcast_to_chain_feeds( + &chain_genesis_hash, + feed_message_serializer, + ); + } + } + } + + /// Handle messages coming from shards. + fn handle_from_shard(&mut self, shard_conn_id: ConnId, msg: FromShardWebsocket) { + log::debug!("Message from shard ({:?}): {:?}", shard_conn_id, msg); + + match msg { + FromShardWebsocket::Initialize { channel } => { + self.shard_channels.insert(shard_conn_id, channel); + } + FromShardWebsocket::Add { + local_id, + ip, + node, + genesis_hash, + } => { + match self.node_state.add_node(genesis_hash, node) { + state::AddNodeResult::ChainOnDenyList => { + if let Some(shard_conn) = self.shard_channels.get_mut(&shard_conn_id) { + let _ = shard_conn + .unbounded_send(ToShardWebsocket::Mute { + local_id, + reason: MuteReason::ChainNotAllowed, + }); + } + } + state::AddNodeResult::ChainOverQuota => { + if let Some(shard_conn) = self.shard_channels.get_mut(&shard_conn_id) { + let _ = shard_conn + .unbounded_send(ToShardWebsocket::Mute { + local_id, + reason: MuteReason::Overquota, + }); + } + } + state::AddNodeResult::NodeAddedToChain(details) => { + let node_id = details.id; + + // Record ID <-> (shardId,localId) for future messages: + self.node_ids.insert(node_id, (shard_conn_id, local_id)); + + // Don't hold onto details too long because we want &mut self later: + let old_chain_label = details.old_chain_label.to_owned(); + let new_chain_label = details.new_chain_label.to_owned(); + let chain_node_count = details.chain_node_count; + let has_chain_label_changed = details.has_chain_label_changed; + + // Tell chain subscribers about the node we've just added: + let mut feed_messages_for_chain = FeedMessageSerializer::new(); + feed_messages_for_chain.push(feed_message::AddedNode( + node_id.get_chain_node_id().into(), + &details.node, + )); + self.finalize_and_broadcast_to_chain_feeds( + &genesis_hash, + feed_messages_for_chain, + ); + // Tell everybody about the new node count and potential rename: + let mut feed_messages_for_all = FeedMessageSerializer::new(); + if has_chain_label_changed { + feed_messages_for_all + .push(feed_message::RemovedChain(&old_chain_label)); + } + feed_messages_for_all + .push(feed_message::AddedChain(&new_chain_label, chain_node_count)); + self.finalize_and_broadcast_to_all_feeds(feed_messages_for_all); + + // Ask for the grographical location of the node. + // Currently we only geographically locate IPV4 addresses so ignore IPV6. + if let Some(IpAddr::V4(ip_v4)) = ip { + let _ = self.tx_to_locator.unbounded_send((node_id, ip_v4)); + } + } + } + } + FromShardWebsocket::Remove { local_id } => { + let node_id = match self.node_ids.remove_by_right(&(shard_conn_id, local_id)) { + Some((node_id, _)) => node_id, + None => { + log::error!( + "Cannot find ID for node with shard/connectionId of {:?}/{:?}", + shard_conn_id, + local_id + ); + return; + } + }; + self.remove_nodes_and_broadcast_result(Some(node_id)); + } + FromShardWebsocket::Update { local_id, payload } => { + let node_id = match self.node_ids.get_by_right(&(shard_conn_id, local_id)) { + Some(id) => *id, + None => { + log::error!( + "Cannot find ID for node with shard/connectionId of {:?}/{:?}", + shard_conn_id, + local_id + ); + return; + } + }; + + let mut feed_message_serializer = FeedMessageSerializer::new(); + let broadcast_finality = + self.node_state + .update_node(node_id, payload, &mut feed_message_serializer); + + if let Some(chain) = self.node_state.get_chain_by_node_id(node_id) { + let genesis_hash = *chain.genesis_hash(); + if broadcast_finality { + self.finalize_and_broadcast_to_chain_finality_feeds( + &genesis_hash, + feed_message_serializer, + ); + } else { + self.finalize_and_broadcast_to_chain_feeds( + &genesis_hash, + feed_message_serializer, + ); + } + } + } + FromShardWebsocket::Disconnected => { + // Find all nodes associated with this shard connection ID: + let node_ids_to_remove: Vec = self + .node_ids + .iter() + .filter(|(_, &(this_shard_conn_id, _))| shard_conn_id == this_shard_conn_id) + .map(|(&node_id, _)| node_id) + .collect(); + + // ... and remove them: + self.remove_nodes_and_broadcast_result(node_ids_to_remove); + } + } + } + + /// Handle messages coming from feeds. + fn handle_from_feed(&mut self, feed_conn_id: ConnId, msg: FromFeedWebsocket) { + log::debug!("Message from feed ({:?}): {:?}", feed_conn_id, msg); + match msg { + FromFeedWebsocket::Initialize { channel } => { + self.feed_channels.insert(feed_conn_id, channel.clone()); + + // Tell the new feed subscription some basic things to get it going: + let mut feed_serializer = FeedMessageSerializer::new(); + feed_serializer.push(feed_message::Version(31)); + for chain in self.node_state.iter_chains() { + feed_serializer + .push(feed_message::AddedChain(chain.label(), chain.node_count())); + } + + // Send this to the channel that subscribed: + if let Some(bytes) = feed_serializer.into_finalized() { + let _ = channel.unbounded_send(ToFeedWebsocket::Bytes(bytes)); + } + } + FromFeedWebsocket::Ping { value } => { + let feed_channel = match self.feed_channels.get_mut(&feed_conn_id) { + Some(chan) => chan, + None => return, + }; + + // Pong! + let mut feed_serializer = FeedMessageSerializer::new(); + feed_serializer.push(feed_message::Pong(&value)); + if let Some(bytes) = feed_serializer.into_finalized() { + let _ = feed_channel.unbounded_send(ToFeedWebsocket::Bytes(bytes)); + } + } + FromFeedWebsocket::Subscribe { chain } => { + let feed_channel = match self.feed_channels.get_mut(&feed_conn_id) { + Some(chan) => chan, + None => return, + }; + + // Unsubscribe from previous chain if subscribed to one: + let old_genesis_hash = self.feed_conn_id_to_chain.remove(&feed_conn_id); + if let Some(old_genesis_hash) = &old_genesis_hash { + if let Some(map) = self.chain_to_feed_conn_ids.get_mut(old_genesis_hash) { + map.remove(&feed_conn_id); + } + } + + // Untoggle request for finality feeds: + self.feed_conn_id_finality.remove(&feed_conn_id); + + // Get old chain if there was one: + let node_state = &self.node_state; + let old_chain = + old_genesis_hash.and_then(|hash| node_state.get_chain_by_genesis_hash(&hash)); + + // Get new chain, ignoring the rest if it doesn't exist. + let new_chain = match self.node_state.get_chain_by_label(&chain) { + Some(chain) => chain, + None => return, + }; + + // Send messages to the feed about this subscription: + let mut feed_serializer = FeedMessageSerializer::new(); + if let Some(old_chain) = old_chain { + feed_serializer.push(feed_message::UnsubscribedFrom(old_chain.label())); + } + feed_serializer.push(feed_message::SubscribedTo(new_chain.label())); + feed_serializer.push(feed_message::TimeSync(time::now())); + feed_serializer.push(feed_message::BestBlock( + new_chain.best_block().height, + new_chain.timestamp(), + new_chain.average_block_time(), + )); + feed_serializer.push(feed_message::BestFinalized( + new_chain.finalized_block().height, + new_chain.finalized_block().hash, + )); + for (idx, (chain_node_id, node)) in new_chain.iter_nodes().enumerate() { + let chain_node_id = chain_node_id.into(); + + // Send subscription confirmation and chain head before doing all the nodes, + // and continue sending batches of 32 nodes a time over the wire subsequently + if idx % 32 == 0 { + if let Some(bytes) = feed_serializer.finalize() { + let _ = feed_channel.unbounded_send(ToFeedWebsocket::Bytes(bytes)); + } + } + feed_serializer.push(feed_message::AddedNode(chain_node_id, node)); + feed_serializer.push(feed_message::FinalizedBlock( + chain_node_id, + node.finalized().height, + node.finalized().hash, + )); + if node.stale() { + feed_serializer.push(feed_message::StaleNode(chain_node_id)); + } + } + if let Some(bytes) = feed_serializer.into_finalized() { + let _ = feed_channel.unbounded_send(ToFeedWebsocket::Bytes(bytes)); + } + + // Actually make a note of the new chain subsciption: + let new_genesis_hash = *new_chain.genesis_hash(); + self.feed_conn_id_to_chain + .insert(feed_conn_id, new_genesis_hash); + self.chain_to_feed_conn_ids + .entry(new_genesis_hash) + .or_default() + .insert(feed_conn_id); + } + FromFeedWebsocket::SendFinality => { + self.feed_conn_id_finality.insert(feed_conn_id); + } + FromFeedWebsocket::NoMoreFinality => { + self.feed_conn_id_finality.remove(&feed_conn_id); + } + FromFeedWebsocket::Disconnected => { + // The feed has disconnected; clean up references to it: + if let Some(chain) = self.feed_conn_id_to_chain.remove(&feed_conn_id) { + self.chain_to_feed_conn_ids.remove(&chain); + } + self.feed_channels.remove(&feed_conn_id); + self.feed_conn_id_finality.remove(&feed_conn_id); + } + } + } + + /// Remove all of the node IDs provided and broadcast messages to feeds as needed. + fn remove_nodes_and_broadcast_result( + &mut self, + node_ids: impl IntoIterator, + ) { + // Group by chain to simplify the handling of feed messages: + let mut node_ids_per_chain: HashMap> = HashMap::new(); + for node_id in node_ids.into_iter() { + if let Some(chain) = self.node_state.get_chain_by_node_id(node_id) { + node_ids_per_chain + .entry(*chain.genesis_hash()) + .or_default() + .push(node_id); + } + } + + // Remove the nodes for each chain + let mut feed_messages_for_all = FeedMessageSerializer::new(); + for (chain_label, node_ids) in node_ids_per_chain { + let mut feed_messages_for_chain = FeedMessageSerializer::new(); + for node_id in node_ids { + self.remove_node( + node_id, + &mut feed_messages_for_chain, + &mut feed_messages_for_all, + ); + } + self.finalize_and_broadcast_to_chain_feeds(&chain_label, feed_messages_for_chain); + } + self.finalize_and_broadcast_to_all_feeds(feed_messages_for_all); + } + + /// Remove a single node by its ID, pushing any messages we'd want to send + /// out to feeds onto the provided feed serializers. Doesn't actually send + /// anything to the feeds; just updates state as needed. + fn remove_node( + &mut self, + node_id: NodeId, + feed_for_chain: &mut FeedMessageSerializer, + feed_for_all: &mut FeedMessageSerializer, + ) { + // Remove our top level association (this may already have been done). + self.node_ids.remove_by_left(&node_id); + + let removed_details = match self.node_state.remove_node(node_id) { + Some(remove_details) => remove_details, + None => { + log::error!("Could not find node {:?}", node_id); + return; + } + }; + + // The chain has been removed (no nodes left in it, or it was renamed): + if removed_details.chain_node_count == 0 || removed_details.has_chain_label_changed { + feed_for_all.push(feed_message::RemovedChain(&removed_details.old_chain_label)); + } + + // If the chain still exists, tell everybody about the new label or updated node count: + if removed_details.chain_node_count != 0 { + feed_for_all.push(feed_message::AddedChain( + &removed_details.new_chain_label, + removed_details.chain_node_count, + )); + } + + // Assuming the chain hasn't gone away, tell chain subscribers about the node removal + if removed_details.chain_node_count != 0 { + feed_for_chain.push(feed_message::RemovedNode( + node_id.get_chain_node_id().into(), + )); + } + } + + /// Finalize a [`FeedMessageSerializer`] and broadcast the result to feeds for the chain. + fn finalize_and_broadcast_to_chain_feeds( + &mut self, + genesis_hash: &BlockHash, + serializer: FeedMessageSerializer, + ) { + if let Some(bytes) = serializer.into_finalized() { + self.broadcast_to_chain_feeds(genesis_hash, ToFeedWebsocket::Bytes(bytes)); + } + } + + /// Send a message to all chain feeds. + fn broadcast_to_chain_feeds(&mut self, genesis_hash: &BlockHash, message: ToFeedWebsocket) { + if let Some(feeds) = self.chain_to_feed_conn_ids.get(genesis_hash) { + for &feed_id in feeds { + if let Some(chan) = self.feed_channels.get_mut(&feed_id) { + let _ = chan.unbounded_send(message.clone()); + } + } + } + } + + /// Finalize a [`FeedMessageSerializer`] and broadcast the result to all feeds + fn finalize_and_broadcast_to_all_feeds(&mut self, serializer: FeedMessageSerializer) { + if let Some(bytes) = serializer.into_finalized() { + self.broadcast_to_all_feeds(ToFeedWebsocket::Bytes(bytes)); + } + } + + /// Send a message to everybody. + fn broadcast_to_all_feeds(&mut self, message: ToFeedWebsocket) { + for chan in self.feed_channels.values_mut() { + let _ = chan.unbounded_send(message.clone()); + } + } + + /// Finalize a [`FeedMessageSerializer`] and broadcast the result to chain finality feeds + fn finalize_and_broadcast_to_chain_finality_feeds( + &mut self, + genesis_hash: &BlockHash, + serializer: FeedMessageSerializer, + ) { + if let Some(bytes) = serializer.into_finalized() { + self.broadcast_to_chain_finality_feeds(genesis_hash, ToFeedWebsocket::Bytes(bytes)); + } + } + + /// Send a message to all chain finality feeds. + fn broadcast_to_chain_finality_feeds( + &mut self, + genesis_hash: &BlockHash, + message: ToFeedWebsocket, + ) { + if let Some(feeds) = self.chain_to_feed_conn_ids.get(genesis_hash) { + // Get all feeds for the chain, but only broadcast to those feeds that + // are also subscribed to receive finality updates. + for &feed_id in feeds.union(&self.feed_conn_id_finality) { + if let Some(chan) = self.feed_channels.get_mut(&feed_id) { + let _ = chan.unbounded_send(message.clone()); + } + } + } + } +} diff --git a/backend/telemetry_core/src/aggregator/mod.rs b/backend/telemetry_core/src/aggregator/mod.rs new file mode 100644 index 0000000..c73fba3 --- /dev/null +++ b/backend/telemetry_core/src/aggregator/mod.rs @@ -0,0 +1,7 @@ +mod aggregator; +mod inner_loop; + +// Expose the various message types that can be worked with externally: +pub use inner_loop::{FromFeedWebsocket, FromShardWebsocket, ToFeedWebsocket, ToShardWebsocket}; + +pub use aggregator::*; diff --git a/backend/telemetry_core/src/feed_message.rs b/backend/telemetry_core/src/feed_message.rs new file mode 100644 index 0000000..e0e108f --- /dev/null +++ b/backend/telemetry_core/src/feed_message.rs @@ -0,0 +1,234 @@ +//! This module provides a way of encoding the various messages that we'll +//! send to subscribed feeds (browsers). + +use serde::Serialize; +use std::mem; + +use crate::state::Node; +use common::node_types::{ + BlockDetails, BlockHash, BlockNumber, NodeHardware, NodeIO, NodeStats, Timestamp, +}; +use serde_json::to_writer; + +type Address = Box; +type FeedNodeId = usize; + +pub trait FeedMessage { + const ACTION: u8; +} + +pub trait FeedMessageWrite: FeedMessage { + fn write_to_feed(&self, ser: &mut FeedMessageSerializer); +} + +impl FeedMessageWrite for T +where + T: FeedMessage + Serialize, +{ + fn write_to_feed(&self, ser: &mut FeedMessageSerializer) { + ser.write(self) + } +} + +pub struct FeedMessageSerializer { + /// Current buffer, + buffer: Vec, +} + +const BUFCAP: usize = 128; + +impl FeedMessageSerializer { + pub fn new() -> Self { + Self { + buffer: Vec::with_capacity(BUFCAP), + } + } + + pub fn push(&mut self, msg: Message) + where + Message: FeedMessageWrite, + { + let glue = match self.buffer.len() { + 0 => b'[', + _ => b',', + }; + + self.buffer.push(glue); + self.write(&Message::ACTION); + self.buffer.push(b','); + msg.write_to_feed(self); + } + + fn write(&mut self, value: &S) + where + S: Serialize, + { + let _ = to_writer(&mut self.buffer, value); + } + + /// Return the bytes we've serialized so far and prepare a new buffer. If you're + /// finished serializing data, prefer [`FeedMessageSerializer::into_finalized`] + pub fn finalize(&mut self) -> Option { + if self.buffer.is_empty() { + return None; + } + + self.buffer.push(b']'); + + let bytes = mem::replace(&mut self.buffer, Vec::with_capacity(BUFCAP)); + + Some(bytes.into()) + } + + /// Return the bytes that we've serialized so far, consuming the serializer. + pub fn into_finalized(mut self) -> Option { + if self.buffer.is_empty() { + return None; + } + + self.buffer.push(b']'); + Some(self.buffer.into()) + } +} + +macro_rules! actions { + ($($action:literal: $t:ty,)*) => { + $( + impl FeedMessage for $t { + const ACTION: u8 = $action; + } + )* + } +} + +actions! { + 0: Version, + 1: BestBlock, + 2: BestFinalized, + 3: AddedNode<'_>, + 4: RemovedNode, + 5: LocatedNode<'_>, + 6: ImportedBlock<'_>, + 7: FinalizedBlock, + 8: NodeStatsUpdate<'_>, + 9: Hardware<'_>, + 10: TimeSync, + 11: AddedChain<'_>, + 12: RemovedChain<'_>, + 13: SubscribedTo<'_>, + 14: UnsubscribedFrom<'_>, + 15: Pong<'_>, + 16: AfgFinalized, + 17: AfgReceivedPrevote, + 18: AfgReceivedPrecommit, + 19: AfgAuthoritySet, + 20: StaleNode, + 21: NodeIOUpdate<'_>, +} + +#[derive(Serialize)] +pub struct Version(pub usize); + +#[derive(Serialize)] +pub struct BestBlock(pub BlockNumber, pub Timestamp, pub Option); + +#[derive(Serialize)] +pub struct BestFinalized(pub BlockNumber, pub BlockHash); + +pub struct AddedNode<'a>(pub FeedNodeId, pub &'a Node); + +#[derive(Serialize)] +pub struct RemovedNode(pub FeedNodeId); + +#[derive(Serialize)] +pub struct LocatedNode<'a>(pub FeedNodeId, pub f32, pub f32, pub &'a str); + +#[derive(Serialize)] +pub struct ImportedBlock<'a>(pub FeedNodeId, pub &'a BlockDetails); + +#[derive(Serialize)] +pub struct FinalizedBlock(pub FeedNodeId, pub BlockNumber, pub BlockHash); + +#[derive(Serialize)] +pub struct NodeStatsUpdate<'a>(pub FeedNodeId, pub &'a NodeStats); + +#[derive(Serialize)] +pub struct NodeIOUpdate<'a>(pub FeedNodeId, pub &'a NodeIO); + +#[derive(Serialize)] +pub struct Hardware<'a>(pub FeedNodeId, pub &'a NodeHardware); + +#[derive(Serialize)] +pub struct TimeSync(pub u64); + +#[derive(Serialize)] +pub struct AddedChain<'a>(pub &'a str, pub usize); + +#[derive(Serialize)] +pub struct RemovedChain<'a>(pub &'a str); + +#[derive(Serialize)] +pub struct SubscribedTo<'a>(pub &'a str); + +#[derive(Serialize)] +pub struct UnsubscribedFrom<'a>(pub &'a str); + +#[derive(Serialize)] +pub struct Pong<'a>(pub &'a str); + +#[derive(Serialize)] +pub struct AfgFinalized(pub Address, pub BlockNumber, pub BlockHash); + +#[derive(Serialize)] +pub struct AfgReceivedPrevote( + pub Address, + pub BlockNumber, + pub BlockHash, + pub Option
, +); + +#[derive(Serialize)] +pub struct AfgReceivedPrecommit( + pub Address, + pub BlockNumber, + pub BlockHash, + pub Option
, +); + +#[derive(Serialize)] +pub struct AfgAuthoritySet( + pub Address, + pub Address, + pub Address, + pub BlockNumber, + pub BlockHash, +); + +#[derive(Serialize)] +pub struct StaleNode(pub FeedNodeId); + +impl FeedMessageWrite for AddedNode<'_> { + fn write_to_feed(&self, ser: &mut FeedMessageSerializer) { + let AddedNode(nid, node) = self; + + let details = node.details(); + let details = ( + &details.name, + &details.implementation, + &details.version, + &details.validator, + &details.network_id, + ); + + ser.write(&( + nid, + details, + node.stats(), + node.io(), + node.hardware(), + node.block_details(), + &node.location(), + &node.startup_time(), + )); + } +} diff --git a/backend/telemetry_core/src/find_location.rs b/backend/telemetry_core/src/find_location.rs new file mode 100644 index 0000000..e2aa661 --- /dev/null +++ b/backend/telemetry_core/src/find_location.rs @@ -0,0 +1,211 @@ +use std::net::Ipv4Addr; +use std::sync::Arc; + +use futures::channel::mpsc; +use futures::{Sink, SinkExt, StreamExt}; +use parking_lot::RwLock; +use rustc_hash::FxHashMap; +use serde::Deserialize; + +use common::node_types::NodeLocation; +use tokio::sync::Semaphore; + +/// The returned location is optional; it may be None if not found. +pub type Location = Option>; + +/// This is responsible for taking an IP address and attempting +/// to find a geographical location from this +pub fn find_location(response_chan: R) -> mpsc::UnboundedSender<(Id, Ipv4Addr)> +where + R: Sink<(Id, Option>)> + Unpin + Send + Clone + 'static, + Id: Clone + Send + 'static, +{ + let (tx, mut rx) = mpsc::unbounded(); + + // cache entries + let mut cache: FxHashMap>> = FxHashMap::default(); + + // Default entry for localhost + cache.insert( + Ipv4Addr::new(127, 0, 0, 1), + Some(Arc::new(NodeLocation { + latitude: 52.516_6667, + longitude: 13.4, + city: "Berlin".into(), + })), + ); + + // Create a locator with our cache. This is used to obtain locations. + let locator = Locator::new(cache); + + // Spawn a loop to handle location requests + tokio::spawn(async move { + // Allow 4 requests at a time. acquiring a token will block while the + // number of concurrent location requests is more than this. + let semaphore = Arc::new(Semaphore::new(4)); + + loop { + while let Some((id, ip_address)) = rx.next().await { + let permit = semaphore.clone().acquire_owned().await.unwrap(); + let mut response_chan = response_chan.clone(); + let locator = locator.clone(); + + // Once we have acquired our permit, spawn a task to avoid + // blocking this loop so that we can handle concurrent requests. + tokio::spawn(async move { + match locator.locate(ip_address).await { + Ok(loc) => { + let _ = response_chan.send((id, loc)).await; + } + Err(e) => { + log::debug!("GET error for ip location: {:?}", e); + } + }; + + // ensure permit is moved into task by dropping it explicitly: + drop(permit); + }); + } + } + }); + + tx +} + +/// This struct can be used to make location requests, given +/// an IPV4 address. +#[derive(Clone)] +struct Locator { + client: reqwest::Client, + cache: Arc>>>>, +} + +impl Locator { + pub fn new(cache: FxHashMap>>) -> Self { + let client = reqwest::Client::new(); + + Locator { + client, + cache: Arc::new(RwLock::new(cache)), + } + } + + pub async fn locate(&self, ip: Ipv4Addr) -> Result>, reqwest::Error> { + // Return location quickly if it's cached: + let cached_loc = { + let cache_reader = self.cache.read(); + cache_reader.get(&ip).map(|o| o.clone()) + }; + if let Some(loc) = cached_loc { + return Ok(loc); + } + + // Look it up via the location services if not cached: + let location = self.iplocate_ipapi_co(ip).await?; + let location = match location { + Some(location) => Ok(Some(location)), + None => self.iplocate_ipinfo_io(ip).await, + }?; + + self.cache.write().insert(ip, location.clone()); + Ok(location) + } + + async fn iplocate_ipapi_co( + &self, + ip: Ipv4Addr, + ) -> Result>, reqwest::Error> { + let location = self + .query(&format!("https://ipapi.co/{}/json", ip)) + .await? + .map(Arc::new); + + Ok(location) + } + + async fn iplocate_ipinfo_io( + &self, + ip: Ipv4Addr, + ) -> Result>, reqwest::Error> { + let location = self + .query(&format!("https://ipinfo.io/{}/json", ip)) + .await? + .and_then(|loc: IPApiLocate| loc.into_node_location().map(Arc::new)); + + Ok(location) + } + + async fn query(&self, url: &str) -> Result, reqwest::Error> + where + for<'de> T: Deserialize<'de>, + { + match self.client.get(url).send().await?.json::().await { + Ok(result) => Ok(Some(result)), + Err(err) => { + log::debug!("JSON error for ip location: {:?}", err); + Ok(None) + } + } + } +} + +/// This is the format returned from ipinfo.co, so we do +/// a little conversion to get it into the shape we want. +#[derive(Deserialize)] +struct IPApiLocate { + city: Box, + loc: Box, +} + +impl IPApiLocate { + fn into_node_location(self) -> Option { + let IPApiLocate { city, loc } = self; + + let mut loc = loc.split(',').map(|n| n.parse()); + + let latitude = loc.next()?.ok()?; + let longitude = loc.next()?.ok()?; + + // Guarantee that the iterator has been exhausted + if loc.next().is_some() { + return None; + } + + Some(NodeLocation { + latitude, + longitude, + city, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn ipapi_locate_to_node_location() { + let ipapi = IPApiLocate { + loc: "12.5,56.25".into(), + city: "Foobar".into(), + }; + + let location = ipapi.into_node_location().unwrap(); + + assert_eq!(location.latitude, 12.5); + assert_eq!(location.longitude, 56.25); + assert_eq!(&*location.city, "Foobar"); + } + + #[test] + fn ipapi_locate_to_node_location_too_many() { + let ipapi = IPApiLocate { + loc: "12.5,56.25,1.0".into(), + city: "Foobar".into(), + }; + + let location = ipapi.into_node_location(); + + assert!(location.is_none()); + } +} diff --git a/backend/telemetry_core/src/main.rs b/backend/telemetry_core/src/main.rs new file mode 100644 index 0000000..6f1d00e --- /dev/null +++ b/backend/telemetry_core/src/main.rs @@ -0,0 +1,340 @@ +mod aggregator; +mod feed_message; +mod find_location; +mod state; + +use std::net::SocketAddr; +use std::str::FromStr; + +use aggregator::{ + Aggregator, FromFeedWebsocket, FromShardWebsocket, ToFeedWebsocket, ToShardWebsocket, +}; +use bincode::Options; +use common::internal_messages; +use common::ready_chunks_all::ReadyChunksAll; +use futures::{channel::mpsc, SinkExt, StreamExt}; +use simple_logger::SimpleLogger; +use structopt::StructOpt; +use warp::filters::ws; +use warp::Filter; + +const VERSION: &str = env!("CARGO_PKG_VERSION"); +const AUTHORS: &str = env!("CARGO_PKG_AUTHORS"); +const NAME: &str = "Substrate Telemetry Backend Core"; +const ABOUT: &str = "This is the Telemetry Backend Core that receives telemetry messages \ + from Substrate/Polkadot nodes and provides the data to a subsribed feed"; + +#[derive(StructOpt, Debug)] +#[structopt(name = NAME, version = VERSION, author = AUTHORS, about = ABOUT)] +struct Opts { + /// This is the socket address that Telemetryis listening to. This is restricted to + /// localhost (127.0.0.1) by default and should be fine for most use cases. If + /// you are using Telemetry in a container, you likely want to set this to '0.0.0.0:8000' + #[structopt(short = "l", long = "listen", default_value = "127.0.0.1:8000")] + socket: std::net::SocketAddr, + /// The desired log level; one of 'error', 'warn', 'info', 'debug' or 'trace', where + /// 'error' only logs errors and 'trace' logs everything. + #[structopt(required = false, long = "log", default_value = "info")] + log_level: log::LevelFilter, + /// Space delimited list of the names of chains that are not allowed to connect to + /// telemetry. Case sensitive. + #[structopt(required = false, long = "denylist")] + denylist: Vec, +} + +#[tokio::main] +async fn main() { + let opts = Opts::from_args(); + + SimpleLogger::new() + .with_level(opts.log_level) + .init() + .expect("Must be able to start a logger"); + + log::info!("Starting Telemetry Core version: {}", VERSION); + + if let Err(e) = start_server(opts).await { + log::error!("Error starting server: {}", e); + } +} + +/// Declare our routes and start the server. +async fn start_server(opts: Opts) -> anyhow::Result<()> { + let shard_aggregator = Aggregator::spawn(opts.denylist).await?; + let feed_aggregator = shard_aggregator.clone(); + + // Handle requests to /health by returning OK. + let health_route = warp::path("health").map(|| "OK"); + + // Handle websocket requests from shards. + let ws_shard_submit_route = warp::path("shard_submit") + .and(warp::ws()) + .and(warp::filters::addr::remote()) + .map(move |ws: ws::Ws, addr: Option| { + let tx_to_aggregator = shard_aggregator.subscribe_shard(); + log::info!("Opening /shard_submit connection from {:?}", addr); + ws.on_upgrade(move |websocket| async move { + let (mut tx_to_aggregator, websocket) = + handle_shard_websocket_connection(websocket, tx_to_aggregator).await; + log::info!("Closing /shard_submit connection from {:?}", addr); + // Tell the aggregator that this connection has closed, so it can tidy up. + let _ = tx_to_aggregator + .send(FromShardWebsocket::Disconnected) + .await; + let _ = websocket.close().await; + }) + }); + + // Handle websocket requests from frontends. + let ws_feed_route = warp::path("feed") + .and(warp::ws()) + .and(warp::filters::addr::remote()) + .map(move |ws: ws::Ws, addr: Option| { + let tx_to_aggregator = feed_aggregator.subscribe_feed(); + log::info!("Opening /feed connection from {:?}", addr); + + // We can decide how many messages can be buffered to be sent, but not specifically how + // large those messages are cumulatively allowed to be: + ws.max_send_queue(1_000) + .on_upgrade(move |websocket| async move { + let (mut tx_to_aggregator, websocket) = + handle_feed_websocket_connection(websocket, tx_to_aggregator).await; + log::info!("Closing /feed connection from {:?}", addr); + // Tell the aggregator that this connection has closed, so it can tidy up. + let _ = tx_to_aggregator.send(FromFeedWebsocket::Disconnected).await; + let _ = websocket.close().await; + }) + }); + + // Merge the routes and start our server: + let routes = ws_shard_submit_route.or(ws_feed_route).or(health_route); + warp::serve(routes).run(opts.socket).await; + Ok(()) +} + +/// This handles messages coming to/from a shard connection +async fn handle_shard_websocket_connection( + mut websocket: ws::WebSocket, + mut tx_to_aggregator: S, +) -> (S, ws::WebSocket) +where + S: futures::Sink + Unpin, +{ + let (tx_to_shard_conn, mut rx_from_aggregator) = mpsc::unbounded(); + + // Tell the aggregator about this new connection, and give it a way to send messages to us: + let init_msg = FromShardWebsocket::Initialize { + channel: tx_to_shard_conn, + }; + if let Err(e) = tx_to_aggregator.send(init_msg).await { + log::error!("Error sending message to aggregator: {}", e); + return (tx_to_aggregator, websocket); + } + + // Loop, handling new messages from the shard or from the aggregator: + loop { + tokio::select! { + // AGGREGATOR -> SHARD + msg = rx_from_aggregator.next() => { + // End the loop when connection from aggregator ends: + let msg = match msg { + Some(msg) => msg, + None => break + }; + + let internal_msg = match msg { + ToShardWebsocket::Mute { local_id, reason } => { + internal_messages::FromTelemetryCore::Mute { local_id, reason } + } + }; + + let bytes = bincode::options() + .serialize(&internal_msg) + .expect("message to shard should serialize"); + + if let Err(e) = websocket.send(ws::Message::binary(bytes)).await { + log::error!("Error sending message to shard; booting it: {}", e); + break + } + } + // SHARD -> AGGREGATOR + msg = websocket.next() => { + // End the loop when connection from shard ends: + let msg = match msg { + Some(msg) => msg, + None => break + }; + + let msg = match msg { + Err(e) => { + log::error!("Error receiving message from shard; booting it: {}", e); + break; + }, + Ok(msg) => msg + }; + + // Close message? Break and allow connection to be dropped. + if msg.is_close() { + break; + } + + // If the message isn't something we want to handle, just ignore it. + // This includes system messages like "pings" and such, so don't log anything. + if !msg.is_binary() && !msg.is_text() { + continue; + } + + let bytes = msg.as_bytes(); + let msg: internal_messages::FromShardAggregator = match bincode::options().deserialize(bytes) { + Ok(msg) => msg, + Err(e) => { + log::error!("Failed to deserialize message from shard; booting it: {}", e); + break; + } + }; + + // Convert and send to the aggregator: + let aggregator_msg = match msg { + internal_messages::FromShardAggregator::AddNode { ip, node, local_id, genesis_hash } => { + FromShardWebsocket::Add { ip, node, genesis_hash, local_id } + }, + internal_messages::FromShardAggregator::UpdateNode { payload, local_id } => { + FromShardWebsocket::Update { local_id, payload } + }, + internal_messages::FromShardAggregator::RemoveNode { local_id } => { + FromShardWebsocket::Remove { local_id } + }, + }; + if let Err(e) = tx_to_aggregator.send(aggregator_msg).await { + log::error!("Failed to send message to aggregator; closing shard: {}", e); + break; + } + } + } + } + + // loop ended; give socket back to parent: + (tx_to_aggregator, websocket) +} + +/// This handles messages coming from a feed connection +async fn handle_feed_websocket_connection( + mut websocket: ws::WebSocket, + mut tx_to_aggregator: S, +) -> (S, ws::WebSocket) +where + S: futures::Sink + Unpin, +{ + // unbounded channel so that slow feeds don't block aggregator progress: + let (tx_to_feed_conn, rx_from_aggregator) = mpsc::unbounded(); + let mut rx_from_aggregator_chunks = ReadyChunksAll::new(rx_from_aggregator); + + // Tell the aggregator about this new connection, and give it a way to send messages to us: + let init_msg = FromFeedWebsocket::Initialize { + channel: tx_to_feed_conn, + }; + if let Err(e) = tx_to_aggregator.send(init_msg).await { + log::error!("Error sending message to aggregator: {}", e); + return (tx_to_aggregator, websocket); + } + + // Loop, handling new messages from the shard or from the aggregator: + loop { + // Without any special handling, if messages come in every ~2.5ms to each feed, the select! loop + // has to wake up 400 times a second to poll things. If we have 1000 feeds, that's 400,000 wakeups + // per second. Even without any work in the loop, that uses a bunch of CPU. As an example, try + // replacing the loop with this: + // + // ``` + // let s = tokio::time::sleep(tokio::time::Duration::from_micros(2500)); + // tokio::select! { + // _ = s => {}, + // _ = websocket.next() => {} + // } + // continue; + // ``` + // + // To combat this, we add a small wait to reduce how often the select loop will be woken up under high load. We + // buffer messages to feeds so that we do as much work as possible during each wakeup, and if the + // wakeup lasts longer than 75ms we don't wait before polling again. This knocks ~80% of a CPU worth of usage + // off on my machine running a soak test with 500 feeds, 4 shards and 100 nodes, doesn't seem to impact + // memory usage much, and still ensures that messages are delivered in a timely fashion. + // + // Increasing the wait to 100ms or more doesn't seem to have much more of a positive impact anyway. + let debounce = tokio::time::sleep_until(tokio::time::Instant::now() + std::time::Duration::from_millis(75)); + + tokio::select! {biased; + + // FRONTEND -> AGGREGATOR (relay messages to the aggregator). Biased, so messages + // from the UI will have priority (especially important with our debounce delay). + msg = websocket.next() => { + // End the loop when connection from feed ends: + let msg = match msg { + Some(msg) => msg, + None => break + }; + + // If we see any errors, log them and end our loop: + let msg = match msg { + Err(e) => { + log::error!("Error in node websocket connection: {}", e); + break; + }, + Ok(msg) => msg + }; + + // Close message? Break and allow connection to be dropped. + if msg.is_close() { + break; + } + + // We ignore all but text messages from the frontend: + let text = match msg.to_str() { + Ok(s) => s, + Err(_) => continue + }; + + // Parse the message into a command we understand and send it to the aggregator: + let cmd = match FromFeedWebsocket::from_str(text) { + Ok(cmd) => cmd, + Err(e) => { + log::warn!("Ignoring invalid command '{}' from the frontend: {}", text, e); + continue + } + }; + if let Err(e) = tx_to_aggregator.send(cmd).await { + log::error!("Failed to send message to aggregator; closing feed: {}", e); + break; + } + } + + // AGGREGATOR -> FRONTEND (buffer messages to the UI) + msgs = rx_from_aggregator_chunks.next() => { + // End the loop when connection from aggregator ends: + let msgs = match msgs { + Some(msgs) => msgs, + None => break + }; + + // There is only one message type at the mo; bytes to send + // to the websocket. collect them all up to dispatch in one shot. + let all_ws_msgs = msgs.into_iter().map(|msg| { + let bytes = match msg { + ToFeedWebsocket::Bytes(bytes) => bytes + }; + Ok(ws::Message::binary(&*bytes)) + }); + + if let Err(e) = websocket.send_all(&mut futures::stream::iter(all_ws_msgs)).await { + log::warn!("Closing feed websocket due to error: {}", e); + break; + } + } + } + + debounce.await; + } + + // loop ended; give socket back to parent: + (tx_to_aggregator, websocket) +} diff --git a/backend/telemetry_core/src/state/chain.rs b/backend/telemetry_core/src/state/chain.rs new file mode 100644 index 0000000..08ee985 --- /dev/null +++ b/backend/telemetry_core/src/state/chain.rs @@ -0,0 +1,370 @@ +use common::node_message::Payload; +use common::node_types::{Block, Timestamp}; +use common::node_types::{BlockHash, BlockNumber}; +use common::{id_type, time, DenseMap, MostSeen, NumStats}; +use once_cell::sync::Lazy; +use std::collections::HashSet; + +use crate::feed_message::{self, FeedMessageSerializer}; +use crate::find_location; + +use super::node::Node; + +id_type! { + /// A Node ID that is unique to the chain it's in. + pub struct ChainNodeId(usize) +} + +pub type Label = Box; + +const STALE_TIMEOUT: u64 = 2 * 60 * 1000; // 2 minutes + +pub struct Chain { + /// Labels that nodes use for this chain. We keep track of + /// the most commonly used label as nodes are added/removed. + labels: MostSeen