From f794950663b2dbf878116583ac0c0d43a316904d Mon Sep 17 00:00:00 2001 From: Zeeshan Lakhani Date: Wed, 29 Nov 2023 15:34:03 -0500 Subject: [PATCH] chore: comments, logs, & cleanup (#456) # Description Remove many TODOs, add comments on *pub* (*pub* crate), and work through logs. - Includes a few other minor fixins and additions. ## Type of change - [X] Refactor (non-breaking change that updates existing functionality) Closes #262. --------- Signed-off-by: Zeeshan Lakhani Co-authored-by: Brian Ginsburg <7957636+bgins@users.noreply.github.com> --- Cargo.lock | 21 + Cargo.toml | 2 + DEVELOPMENT.md | 41 +- README.md | 3 +- examples/README.md | 2 +- examples/websocket-relay/example_test.wasm | Bin 295627 -> 296851 bytes .../relay-app/src/lib/workflow.ts | 12 +- examples/websocket-relay/src/main.rs | 9 +- flake.lock | 12 +- flake.nix | 3 +- homestar-core/src/test_utils/workflow.rs | 2 +- homestar-core/src/workflow/instruction.rs | 4 +- homestar-core/src/workflow/task.rs | 2 +- homestar-functions/test/src/lib.rs | 12 +- homestar-functions/test/wit/host.wit | 1 + homestar-runtime/Cargo.toml | 3 +- .../src/{event_handler => }/channel.rs | 4 +- homestar-runtime/src/cli/show.rs | 2 + homestar-runtime/src/db.rs | 9 +- homestar-runtime/src/db/utils.rs | 3 + homestar-runtime/src/event_handler.rs | 47 +- homestar-runtime/src/event_handler/cache.rs | 6 + homestar-runtime/src/event_handler/event.rs | 180 +- .../src/event_handler/notification.rs | 36 +- .../src/event_handler/notification/receipt.rs | 9 +- .../src/event_handler/notification/swarm.rs | 4 + .../src/event_handler/swarm_event.rs | 312 +- homestar-runtime/src/lib.rs | 5 +- homestar-runtime/src/libp2p/mod.rs | 2 + homestar-runtime/src/libp2p/multiaddr.rs | 2 + homestar-runtime/src/main.rs | 4 +- homestar-runtime/src/metrics.rs | 2 + homestar-runtime/src/network/error.rs | 2 + homestar-runtime/src/network/mod.rs | 7 +- .../src/network/pubsub/message.rs | 4 + homestar-runtime/src/network/rpc.rs | 22 +- homestar-runtime/src/network/swarm.rs | 45 +- homestar-runtime/src/network/webserver.rs | 66 +- .../src/network/webserver/listener.rs | 4 +- .../src/network/webserver/notifier.rs | 12 +- homestar-runtime/src/network/webserver/rpc.rs | 53 +- homestar-runtime/src/receipt.rs | 5 +- homestar-runtime/src/runner.rs | 116 +- homestar-runtime/src/runner/nodeinfo.rs | 15 +- homestar-runtime/src/runner/response.rs | 2 +- homestar-runtime/src/scheduler.rs | 16 +- homestar-runtime/src/settings.rs | 4 +- .../src/settings/libp2p_config.rs | 2 +- .../src/settings/pubkey_config.rs | 34 +- homestar-runtime/src/tasks.rs | 2 - homestar-runtime/src/tasks/fetch.rs | 20 +- homestar-runtime/src/tasks/wasm.rs | 4 + .../src/test_utils/worker_builder.rs | 33 +- homestar-runtime/src/worker.rs | 72 +- homestar-runtime/src/workflow.rs | 7 +- homestar-runtime/src/workflow/info.rs | 22 + homestar-runtime/src/workflow/settings.rs | 5 + homestar-runtime/tests/cli.rs | 4 +- .../tests/fixtures/test-workflow-add-one.json | 6 +- .../test-workflow-image-pipeline.json | 10 +- .../tests/fixtures/test-workflow-jco.json | 25 +- .../fixtures/test-workflow-no-awaits1.json | 2 +- .../fixtures/test-workflow-no-awaits2.json | 4 +- homestar-runtime/tests/network.rs | 6 +- homestar-runtime/tests/webserver.rs | 9 +- homestar-wasm/fixtures/example_add.wasm | Bin 8780 -> 8774 bytes homestar-wasm/fixtures/example_add.wat | 2642 ++++++++-------- .../fixtures/example_add_component.wasm | Bin 8803 -> 8797 bytes .../fixtures/example_add_component.wat | 2644 ++++++++--------- homestar-wasm/fixtures/example_test.wasm | Bin 295627 -> 296851 bytes .../fixtures/example_test_component.wasm | Bin 296304 -> 297577 bytes 71 files changed, 3680 insertions(+), 3002 deletions(-) rename homestar-runtime/src/{event_handler => }/channel.rs (93%) diff --git a/Cargo.lock b/Cargo.lock index c63b475a..3c858554 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1035,6 +1035,26 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28c122c3980598d243d63d9a704629a2d748d101f278052ff068be5a4423ab6f" +[[package]] +name = "const_format" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a214c7af3d04997541b18d432afaff4c455e79e2029079647e72fc2bd27673" +dependencies = [ + "const_format_proc_macros", +] + +[[package]] +name = "const_format_proc_macros" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f6ff08fd20f4f299298a28e2dfa8a8ba1036e6cd2460ac1de7b425d76f2500" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + [[package]] name = "constant_time_eq" version = "0.3.0" @@ -2494,6 +2514,7 @@ dependencies = [ "clap", "config", "console-subscriber", + "const_format", "criterion", "crossbeam", "daemonize", diff --git a/Cargo.toml b/Cargo.toml index d22f0536..0035ea79 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -76,6 +76,7 @@ lto = true debug-assertions = false [profile.release.package.homestar-runtime] +# Will slow-down compile, but improve perf on generated code. codegen-units = 1 debug-assertions = false @@ -87,6 +88,7 @@ debug-assertions = false # Example: `cargo build -p homestar-functions-test --target wasm32-unknown-unknown --profile release-wasm-fn` [profile.release-wasm-fn] inherits = "release" +# Will slow-down compile, but improve perf on generated code. codegen-units = 1 # Tell `rustc` to optimize for small code size. opt-level = "z" # 'z' to optimize "aggressively" for size diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 072e8502..a92f110e 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -2,6 +2,7 @@ ## Outline +- [Building and Running the Project](#building-and-running-the-project) - [Testing the Project](#testing-the-project) - [Running the Runtime on Docker](#running-the-runtime-on-docker) - [Nix](#nix) @@ -10,18 +11,51 @@ - [Recommended Development Flow](#recommended-development-flow) - [Conventional Commits](#conventional-commits) +## Building and Running the Project + +- Building `homestar`: + + For the fastest compile-times and prettiest logs while developing `homestar`, + build with: + + ``` console + cargo build --no-default-features --features dev + ``` + + This removes underlying `wasmtime` `zstd` compression while also turning on + ANSI color-coded logs. If you build with default features, `zstd` compression + and other `wasmtime` defaults will be included in the build. + +- Running the `homestar` server/runtime: + + ``` console + cargo run --no-default-features --features dev -- start + ``` + +- Running with [`tokio-console`][tokio-console] for diagnosis and debugging: + + ``` console + cargo run --no-default-features --features dev,console -- start + ``` + + Then, in another window: + + ```console + tokio-console --retain-for 60sec + ``` + ## Testing the Project - Running the tests: -We recommend using [cargo nextest][cargo-nextest], which is installed by default -in our [Nix flake](#nix) or can be [installed separately][cargo-nextest-install]. + We recommend using [cargo nextest][cargo-nextest], which is installed by default + in our [Nix flake](#nix) or can be [installed separately][cargo-nextest-install]. ```console cargo nextest run --all-features --no-capture ``` -The above command translates to this using the default `cargo test`: + This command translates to the default `cargo test` command: ```console cargo test --all-features -- --nocapture @@ -139,4 +173,5 @@ a type of `fix`, `feat`, `docs`, `ci`, `refactor`, etc..., structured like so: [nix]:https://nixos.org/download.html [nix-flake]: https://nixos.wiki/wiki/Flakes [pre-commit]: https://pre-commit.com/ +[tokio-console]: https://github.com/tokio-rs/console [wit-bindgen]: https://github.com/bytecodealliance/wit-bindgen diff --git a/README.md b/README.md index 6aa1a7e7..0a0fd360 100644 --- a/README.md +++ b/README.md @@ -103,7 +103,7 @@ Our current list includes: - [websocket relay](./examples/websocket-relay/README.md) - An example (browser-based) application that connects to the `homestar-runtime` over a - websocket connection in order to run a couple static Wasm-based, image + WebSocket connection in order to run a couple static Wasm-based, image processing workflows that chain inputs and outputs. ## Workspace @@ -177,7 +177,6 @@ We would be happy to try to answer your question or try opening a new issue on G - [IPVM - IPFS and WASM][ipfs-thing-ipvm] by Brooklyn Zelenka - [Breaking Down the Interplanetary Virtual Machine][blog-1] - [Ucan Invocation Spec][ucan-invocation] -- [Wasm/Wit Demo - Februrary 2023][demo-1] by Zeeshan Lakhani ## License diff --git a/examples/README.md b/examples/README.md index b0766833..a99ab3fc 100644 --- a/examples/README.md +++ b/examples/README.md @@ -5,7 +5,7 @@ and the `homestar runtime`. Each example is set up as its own crate, demonstrating the necessary dependencies and setup(s). * [websocket relay](./websocket-relay) - An example (browser-based) application - that connects to the `homestar-runtime` over a websocket connection in order + that connects to the `homestar-runtime` over a WebSocket connection in order to run a couple static Wasm-based, image processing workflows that chain inputs and outputs using [inlined promises][pipelines]. diff --git a/examples/websocket-relay/example_test.wasm b/examples/websocket-relay/example_test.wasm index 0d0e7528d835ff49d349f962434efb46988b1458..15306f11e0e5d808928e8c95f241ea52cd44cbe8 100755 GIT binary patch delta 29460 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 F{|D@VwJHDr diff --git a/examples/websocket-relay/relay-app/src/lib/workflow.ts b/examples/websocket-relay/relay-app/src/lib/workflow.ts index d362620e..f71e7448 100644 --- a/examples/websocket-relay/relay-app/src/lib/workflow.ts +++ b/examples/websocket-relay/relay-app/src/lib/workflow.ts @@ -256,7 +256,7 @@ export const workflowOnePromised = WorkflowBuilder.workflow({ WorkflowBuilder.crop({ name: "crop", resource: - "ipfs://bafybeiczefaiu7464ehupezpzulnti5jvcwnvdalqrdliugnnwcdz6ljia", + "ipfs://bafybeig6u35v6t3f4j3zgz2jvj4erd45fbkeolioaddu3lmu6uxm3ilb7a", args: { data: "{{ cid:bafybeiejevluvtoevgk66plh5t6xiy3ikyuuxg3vgofuvpeckb6eadresm }}", x: 150, @@ -268,7 +268,7 @@ export const workflowOnePromised = WorkflowBuilder.workflow({ WorkflowBuilder.rotate90({ name: "rotate90", resource: - "ipfs://bafybeiczefaiu7464ehupezpzulnti5jvcwnvdalqrdliugnnwcdz6ljia", + "ipfs://bafybeig6u35v6t3f4j3zgz2jvj4erd45fbkeolioaddu3lmu6uxm3ilb7a", args: { data: "{{needs.crop.output}}", }, @@ -276,7 +276,7 @@ export const workflowOnePromised = WorkflowBuilder.workflow({ WorkflowBuilder.blur({ name: "blur", resource: - "ipfs://bafybeiczefaiu7464ehupezpzulnti5jvcwnvdalqrdliugnnwcdz6ljia", + "ipfs://bafybeig6u35v6t3f4j3zgz2jvj4erd45fbkeolioaddu3lmu6uxm3ilb7a", args: { data: "{{needs.rotate90.output}}", sigma: 20.2, @@ -293,7 +293,7 @@ export const workflowTwoPromised = WorkflowBuilder.workflow({ WorkflowBuilder.crop({ name: "crop", resource: - "ipfs://bafybeiczefaiu7464ehupezpzulnti5jvcwnvdalqrdliugnnwcdz6ljia", + "ipfs://bafybeig6u35v6t3f4j3zgz2jvj4erd45fbkeolioaddu3lmu6uxm3ilb7a", args: { data: "{{ cid:bafybeiejevluvtoevgk66plh5t6xiy3ikyuuxg3vgofuvpeckb6eadresm }}", x: 150, @@ -305,7 +305,7 @@ export const workflowTwoPromised = WorkflowBuilder.workflow({ WorkflowBuilder.rotate90({ name: "rotate90", resource: - "ipfs://bafybeiczefaiu7464ehupezpzulnti5jvcwnvdalqrdliugnnwcdz6ljia", + "ipfs://bafybeig6u35v6t3f4j3zgz2jvj4erd45fbkeolioaddu3lmu6uxm3ilb7a", args: { data: "{{needs.crop.output}}", }, @@ -313,7 +313,7 @@ export const workflowTwoPromised = WorkflowBuilder.workflow({ WorkflowBuilder.grayscale({ name: "grayscale", resource: - "ipfs://bafybeiczefaiu7464ehupezpzulnti5jvcwnvdalqrdliugnnwcdz6ljia", + "ipfs://bafybeig6u35v6t3f4j3zgz2jvj4erd45fbkeolioaddu3lmu6uxm3ilb7a", args: { data: "{{needs.rotate90.output}}", }, diff --git a/examples/websocket-relay/src/main.rs b/examples/websocket-relay/src/main.rs index d5b00051..b17d847a 100644 --- a/examples/websocket-relay/src/main.rs +++ b/examples/websocket-relay/src/main.rs @@ -16,18 +16,11 @@ fn main() -> Result<()> { // daemon. Typically, these would be started separately. let ipfs_daemon = ipfs_setup(); - info!( - subject = "settings", - category = "homestar_init", - "starting with settings: {:?}", - settings, - ); + info!("starting with settings: {:?}", settings,); let db = Db::setup_connection_pool(settings.node(), None).expect("to setup database pool"); info!( - subject = "database", - category = "homestar_init", "starting with database: {}", Db::url().expect("database url to be provided"), ); diff --git a/flake.lock b/flake.lock index 4c33b930..685d0257 100644 --- a/flake.lock +++ b/flake.lock @@ -36,11 +36,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1699186365, - "narHash": "sha256-Pxrw5U8mBsL3NlrJ6q1KK1crzvSUcdfwb9083sKDrcU=", + "lastModified": 1701080425, + "narHash": "sha256-QUaQPXLMsgIWxY2JsbK2TlqKHtcbhf9BGpmn4ilAkrI=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "a0b3b06b7a82c965ae0bb1d59f6e386fe755001d", + "rev": "21ea9c80732bc4168ed38c5c2f1f4df37c57a6dd", "type": "github" }, "original": { @@ -68,11 +68,11 @@ ] }, "locked": { - "lastModified": 1699323235, - "narHash": "sha256-ZFRItRv0dDSzsfpqSjj9qWM/SA1kRrOk6R04qhBZuxM=", + "lastModified": 1701224160, + "narHash": "sha256-qnMmxNMKmd6Soel0cfauyMJ+LzuZbvmiDQPSIuTbQ+M=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "8a9d6f544c08ee898c7f3761cc9587be7565db5e", + "rev": "4a080e26d55eaedb95ab1bf8eeaeb84149c10f12", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index cbf11fa8..b7795159 100644 --- a/flake.nix +++ b/flake.nix @@ -57,7 +57,6 @@ cargo-expand cargo-nextest cargo-sort - cargo-spellcheck cargo-unused-features cargo-udeps cargo-watch @@ -101,7 +100,7 @@ devRunServer = pkgs.writeScriptBin "cargo-run-dev" '' #!${pkgs.stdenv.shell} - cargo run --no-default-features --features dev -- start -c homestar-runtime/config/settings.toml + cargo run --no-default-features --features dev -- start ''; doc = pkgs.writeScriptBin "doc" '' diff --git a/homestar-core/src/test_utils/workflow.rs b/homestar-core/src/test_utils/workflow.rs index 956cedc4..2c11f01e 100644 --- a/homestar-core/src/test_utils/workflow.rs +++ b/homestar-core/src/test_utils/workflow.rs @@ -19,7 +19,7 @@ use std::collections::BTreeMap; use url::Url; const RAW: u64 = 0x55; -const WASM_CID: &str = "bafybeiczefaiu7464ehupezpzulnti5jvcwnvdalqrdliugnnwcdz6ljia"; +const WASM_CID: &str = "bafybeig6u35v6t3f4j3zgz2jvj4erd45fbkeolioaddu3lmu6uxm3ilb7a"; type NonceBytes = Vec; diff --git a/homestar-core/src/workflow/instruction.rs b/homestar-core/src/workflow/instruction.rs index 991a4134..3c9c18d8 100644 --- a/homestar-core/src/workflow/instruction.rs +++ b/homestar-core/src/workflow/instruction.rs @@ -336,7 +336,7 @@ mod test { ( RESOURCE_KEY.into(), Ipld::String( - "ipfs://bafybeiczefaiu7464ehupezpzulnti5jvcwnvdalqrdliugnnwcdz6ljia".into() + "ipfs://bafybeig6u35v6t3f4j3zgz2jvj4erd45fbkeolioaddu3lmu6uxm3ilb7a".into() ) ), (OP_KEY.into(), Ipld::String("ipld/fun".to_string())), @@ -357,7 +357,7 @@ mod test { "func": "join-strings" }, "nnc": "", "op": "wasm/run", - "rsc": "ipfs://bafybeiczefaiu7464ehupezpzulnti5jvcwnvdalqrdliugnnwcdz6ljia"}); + "rsc": "ipfs://bafybeig6u35v6t3f4j3zgz2jvj4erd45fbkeolioaddu3lmu6uxm3ilb7a"}); let instruction = Instruction::::try_from(ipld.clone()).unwrap(); let instr_cid = instruction.to_cid().unwrap(); diff --git a/homestar-core/src/workflow/task.rs b/homestar-core/src/workflow/task.rs index 8bd6d914..02b35255 100644 --- a/homestar-core/src/workflow/task.rs +++ b/homestar-core/src/workflow/task.rs @@ -190,7 +190,7 @@ mod test { ( "rsc".into(), Ipld::String( - "ipfs://bafybeiczefaiu7464ehupezpzulnti5jvcwnvdalqrdliugnnwcdz6ljia".into(), + "ipfs://bafybeig6u35v6t3f4j3zgz2jvj4erd45fbkeolioaddu3lmu6uxm3ilb7a".into(), ), ), ("op".into(), Ipld::String("ipld/fun".to_string())), diff --git a/homestar-functions/test/src/lib.rs b/homestar-functions/test/src/lib.rs index 706aee9a..b4332144 100644 --- a/homestar-functions/test/src/lib.rs +++ b/homestar-functions/test/src/lib.rs @@ -6,7 +6,11 @@ wit_bindgen::generate!({ }); use base64::{engine::general_purpose, Engine}; -use std::io::Cursor; +use std::{ + collections::hash_map::DefaultHasher, + hash::{Hash, Hasher}, + io::Cursor, +}; pub struct Component; @@ -115,6 +119,12 @@ impl Guest for Component { .unwrap(); Self::rotate90(decoded) } + + fn hash(s: String) -> Vec { + let mut hash = DefaultHasher::new(); + s.hash(&mut hash); + hash.finish().to_be_bytes().to_vec() + } } #[cfg(test)] diff --git a/homestar-functions/test/wit/host.wit b/homestar-functions/test/wit/host.wit index cab94571..87cdb521 100644 --- a/homestar-functions/test/wit/host.wit +++ b/homestar-functions/test/wit/host.wit @@ -13,4 +13,5 @@ world test { export grayscale-base64: func(data: string) -> list export rotate90: func(data: list) -> list export rotate90-base64: func(data: string) -> list + export hash: func(data: string) -> list } diff --git a/homestar-runtime/Cargo.toml b/homestar-runtime/Cargo.toml index e991ca26..5588f3d6 100644 --- a/homestar-runtime/Cargo.toml +++ b/homestar-runtime/Cargo.toml @@ -54,6 +54,7 @@ config = { version = "0.13", default-features = false, features = ["toml"] } console-subscriber = { version = "0.2", default-features = false, features = [ "parking_lot", ], optional = true } +const_format = "0.2" crossbeam = "0.8" dagga = "0.2" dashmap = "5.5" @@ -218,7 +219,7 @@ wait-timeout = "0.2" [features] default = ["wasmtime-default", "ipfs", "monitoring", "websocket-notify"] -dev = ["ansi-logs", "console", "ipfs", "monitoring", "websocket-notify"] +dev = ["ansi-logs", "ipfs", "monitoring", "websocket-notify"] ansi-logs = ["tracing-logfmt/ansi_logs"] console = ["dep:console-subscriber"] ipfs = ["dep:ipfs-api", "dep:ipfs-api-backend-hyper"] diff --git a/homestar-runtime/src/event_handler/channel.rs b/homestar-runtime/src/channel.rs similarity index 93% rename from homestar-runtime/src/event_handler/channel.rs rename to homestar-runtime/src/channel.rs index 4eab0e24..d06eb361 100644 --- a/homestar-runtime/src/event_handler/channel.rs +++ b/homestar-runtime/src/channel.rs @@ -1,5 +1,5 @@ -//! Wrapper around [crossbeam::channel] and [flume::bounded] to provide common -//! interfaces for sync/async bounded and non-tokio "oneshot" channels. +//! Wrapper around [crossbeam::channel] and [flume] to provide common +//! interfaces for sync/async (un)bounded and non-tokio "oneshot" channels. use crossbeam::channel; diff --git a/homestar-runtime/src/cli/show.rs b/homestar-runtime/src/cli/show.rs index c631b539..d38bdd2d 100644 --- a/homestar-runtime/src/cli/show.rs +++ b/homestar-runtime/src/cli/show.rs @@ -1,3 +1,5 @@ +//! Styled, output response for console table. + use std::{ fmt, io::{self, Write}, diff --git a/homestar-runtime/src/db.rs b/homestar-runtime/src/db.rs index 02f14fc7..f8f06238 100644 --- a/homestar-runtime/src/db.rs +++ b/homestar-runtime/src/db.rs @@ -25,7 +25,6 @@ use tracing::info; pub mod schema; pub(crate) mod utils; -pub(crate) const ENV: &str = "DATABASE_URL"; const MIGRATIONS: EmbeddedMigrations = embed_migrations!("migrations/"); const PRAGMAS: &str = " PRAGMA journal_mode = WAL; -- better write-concurrency @@ -35,6 +34,9 @@ PRAGMA busy_timeout = 1000; -- sleep if the database is busy PRAGMA foreign_keys = ON; -- enforce foreign keys "; +/// Database environment variable. +pub(crate) const ENV: &str = "DATABASE_URL"; + /// A Sqlite connection [pool]. /// /// [pool]: r2d2::Pool @@ -92,7 +94,7 @@ pub trait Database: Send + Sync + Clone { fn setup(url: &str) -> Result { info!( subject = "database", - category = "homestar_init", + category = "homestar.init", "setting up database at {}, running migrations if needed", url ); @@ -211,6 +213,9 @@ pub trait Database: Send + Sync + Clone { } /// Store localized workflow cid and information, e.g. number of tasks. + /// + /// On conflicts, do nothing. + /// Otherwise, return the stored workflow. fn store_workflow( workflow: workflow::Stored, conn: &mut Connection, diff --git a/homestar-runtime/src/db/utils.rs b/homestar-runtime/src/db/utils.rs index 6dc7c64c..08d76e36 100644 --- a/homestar-runtime/src/db/utils.rs +++ b/homestar-runtime/src/db/utils.rs @@ -1,5 +1,8 @@ +//! Utility functions Database interaction. + use chrono::NaiveDateTime; +/// Trait for converting nanoseconds to a timestamp. pub(crate) trait Timestamp { fn timestamp_from_nanos(&self) -> Option; } diff --git a/homestar-runtime/src/event_handler.rs b/homestar-runtime/src/event_handler.rs index fdc6e9d9..0a3f9490 100644 --- a/homestar-runtime/src/event_handler.rs +++ b/homestar-runtime/src/event_handler.rs @@ -5,6 +5,7 @@ use crate::network::webserver::{self, notifier}; #[cfg(feature = "ipfs")] use crate::network::IpfsCli; use crate::{ + channel, db::Database, network::swarm::{ComposedBehaviour, PeerDiscoveryInfo, RequestResponseKey}, settings, @@ -22,7 +23,6 @@ use swarm_event::ResponseEvent; use tokio::{runtime::Handle, select}; pub(crate) mod cache; -pub mod channel; pub(crate) mod error; pub(crate) mod event; #[cfg(feature = "websocket-notify")] @@ -51,24 +51,45 @@ where #[cfg_attr(docsrs, doc(cfg(feature = "websocket-notify")))] #[allow(missing_debug_implementations, dead_code)] pub(crate) struct EventHandler { + /// Minimum number of peers required to receive a receipt. receipt_quorum: usize, + /// Minimum number of peers required to receive workflow information. workflow_quorum: usize, + /// Timeout for p2p provider requests. p2p_provider_timeout: Duration, + /// Accessible database instance. db: DB, + /// [libp2p::swarm::Swarm] swarm instance. swarm: Swarm, + /// [moka::future::Cache] instance, used for retry logic. cache: Arc>, + /// [channel::AsyncChannelSender] for sending [Event]s to the [EventHandler]. sender: Arc>, + /// [channel::AsyncChannelReceiver] for receiving [Event]s from the [EventHandler]. receiver: channel::AsyncChannelReceiver, + /// [QueryId] to [RequestResponseKey] and [P2PSender] mapping. query_senders: FnvHashMap)>, + /// [PeerId] to [ConnectedPoint] connections mapping. connections: Connections, + /// [RequestId] to [RequestResponseKey] and [P2PSender] mapping. request_response_senders: FnvHashMap, + /// Rendezvous protocol configurations and state (cookies). rendezvous: Rendezvous, + /// Whether or not to enable pubsub. pubsub_enabled: bool, + /// [tokio::sync::broadcast::Sender] for websocket event + /// notification messages. ws_evt_sender: webserver::Notifier, + /// [tokio::sync::broadcast::Sender] for websocket workflow-related + /// notification messages. ws_workflow_sender: webserver::Notifier, + /// [libp2p::Multiaddr] addresses to dial. node_addresses: Vec, + /// [libp2p::Multiaddr] externally reachable addresses to announce to the network. announce_addresses: Vec, + /// Maximum number of externally reachable addresses to announce to the network. external_address_limit: u32, + /// Interval for polling the cache for expired entries. poll_cache_interval: Duration, } @@ -76,22 +97,39 @@ pub(crate) struct EventHandler { #[cfg(not(feature = "websocket-notify"))] #[allow(missing_debug_implementations, dead_code)] pub(crate) struct EventHandler { + /// Minimum number of peers required to receive a receipt. receipt_quorum: usize, + /// Minimum number of peers required to receive workflow information. workflow_quorum: usize, + /// Timeout for p2p provider requests. p2p_provider_timeout: Duration, + /// Accesible database instance. db: DB, + /// [libp2p::swarm::Swarm] swarm instance. swarm: Swarm, + /// [moka::future::Cache] instance, centered around retry logic. cache: Arc>, + /// [channel::AsyncChannelReceiver] for receiving [Event]s from the [EventHandler]. sender: Arc>, + /// [channel::AsyncChannelReceiver] for receiving [Event]s from the [EventHandler]. receiver: channel::AsyncChannelReceiver, + /// [QueryId] to [RequestResponseKey] and [P2PSender] mapping. query_senders: FnvHashMap)>, + /// [PeerId] to [ConnectedPoint] connections mapping. connections: Connections, + /// [RequestId] to [RequestResponseKey] and [P2PSender] mapping. request_response_senders: FnvHashMap, + /// Rendezvous protocol configurations and state (cookies). rendezvous: Rendezvous, + /// Whether or not to enable pubsub. pubsub_enabled: bool, + /// [libp2p::Multiaddr] addresses to dial. node_addresses: Vec, + /// [libp2p::Multiaddr] externally reachable addresses to announce to the network. announce_addresses: Vec, + /// Maximum number of externally reachable addresses to announce to the network. external_address_limit: u32, + /// Interval for polling the cache for expired entries. poll_cache_interval: Duration, } @@ -210,8 +248,8 @@ where self.sender.clone() } - /// [tokio::sync::broadcast::Sender] for sending messages through the - /// webSocket server to subscribers. + /// [tokio::sync::broadcast::Sender] for sending workflow-related messages + /// through the WebSocket server to subscribers. #[cfg(feature = "websocket-notify")] #[cfg_attr(docsrs, doc(cfg(feature = "websocket-notify")))] #[allow(dead_code)] @@ -219,7 +257,8 @@ where self.ws_workflow_sender.clone() } - /// TODO + /// [tokio::sync::broadcast::Sender] for sending event-related messages + /// through the WebSocket server to subscribers. #[cfg(feature = "websocket-notify")] #[cfg_attr(docsrs, doc(cfg(feature = "websocket-notify")))] #[allow(dead_code)] diff --git a/homestar-runtime/src/event_handler/cache.rs b/homestar-runtime/src/event_handler/cache.rs index a5227dad..a4fedb8b 100644 --- a/homestar-runtime/src/event_handler/cache.rs +++ b/homestar-runtime/src/event_handler/cache.rs @@ -1,3 +1,5 @@ +//! Event-handler cache for retry events. + use crate::{channel, event_handler::Event}; use libp2p::PeerId; use moka::{ @@ -24,6 +26,7 @@ impl ExpiryBase for Expiry { } } +/// A cache value, made-up of an expiration and data map. #[derive(Clone, Debug)] pub(crate) struct CacheValue { expiration: Duration, @@ -36,18 +39,21 @@ impl CacheValue { } } +/// Kinds of data to be stored in the cache. #[derive(Clone, Debug)] pub(crate) enum CacheData { Peer(PeerId), OnExpiration(DispatchEvent), } +/// Events to be dispatched on cache expiration. #[derive(Clone, Debug)] pub(crate) enum DispatchEvent { RegisterPeer, DiscoverPeers, } +/// Setup a cache with an eviction listener. pub(crate) fn setup_cache( sender: Arc>, ) -> Cache { diff --git a/homestar-runtime/src/event_handler/event.rs b/homestar-runtime/src/event_handler/event.rs index 8ab39f62..6cf2c33a 100644 --- a/homestar-runtime/src/event_handler/event.rs +++ b/homestar-runtime/src/event_handler/event.rs @@ -34,6 +34,8 @@ use std::{collections::HashSet, num::NonZeroUsize, sync::Arc}; use tokio::runtime::Handle; use tracing::{error, info, warn}; +const RENDEZVOUS_NAMESPACE: &str = "homestar"; + /// A [Receipt] captured (inner) event. #[allow(dead_code)] #[derive(Debug, Clone)] @@ -115,12 +117,10 @@ pub(crate) enum Event { RegisterPeer(PeerId), /// Discover peers from a rendezvous node. DiscoverPeers(PeerId), - /// TODO + /// Dynamically get listeners for the swarm. GetListeners(AsyncChannelSender>), } -const RENDEZVOUS_NAMESPACE: &str = "homestar"; - #[allow(unreachable_patterns)] impl Event { async fn handle_info(self, event_handler: &mut EventHandler) -> Result<()> @@ -132,7 +132,11 @@ impl Event { let _ = captured.publish_and_notify(event_handler); } Event::Shutdown(tx) => { - info!("event_handler server shutting down"); + info!( + subject = "shutdown", + category = "handle_event", + "event_handler server shutting down" + ); event_handler.shutdown().await; let _ = tx.send_async(()).await; } @@ -183,7 +187,12 @@ impl Event { } } Event::Providers(Err(err)) => { - error!("failed to find providers: {}", err); + info!( + subject = "libp2p.providers.err", + category = "handle_event", + err=?err, + "failed to find providers", + ); } Event::RegisterPeer(peer_id) => { if let Some(rendezvous_client) = event_handler @@ -199,6 +208,8 @@ impl Event { Some(event_handler.rendezvous.registration_ttl.as_secs()), ) { warn!( + subject = "libp2p.register.rendezvous.err", + category = "handle_event", peer_id = peer_id.to_string(), err = format!("{err}"), "failed to register with rendezvous peer" @@ -265,6 +276,14 @@ impl Captured { ) } + // short-circuit if no peers + // + // - don't gossip receipt + // - don't store receipt or workflow info on DHT + if event_handler.connections.peers.is_empty() { + return Ok((self.receipt, invocation_receipt)); + } + if event_handler.pubsub_enabled { match event_handler.swarm.behaviour_mut().gossip_publish( pubsub::RECEIPTS_TOPIC, @@ -272,6 +291,8 @@ impl Captured { ) { Ok(msg_id) => { info!( + subject = "libp2p.gossip.publish", + category = "publish_event", cid = receipt_cid.to_string(), message_id = msg_id.to_string(), "message published on {} topic for receipt with cid: {receipt_cid}", @@ -292,8 +313,10 @@ impl Captured { } Err(err) => { warn!( - err=?err, + subject = "libp2p.gossip.publish.err", + category = "publish_event", cid = receipt_cid.to_string(), + err=?err, "message not published on {} topic for receipt", pubsub::RECEIPTS_TOPIC ) @@ -322,7 +345,12 @@ impl Captured { Record::new(instruction_bytes, receipt_bytes.to_vec()), receipt_quorum, ) - .map_err(|err| warn!(err=?err, "receipt not PUT on dht")); + .map_err(|err| { + warn!(subject = "libp2p.put_record.err", + category = "publish_event", + err=?err, + "receipt not PUT onto DHT") + }); Arc::make_mut(&mut self.workflow).increment_progress(receipt_cid); let workflow_cid_bytes = self.workflow.cid_as_bytes(); @@ -335,15 +363,27 @@ impl Captured { Record::new(workflow_cid_bytes, workflow_bytes), workflow_quorum, ) - .map_err(|err| warn!(err=?err, "workflow information not PUT on dht")); + .map_err(|err| { + warn!(subject = "libp2p.put_record.err", + category = "publish_event", + err=?err, + "workflow information not PUT onto DHT") + }); } else { error!( - "cannot convert workflow information {} to bytes", - self.workflow.cid() + subject = "libp2p.put_record.err", + category = "publish_event", + cid = self.workflow.cid().to_string(), + "cannot convert workflow information to bytes", ); } } else { - error!("cannot convert receipt {receipt_cid} to bytes"); + error!( + subject = "libp2p.put_record.err", + category = "publish_event", + cid = receipt_cid.to_string(), + "cannot convert receipt to bytes" + ); } Ok((self.receipt, invocation_receipt)) @@ -401,28 +441,35 @@ impl Replay { TopicMessage::CapturedReceipt(pubsub::Message::new(receipt.clone())), ) .map(|msg_id| { - info!(cid=receipt_cid, - message_id = msg_id.to_string(), - "message published on {} topic for receipt with cid: {receipt_cid}", - pubsub::RECEIPTS_TOPIC); - - #[cfg(feature = "websocket-notify")] - notification::emit_event( - event_handler.ws_evt_sender(), - EventNotificationTyp::SwarmNotification( - SwarmNotification::PublishedReceiptPubsub, - ), - btreemap! { - "cid" => receipt.cid().to_string(), - "ran" => receipt.ran().to_string() - }, - ); + info!( + subject = "libp2p.gossip.publish.replay", + category = "publish_event", + cid = receipt_cid, + message_id = msg_id.to_string(), + "message published on {} topic for receipt with cid: {receipt_cid}", + pubsub::RECEIPTS_TOPIC + ); + + #[cfg(feature = "websocket-notify")] + notification::emit_event( + event_handler.ws_evt_sender(), + EventNotificationTyp::SwarmNotification( + SwarmNotification::PublishedReceiptPubsub, + ), + btreemap! { + "cid" => receipt.cid().to_string(), + "ran" => receipt.ran().to_string() + }, + ); }) - .map_err( - |err| - warn!(err=?err, cid=receipt_cid, - "message not published on {} topic for receipt", pubsub::RECEIPTS_TOPIC), - ); + .map_err(|err| { + warn!( + subject = "libp2p.gossip.publish.replay.err", + category = "publish_event", + err=?err, + cid=receipt_cid, + "message not published on {} topic for receipt", pubsub::RECEIPTS_TOPIC) + }); }); } Ok(()) @@ -444,8 +491,6 @@ impl QueryRecord { DB: Database, { if event_handler.connections.peers.is_empty() { - info!("no connections to send request to"); - if let Some(sender) = self.sender { let _ = sender.send_async(ResponseEvent::NoPeersAvailable).await; } @@ -468,8 +513,6 @@ impl QueryRecord { DB: Database, { if event_handler.connections.peers.is_empty() { - info!("no connections to send request to"); - if let Some(sender) = self.sender { let _ = sender.send_async(ResponseEvent::NoPeersAvailable).await; } @@ -495,8 +538,6 @@ impl QueryRecord { DB: Database, { if event_handler.connections.peers.is_empty() { - info!("no connections to send request to"); - if let Some(sender) = self.sender { let _ = sender.send_async(ResponseEvent::NoPeersAvailable).await; } @@ -534,7 +575,10 @@ where #[cfg(not(feature = "ipfs"))] async fn handle_event(self, event_handler: &mut EventHandler) { if let Err(err) = self.handle_info(event_handler).await { - error!(error=?err, "error storing event") + error!(subject = "handle.err", + category = "handle_event", + error=?err, + "error storing event") } } @@ -551,37 +595,59 @@ where let handle = Handle::current(); let ipfs = ipfs.clone(); handle.spawn(async move { - if let Ok(bytes) = receipt.try_into() { - match ipfs.put_receipt_bytes(bytes).await { - Ok(put_cid) => { - info!(cid = put_cid, "IPLD DAG node stored"); - #[cfg(debug_assertions)] - debug_assert_eq!(put_cid, cid.to_string()); - } - Err(err) => { - error!(error=?err, cid=cid.to_string(), "failed to store IPLD DAG node"); + if let Ok(bytes) = receipt.try_into() { + match ipfs.put_receipt_bytes(bytes).await { + Ok(put_cid) => { + info!( + subject = "ipfs.put.receipt", + category = "handle_event", + cid = put_cid, + "IPLD DAG node stored" + ); + #[cfg(debug_assertions)] + debug_assert_eq!(put_cid, cid.to_string()); + } + Err(err) => { + warn!(subject = "ipfs.put.receipt.err", + category = "handle_event", + error=?err, + cid=cid.to_string(), + "failed to store IPLD DAG node"); + } } + } else { + warn!( + subject = "ipfs.put.receipt.err", + category = "handle_event", + cid = cid.to_string(), + "failed to convert receipt to bytes" + ); } - } else { - warn!(cid=cid.to_string(), "failed to convert receipt to bytes"); - } - }); + }); } - #[cfg(feature = "test-utils")] - info!(cid = cid.to_string(), "cid stored on the network"); } else { - error!("failed to store receipt"); + error!( + subject = "ipfs.put.receipt.err", + category = "handle_event", + "failed to capture receipt" + ); } } #[cfg(feature = "websocket-notify")] Event::ReplayReceipts(replay) => { if let Err(err) = replay.notify(event_handler) { - error!(error=?err, "error notifying receipts") + error!(subject = "replay.err", + category = "handle_event", + error=?err, + "error replaying and notifying receipts") } } event => { if let Err(err) = event.handle_info(event_handler).await { - error!(error=?err, "error storing event") + error!(subject = "event.err", + category = "handle_event", + error=?err, + "error storing event") } } } diff --git a/homestar-runtime/src/event_handler/notification.rs b/homestar-runtime/src/event_handler/notification.rs index 1d630d6d..f074c07c 100644 --- a/homestar-runtime/src/event_handler/notification.rs +++ b/homestar-runtime/src/event_handler/notification.rs @@ -1,3 +1,5 @@ +//! Evented notifications emitted to clients. + use crate::{ network::webserver::{ notifier::{self, Header, Message, Notifier, SubscriptionTyp}, @@ -16,8 +18,8 @@ use homestar_core::{ }; use libipld::{serde::from_ipld, Ipld}; use serde::{Deserialize, Serialize}; -use std::{collections::BTreeMap, str::FromStr}; -use tracing::{info, warn}; +use std::{collections::BTreeMap, fmt, str::FromStr}; +use tracing::{debug, warn}; pub(crate) mod receipt; pub(crate) mod swarm; @@ -39,9 +41,11 @@ pub(crate) fn emit_receipt( let notification = ReceiptNotification::with(invocation_receipt, receipt_cid, metadata.clone()); if let Ok(json) = notification.to_json() { - info!( + debug!( + subject = "notification.receipt", + category = "notification", cid = receipt_cid.to_string(), - "Sending receipt to websocket" + "emitting receipt to WebSocket" ); if let Some(ipld) = metadata { match (ipld.get(WORKFLOW_KEY), ipld.get(WORKFLOW_NAME_KEY)) { @@ -58,7 +62,12 @@ pub(crate) fn emit_receipt( } } } else { - warn!("Unable to serialize receipt as bytes: {receipt:?}"); + warn!( + subject = "notification.err", + category = "notification", + cid = receipt_cid.to_string(), + "unable to serialize receipt notification as bytes" + ); } } @@ -77,7 +86,12 @@ pub(crate) fn emit_event( if let Ok(json) = notification.to_json() { let _ = notifier.notify(Message::new(header, json)); } else { - warn!("Unable to serialize notification as bytes: {notification:?}"); + warn!( + subject = "notification.err", + category = "notification", + "unable to serialize event notification as bytes: {}", + notification.typ + ); } } @@ -153,6 +167,16 @@ pub(crate) enum EventNotificationTyp { SwarmNotification(SwarmNotification), } +impl fmt::Display for EventNotificationTyp { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + EventNotificationTyp::SwarmNotification(subtype) => { + write!(f, "swarm notification: {}", subtype) + } + } + } +} + impl DagJson for EventNotificationTyp where Ipld: From {} impl From for Ipld { diff --git a/homestar-runtime/src/event_handler/notification/receipt.rs b/homestar-runtime/src/event_handler/notification/receipt.rs index f2f222ec..8ca58583 100644 --- a/homestar-runtime/src/event_handler/notification/receipt.rs +++ b/homestar-runtime/src/event_handler/notification/receipt.rs @@ -1,23 +1,26 @@ +//! Notification receipts. + use homestar_core::{ipld::DagJson, workflow::Receipt}; use libipld::{ipld, Cid, Ipld}; -/// A [Receipt] that is sent out *just* for websocket notifications. +/// A [Receipt] that is sent out for websocket notifications. #[derive(Debug, Clone, PartialEq)] pub(crate) struct ReceiptNotification(Ipld); impl ReceiptNotification { - /// TODO + /// Obtain a reference to the inner [Ipld] value. #[allow(dead_code)] pub(crate) fn inner(&self) -> &Ipld { &self.0 } - /// TODO + /// Obtain ownership of the inner [Ipld] value. #[allow(dead_code)] pub(crate) fn into_inner(self) -> Ipld { self.0.to_owned() } + /// Create a new [ReceiptNotification]. pub(crate) fn with(receipt: Receipt, cid: Cid, metadata: Option) -> Self { let receipt: Ipld = receipt.into(); let data = ipld!({ diff --git a/homestar-runtime/src/event_handler/notification/swarm.rs b/homestar-runtime/src/event_handler/notification/swarm.rs index c2745bd2..d5aeda46 100644 --- a/homestar-runtime/src/event_handler/notification/swarm.rs +++ b/homestar-runtime/src/event_handler/notification/swarm.rs @@ -1,3 +1,7 @@ +// Notification types for [swarm] events. +// +// [swarm]: libp2p_swarm::Swarm + use anyhow::anyhow; use serde::{Deserialize, Serialize}; use std::{fmt, str::FromStr}; diff --git a/homestar-runtime/src/event_handler/swarm_event.rs b/homestar-runtime/src/event_handler/swarm_event.rs index a9ced8d0..efa6abef 100644 --- a/homestar-runtime/src/event_handler/swarm_event.rs +++ b/homestar-runtime/src/event_handler/swarm_event.rs @@ -61,9 +61,9 @@ const RENDEZVOUS_NAMESPACE: &str = "homestar"; pub(crate) enum ResponseEvent { /// Found [PeerRecord] on the DHT. Found(Result), - /// TODO + /// No peers available to network with on the DHT. NoPeersAvailable, - /// Found Providers/[PeerId]s on the DHT. + /// Found providers/[PeerId]s on the DHT. Providers(Result>), } @@ -115,17 +115,33 @@ async fn handle_swarm_event( SwarmEvent::Behaviour(ComposedEvent::Identify(identify_event)) => { match identify_event { identify::Event::Error { peer_id, error } => { - warn!(peer_id=peer_id.to_string(), err=?error, "error while attempting to identify the remote") + warn!(subject = "libp2p.identify.err", + category = "handle_swarm_event", + peer_id=peer_id.to_string(), + err=?error, + "error while attempting to identify the remote") } identify::Event::Sent { peer_id } => { - debug!(peer_id = peer_id.to_string(), "sent identify info to peer") + debug!( + subject = "libp2p.identify.sent", + category = "handle_swarm_event", + peer_id = peer_id.to_string(), + "sent identify info to peer" + ) } identify::Event::Received { peer_id, info } => { - debug!(peer_id=peer_id.to_string(), info=?info, "identify info received from peer"); + debug!(subject = "libp2p.identify.recv", + category = "handle_swarm_event", + peer_id=peer_id.to_string(), + info=?info, + "identify info received from peer"); // Ignore peers that do not use the Homestar protocol if info.protocol_version != HOMESTAR_PROTOCOL_VER { - info!(protocol_version=info.protocol_version, "peer was not using our homestar protocol version: {HOMESTAR_PROTOCOL_VER}"); + debug!(subject ="libp2p.identify.recv", + category="handle_swarm_event", + protocol_version=info.protocol_version, + "peer was not using our homestar protocol version: {HOMESTAR_PROTOCOL_VER}"); return; } @@ -145,7 +161,6 @@ async fn handle_swarm_event( .all(|proto| !proto.is_private()) // Identify observed a potentially valid external address that we weren't aware of. // Add it to the addresses we announce to other peers. - // TODO: have a set of _maybe_ external addresses that we validate with other peers first before adding it .then(|| event_handler.swarm.add_external_address(info.observed_addr)); } @@ -156,6 +171,8 @@ async fn handle_swarm_event( for addr in info.listen_addrs { behavior.kademlia.add_address(&peer_id, addr); debug!( + subject = "libp2p.identify.recv", + category = "handle_swarm_event", peer_id = peer_id.to_string(), "added identified node to kademlia routing table" ); @@ -177,6 +194,8 @@ async fn handle_swarm_event( Some(event_handler.rendezvous.registration_ttl.as_secs()), ) { warn!( + subject = "libp2p.identify.recv", + category = "handle_swarm_event", peer_id = peer_id.to_string(), err = format!("{err}"), "failed to register with rendezvous peer" @@ -194,6 +213,8 @@ async fn handle_swarm_event( } } identify::Event::Pushed { peer_id } => debug!( + subject = "libp2p.identify.pushed", + category = "handle_swarm_event", peer_id = peer_id.to_string(), "pushed identify info to peer" ), @@ -208,6 +229,8 @@ async fn handle_swarm_event( } => { if cookie.namespace() == Some(&Namespace::from_static(RENDEZVOUS_NAMESPACE)) { debug!( + subject = "libp2p.rendezvous.client.discovered", + category = "handle_swarm_event", peer_id = rendezvous_node.to_string(), "received discovery from rendezvous server" ); @@ -222,7 +245,11 @@ async fn handle_swarm_event( // Skip dialing peers if at connected peers limit if connected_peers_count >= event_handler.connections.max_peers as usize { - warn!("peers discovered through rendezvous not dialed because max connected peers limit reached"); + debug!( + subject = "libp2p.rendezvous.client.discovered.err", + category = "handle_swarm_event", + "peers discovered not dialed because max connected peers limit reached" + ); return; } @@ -263,14 +290,18 @@ async fn handle_swarm_event( ); } Err(err) => { - warn!(peer_id=peer_id.to_string(), err=?err, "failed to dial peer discovered through rendezvous"); + warn!(subject = "libp2p.rendezvous.client.discovered.err", + category = "handle_swarm_event", + peer_id=peer_id.to_string(), + err=?err, + "failed to dial discovered peer"); } }; } else if !self_registration { - warn!( - peer_id=registration.record.peer_id().to_string(), - "peer discovered through rendezvous not dialed because the max connected peers limit was reached" - ) + debug!(subject = "libp2p.rendezvous.client.discovered.err", + category = "handle_swarm_event", + peer_id=registration.record.peer_id().to_string(), + "peer discovered not dialed because the max connected peers limit was reached") } } @@ -298,22 +329,31 @@ async fn handle_swarm_event( .await; } else { // Do not dial peers that are not using our namespace - warn!(peer_id=rendezvous_node.to_string(), namespace=?cookie.namespace(), "rendezvous peer gave records from an unexpected namespace"); + debug!(subject = "libp2p.rendezvous.client.discovered.err", + category = "handle_swarm_event", + peer_id=rendezvous_node.to_string(), + namespace=?cookie.namespace(), + "rendezvous peer gave records from an unexpected namespace"); } } rendezvous::client::Event::DiscoverFailed { rendezvous_node, error, .. - } => { - error!(peer_id=rendezvous_node.to_string(), err=?error, "failed to discover peers from rendezvous peer") - } + } => warn!(subject = "libp2p.rendezvous.client.discovered.err", + category = "handle_swarm_event", + peer_id=rendezvous_node.to_string(), + err=?error, + "failed to discover peers"), + rendezvous::client::Event::Registered { rendezvous_node, ttl, .. } => { debug!( + subject = "libp2p.rendezvous.client.registered", + category = "handle_swarm_event", peer_id = rendezvous_node.to_string(), ttl = ttl, "registered self with rendezvous node" @@ -344,7 +384,11 @@ async fn handle_swarm_event( error, .. } => { - error!(peer_id=rendezvous_node.to_string(), err=?error, "failed to register self with rendezvous peer") + warn!(subject = "libp2p.rendezvous.client.registered.err", + category = "handle_swarm_event", + peer_id=rendezvous_node.to_string(), + err=?error, + "failed to register self with rendezvous peer") } rendezvous::client::Event::Expired { peer } => { // re-discover records from peer @@ -373,14 +417,22 @@ async fn handle_swarm_event( SwarmEvent::Behaviour(ComposedEvent::RendezvousServer(rendezvous_server_event)) => { match rendezvous_server_event { rendezvous::server::Event::DiscoverServed { enquirer, .. } => debug!( + subject = "libp2p.rendezvous.server.discover", + category = "handle_swarm_event", peer_id = enquirer.to_string(), "served rendezvous discover request to peer" ), rendezvous::server::Event::DiscoverNotServed { enquirer, error } => { - warn!(peer_id=enquirer.to_string(), err=?error, "did not serve rendezvous discover request") + warn!(subject = "libp2p.rendezvous.server.discover.err", + category = "handle_swarm_event", + peer_id=enquirer.to_string(), + err=?error, + "did not serve rendezvous discover request") } rendezvous::server::Event::PeerRegistered { peer, .. } => { debug!( + subject = "libp2p.rendezvous.server.peer_registered", + category = "handle_swarm_event", peer_id = peer.to_string(), "registered peer through rendezvous" ) @@ -390,10 +442,17 @@ async fn handle_swarm_event( namespace, error, } => { - warn!(peer_id=peer.to_string(), err=?error, namespace=?namespace, "did not register peer with rendezvous") + debug!(subject = "libp2p.rendezvous.server.peer_registered.err", + category = "handle_swarm_event", + peer_id=peer.to_string(), + err=?error, + namespace=?namespace, + "did not register peer with rendezvous") } rendezvous::server::Event::RegistrationExpired(registration) => { debug!( + subject = "libp2p.rendezvous.server.registration_expired", + category = "handle_swarm_event", peer_id = registration.record.peer_id().to_string(), "rendezvous peer registration expired on server" ) @@ -409,10 +468,11 @@ async fn handle_swarm_event( } => { let bytes: Vec = message.data; match pubsub::Message::::try_from(bytes) { - // TODO: dont fail blindly if we get a non receipt message Ok(msg) => { let receipt = msg.payload; info!( + subject = "libp2p.gossipsub.recv", + category = "handle_swarm_event", peer_id = propagation_source.to_string(), message_id = message_id.to_string(), "message received on receipts topic: {}", @@ -439,16 +499,19 @@ async fn handle_swarm_event( }, ); } - Err(err) => info!(err=?err, "cannot handle incoming gossipsub message"), + Err(err) => debug!(subject = "libp2p.gossipsub.err", + category = "handle_swarm_event", + err=?err, + "cannot handle incoming gossipsub message"), } } - gossipsub::Event::Subscribed { peer_id, topic } => { - debug!( - peer_id = peer_id.to_string(), - topic = topic.to_string(), - "subscribed to topic over gossipsub" - ) - } + gossipsub::Event::Subscribed { peer_id, topic } => debug!( + subject = "libp2p.gossipsub.subscribed", + category = "handle_swarm_event", + peer_id = peer_id.to_string(), + topic = topic.to_string(), + "subscribed to topic over gossipsub" + ), _ => {} }, @@ -458,9 +521,11 @@ async fn handle_swarm_event( .. })) => { match result { - QueryResult::Bootstrap(Ok(BootstrapOk { peer, .. })) => { - debug!("successfully bootstrapped peer: {peer}") - } + QueryResult::Bootstrap(Ok(BootstrapOk { peer, .. })) => debug!( + subject = "libp2p.kad.bootstrap", + category = "handle_swarm_event", + "successfully bootstrapped peer: {peer}" + ), QueryResult::GetProviders(Ok(GetProvidersOk::FoundProviders { key: _, providers, @@ -488,7 +553,10 @@ async fn handle_swarm_event( } QueryResult::GetProviders(Err(err)) => { - warn!(err=?err, "error retrieving outbound query providers"); + warn!(subject = "libp2p.kad.get_providers.err", + category = "handle_swarm_event", + err=?err, + "error retrieving outbound query providers"); let Some((_, sender)) = event_handler.query_senders.remove(&id) else { return; @@ -502,8 +570,11 @@ async fn handle_swarm_event( } QueryResult::GetRecord(Ok(GetRecordOk::FoundRecord(peer_record))) => { debug!( + subject = "libp2p.kad.get_record.err", + category = "handle_swarm_event", "found record {:#?}, published by {:?}", - peer_record.record.key, peer_record.record.publisher + peer_record.record.key, + peer_record.record.publisher ); match peer_record.found_record() { Ok(event) => { @@ -516,8 +587,10 @@ async fn handle_swarm_event( } } Err(err) => { - warn!(err=?err, "error retrieving record"); - + warn!(subject = "libp2p.kad.get_record.err", + category = "handle_swarm_event", + err=?err, + "error retrieving record"); let Some((_, sender)) = event_handler.query_senders.remove(&id) else { return; }; @@ -530,7 +603,10 @@ async fn handle_swarm_event( } QueryResult::GetRecord(Ok(_)) => {} QueryResult::GetRecord(Err(err)) => { - warn!(err=?err, "error retrieving record"); + warn!(subject = "libp2p.kad.get_record.err", + category = "handle_swarm_event", + err=?err, + "error retrieving record"); // Upon an error, attempt to find the record on the DHT via // a provider if it's a Workflow/Info one. @@ -562,42 +638,67 @@ async fn handle_swarm_event( )))) .await; } else { - warn!("not a valid provider record tag: {capsule_tag}",) + warn!( + subject = "libp2p.kad.req_resp.err", + category = "handle_swarm_event", + "not a valid provider record tag: {capsule_tag}", + ) } } - None => { - info!("No provider found for outbound query {id:?}") - } + None => debug!( + subject = "libp2p.kad.req_resp.err", + category = "handle_swarm_event", + "No provider found for outbound query {id:?}" + ), } } - QueryResult::PutRecord(Ok(PutRecordOk { key })) => { - debug!("successfully put record {key:#?}"); - } - QueryResult::PutRecord(Err(err)) => { - warn!("error putting record: {err}") - } + QueryResult::PutRecord(Ok(PutRecordOk { key })) => debug!( + subject = "libp2p.kad.put_record", + category = "handle_swarm_event", + "successfully put record {key:#?}" + ), + QueryResult::PutRecord(Err(err)) => warn!( + subject = "libp2p.kad.put_record.err", + category = "handle_swarm_event", + err=?err, + "error putting record"), QueryResult::StartProviding(Ok(AddProviderOk { key })) => { // Currently, we don't send anything to the channel, // once they key is provided. let _ = event_handler.query_senders.remove(&id); - debug!("successfully providing {key:#?}"); + debug!( + subject = "libp2p.kad.provide_record", + category = "handle_swarm_event", + "successfully providing {key:#?}" + ); } QueryResult::StartProviding(Err(err)) => { // Currently, we don't send anything to the channel, // once they key is provided. let _ = event_handler.query_senders.remove(&id); - warn!("error providing key: {:#?}", err.key()); + warn!( + subject = "libp2p.kad.provide_record.err", + category = "handle_swarm_event", + "error providing key: {:#?}", + err.key() + ); } _ => {} } } SwarmEvent::Behaviour(ComposedEvent::Kademlia(kad::Event::InboundRequest { request })) => { - debug!("kademlia inbound request received {request:?}") + debug!( + subject = "libp2p.kad.inbound_request", + category = "handle_swarm_event", + "kademlia inbound request received {request:?}" + ) } SwarmEvent::Behaviour(ComposedEvent::Kademlia(kad::Event::RoutingUpdated { peer, .. })) => { debug!( + subject = "libp2p.kad.routing", + category = "handle_swarm_event", peer = peer.to_string(), "kademlia routing table updated with peer" ) @@ -646,7 +747,11 @@ async fn handle_swarm_event( } } Err(err) => { - warn!(err=?err, cid=?cid, "error retrieving workflow info"); + warn!(subject = "libp2p.req_resp.err", + category = "handle_swarm_event", + err=?err, + cid=?cid, + "error retrieving workflow info"); let _ = event_handler .swarm @@ -687,11 +792,11 @@ async fn handle_swarm_event( let _ = sender.send_async(ResponseEvent::Found(Ok(event))).await; } Err(err) => { - warn!( - err=?err, - cid = key_cid.as_str(), - "error returning capsule for request_id: {request_id}" - ); + warn!(subject = "libp2p.req_resp.resp.err", + category = "handle_swarm_event", + err=?err, + cid = key_cid.as_str(), + "error returning capsule for request_id: {request_id}"); let _ = sender.send_async(ResponseEvent::Found(Err(err))).await; } @@ -702,7 +807,9 @@ async fn handle_swarm_event( }, SwarmEvent::Behaviour(ComposedEvent::Mdns(mdns::Event::Discovered(list))) => { for (peer_id, multiaddr) in list { - info!( + debug!( + subject = "libp2p.mdns.discovered", + category = "handle_swarm_event", peer_id = peer_id.to_string(), addr = multiaddr.to_string(), "mDNS discovered a new peer" @@ -717,9 +824,10 @@ async fn handle_swarm_event( .build(), ); } else { - warn!( - peer_id = peer_id.to_string(), - "peer discovered by mDNS not dialed because max connected peers limit reached" + debug!(subject = "libp2p.mdns.discovered.err", + category = "handle_swarm_event", + peer_id = peer_id.to_string(), + "peer discovered by mDNS not dialed because max connected peers limit reached" ) } } @@ -729,13 +837,17 @@ async fn handle_swarm_event( if let Some(mdns) = behaviour.mdns.as_ref() { for (peer_id, multiaddr) in list { - info!( + debug!( + subject = "libp2p.mdns.expired", + category = "handle_swarm_event", peer_id = peer_id.to_string(), "mDNS discover peer has expired" ); if mdns.has_node(&peer_id) { behaviour.kademlia.remove_address(&peer_id, &multiaddr); debug!( + subject = "libp2p.mdns.expired", + category = "handle_swarm_event", peer_id = peer_id.to_string(), "removed peer address from kademlia table" ); @@ -747,8 +859,11 @@ async fn handle_swarm_event( let local_peer = *event_handler.swarm.local_peer_id(); info!( + subject = "libp2p.listen.addr", + category = "handle_swarm_event", peer_id = local_peer.to_string(), - "local node is listening on {}", address + "local node is listening on {}", + address ); #[cfg(feature = "websocket-notify")] @@ -765,7 +880,12 @@ async fn handle_swarm_event( SwarmEvent::ConnectionEstablished { peer_id, endpoint, .. } => { - debug!(peer_id=peer_id.to_string(), endpoint=?endpoint, "peer connection established"); + debug!(subject = "libp2p.conn.established", + category = "handle_swarm_event", + peer_id=peer_id.to_string(), + endpoint=?endpoint, + "peer connection established"); + // add peer to connected peers list event_handler .connections @@ -789,6 +909,8 @@ async fn handle_swarm_event( .. } => { debug!( + subject = "libp2p.conn.closed", + category = "handle_swarm_event", peer_id = peer_id.to_string(), "peer connection closed, cause: {cause:#?}, endpoint: {endpoint:#?}" ); @@ -801,7 +923,11 @@ async fn handle_swarm_event( } else { // TODO: We may want to check the multiadress without relying on // the peer ID. This would give more flexibility when configuring nodes. - warn!("Configured peer must include a peer ID: {multiaddr}"); + warn!( + subject = "libp2p.conn.closed", + category = "handle_swarm_event", + "Configured peer must include a peer ID: {multiaddr}" + ); true } }) { @@ -812,6 +938,8 @@ async fn handle_swarm_event( .remove_peer(&peer_id); debug!( + subject = "libp2p.kad.remove", + category = "handle_swarm_event", peer_id = peer_id.to_string(), "removed peer from kademlia table" ); @@ -832,11 +960,12 @@ async fn handle_swarm_event( peer_id, error, } => { - error!( - peer_id=peer_id.map(|p| p.to_string()).unwrap_or_default(), - err=?error, - connection_id=?connection_id, - "outgoing connection error" + warn!(subject = "libp2p.outgoing.err", + category = "handle_swarm_event", + peer_id=peer_id.map(|p| p.to_string()).unwrap_or_default(), + err=?error, + connection_id=?connection_id, + "outgoing connection error" ); #[cfg(feature = "websocket-notify")] @@ -855,13 +984,13 @@ async fn handle_swarm_event( send_back_addr, error, } => { - error!( - err=?error, - connection_id=?connection_id, - local_address=local_addr.to_string(), - remote_address=send_back_addr.to_string(), - "incoming connection error" - ); + warn!(subject = "libp2p.incoming.err", + category = "handle_swarm_event", + err=?error, + connection_id=?connection_id, + local_address=local_addr.to_string(), + remote_address=send_back_addr.to_string(), + "incoming connection error"); #[cfg(feature = "websocket-notify")] notification::emit_event( @@ -873,13 +1002,31 @@ async fn handle_swarm_event( ); } SwarmEvent::ListenerError { listener_id, error } => { - error!(err=?error, listener_id=?listener_id, "listener error") + error!(subject = "libp2p.listener.err", + category = "handle_swarm_event", + err=?error, + listener_id=?listener_id, + "listener error") } SwarmEvent::Dialing { peer_id, .. } => match peer_id { - Some(id) => debug!(peer_id = id.to_string(), "dialing peer"), - None => debug!("dialing an unknown peer"), + Some(id) => { + debug!( + subject = "libp2p.dialing", + category = "handle_swarm_event", + peer_id = id.to_string(), + "dialing peer" + ) + } + None => debug!( + subject = "libp2p.dialing", + category = "handle_swarm_event", + "dialing an unknown peer" + ), }, - e => debug!(e=?e, "uncaught event"), + e => debug!(subject = "libp2p.event", + category = "handle_swarm_event", + e=?e, + "uncaught event"), } } @@ -915,10 +1062,7 @@ fn decode_capsule(key_cid: Cid, value: &Vec) -> Result { Ok(ipld) => Err(anyhow!( "decode mismatch: expected an Ipld map, got {ipld:#?}", )), - Err(err) => { - warn!(error=?err, "error deserializing record value"); - Err(anyhow!("error deserializing record value")) - } + Err(err) => Err(anyhow!("error deserializing record value: {err}")), } } diff --git a/homestar-runtime/src/lib.rs b/homestar-runtime/src/lib.rs index e6f84323..b81b5364 100644 --- a/homestar-runtime/src/lib.rs +++ b/homestar-runtime/src/lib.rs @@ -17,6 +17,7 @@ //! [homestar-core]: homestar_core //! [homestar-wasm]: homestar_wasm +pub mod channel; pub mod cli; pub mod daemon; pub mod db; @@ -37,12 +38,14 @@ pub mod workflow; pub mod test_utils; pub use db::Db; -pub use event_handler::channel; pub(crate) mod libp2p; pub use logger::*; pub(crate) mod metrics; +#[allow(unused_imports)] +pub(crate) use event_handler::EventHandler; pub use receipt::{Receipt, RECEIPT_TAG, VERSION_KEY}; pub use runner::Runner; +pub(crate) use scheduler::TaskScheduler; pub use settings::Settings; pub(crate) use worker::Worker; pub use workflow::WORKFLOW_TAG; diff --git a/homestar-runtime/src/libp2p/mod.rs b/homestar-runtime/src/libp2p/mod.rs index 8254556f..c2ac9fe1 100644 --- a/homestar-runtime/src/libp2p/mod.rs +++ b/homestar-runtime/src/libp2p/mod.rs @@ -1 +1,3 @@ +//! [libp2p] utilities. + pub(crate) mod multiaddr; diff --git a/homestar-runtime/src/libp2p/multiaddr.rs b/homestar-runtime/src/libp2p/multiaddr.rs index 894d7008..12f43531 100644 --- a/homestar-runtime/src/libp2p/multiaddr.rs +++ b/homestar-runtime/src/libp2p/multiaddr.rs @@ -1,5 +1,7 @@ +/// Multiaddr extension methods. use libp2p::{multiaddr::Protocol, Multiaddr, PeerId}; +/// [Multiaddr] extension trait. pub(crate) trait MultiaddrExt { fn peer_id(&self) -> Option; } diff --git a/homestar-runtime/src/main.rs b/homestar-runtime/src/main.rs index f9fce8f7..9639b3aa 100644 --- a/homestar-runtime/src/main.rs +++ b/homestar-runtime/src/main.rs @@ -37,7 +37,7 @@ fn main() -> Result<()> { info!( subject = "settings", - category = "homestar_init", + category = "homestar.init", "starting with settings: {:?}", settings, ); @@ -47,7 +47,7 @@ fn main() -> Result<()> { info!( subject = "database", - category = "homestar_init", + category = "homestar.init", "starting with database: {}", Db::url().expect("database url to be provided"), ); diff --git a/homestar-runtime/src/metrics.rs b/homestar-runtime/src/metrics.rs index e0b308e9..d5a3623a 100644 --- a/homestar-runtime/src/metrics.rs +++ b/homestar-runtime/src/metrics.rs @@ -11,6 +11,7 @@ mod exporter; mod node; /// Start metrics collection and setup scrape endpoint. +/// Also, spawn a task to collect process metrics at a regular interval. #[cfg(feature = "monitoring")] pub(crate) async fn start( monitor_settings: &settings::Monitoring, @@ -27,6 +28,7 @@ pub(crate) async fn start( Ok(metrics_hdl) } +/// Start metrics collection and setup scrape endpoint. #[cfg(not(feature = "monitoring"))] pub(crate) async fn start(network_settings: &settings::Network) -> Result { let metrics_hdl = exporter::setup_metrics_recorder(network_settings)?; diff --git a/homestar-runtime/src/network/error.rs b/homestar-runtime/src/network/error.rs index 10f18416..1e0319f1 100644 --- a/homestar-runtime/src/network/error.rs +++ b/homestar-runtime/src/network/error.rs @@ -1,3 +1,5 @@ +//! # Error types centered around the networking. + #[derive(thiserror::Error, Debug)] pub(crate) enum Error { #[error("pubsub error: {0}")] diff --git a/homestar-runtime/src/network/mod.rs b/homestar-runtime/src/network/mod.rs index 01a55a31..e47cb79e 100644 --- a/homestar-runtime/src/network/mod.rs +++ b/homestar-runtime/src/network/mod.rs @@ -1,9 +1,8 @@ -//! [libp2p], multi-use [http] and [websocket] server, and [ipfs] networking +//! [libp2p], multi-use [HTTP] and [WebSocket] server, and [ipfs] networking //! interfaces. //! -//! [libp2p]: libp2p -//! [http]: jsonrpsee::server -//! [websocket]: jsonrpsee::server +//! [HTTP]: jsonrpsee::server +//! [WebSocket]: jsonrpsee::server //! [ipfs]: ipfs_api pub(crate) mod error; diff --git a/homestar-runtime/src/network/pubsub/message.rs b/homestar-runtime/src/network/pubsub/message.rs index 841212b8..8153af05 100644 --- a/homestar-runtime/src/network/pubsub/message.rs +++ b/homestar-runtime/src/network/pubsub/message.rs @@ -1,3 +1,7 @@ +//! [Message] type for messages transmitted over [gossipsub]. +//! +//! [gossipsub]: libp2p::gossipsub + use anyhow::{anyhow, Result}; use homestar_core::workflow::Nonce; use libipld::{self, cbor::DagCborCodec, prelude::Codec, serde::from_ipld, Ipld}; diff --git a/homestar-runtime/src/network/rpc.rs b/homestar-runtime/src/network/rpc.rs index 667f1d9c..f260634f 100644 --- a/homestar-runtime/src/network/rpc.rs +++ b/homestar-runtime/src/network/rpc.rs @@ -1,4 +1,4 @@ -//! RPC server implementation. +//! CLI-focused RPC server implementation. use crate::{ channel::{AsyncChannel, AsyncChannelReceiver, AsyncChannelSender}, @@ -138,7 +138,9 @@ impl Interface for ServerHandler { }, _ = time::sleep_until(now + self.timeout) => { let s = format!("server timeout of {} ms reached", self.timeout.as_millis()); - info!("{s}"); + info!(subject = "rpc.timeout", + category = "rpc", + "{s}"); Err(Error::FailureToReceiveOnChannel(s)) } @@ -180,7 +182,12 @@ impl Server { tarpc::serde_transport::tcp::listen(self.addr, MessagePack::default).await?; listener.config_mut().max_frame_length(usize::MAX); - info!("RPC server listening on {}", self.addr); + info!( + subject = "rpc.spawn", + category = "rpc", + "RPC server listening on {}", + self.addr + ); // setup valved listener for cancellation let (exit, incoming) = Valved::new(listener); @@ -203,11 +210,16 @@ impl Server { select! { Ok(ServerMessage::GracefulShutdown(tx)) = self.receiver.recv_async() => { - info!("RPC server shutting down"); + info!(subject = "shutdown", + category = "homestar.shutdown", + "RPC server shutting down"); drop(exit); let _ = tx.send_async(()).await; } - _ = fut => warn!("RPC server exited unexpectedly"), + _ = fut => + warn!(subject = "rpc.spawn.err", + category = "rpc", + "RPC server exited unexpectedly"), } }); diff --git a/homestar-runtime/src/network/swarm.rs b/homestar-runtime/src/network/swarm.rs index ce638854..22ab6b2a 100644 --- a/homestar-runtime/src/network/swarm.rs +++ b/homestar-runtime/src/network/swarm.rs @@ -1,9 +1,6 @@ -#![allow(missing_docs)] - //! Sets up a [libp2p] [Swarm], containing the state of the network and the way //! it should behave. //! -//! [libp2p]: libp2p //! [Swarm]: libp2p::Swarm use crate::{ @@ -11,6 +8,7 @@ use crate::{ settings, Receipt, RECEIPT_TAG, WORKFLOW_TAG, }; use anyhow::{Context, Result}; +use const_format::formatcp; use enum_assoc::Assoc; use faststr::FastStr; use libp2p::{ @@ -32,7 +30,10 @@ use serde::{Deserialize, Serialize}; use std::fmt; use tracing::{info, warn}; -pub(crate) const HOMESTAR_PROTOCOL_VER: &str = "homestar/0.0.1"; +/// Homestar protocol version, shared among peers, tied to the homestar version. +pub(crate) const HOMESTAR_PROTOCOL_VER: &str = formatcp!("homestar/{VERSION}"); + +const VERSION: &str = env!("CARGO_PKG_VERSION"); /// Build a new [Swarm] with a given transport and a tokio executor. pub(crate) async fn new(settings: &settings::Network) -> Result> { @@ -42,7 +43,12 @@ pub(crate) async fn new(settings: &settings::Network) -> Result)), /// Acknowledgement of a [Workflow] run. AckWorkflow((Cid, FastStr)), - /// TODO + /// Message sent to the [Runner] to gather node information from the [EventHandler]. + /// + /// [Runner]: crate::Runner + /// [EventHandler]: crate::EventHandler GetNodeInfo, - /// TODO + /// Acknowledgement of a [Message::GetNodeInfo] request, receiving static and dynamic + /// node information. AckNodeInfo((StaticNodeInfo, DynamicNodeInfo)), } -/// WebSocket server fields. +/// Server fields. #[cfg(feature = "websocket-notify")] #[derive(Clone, Debug)] pub(crate) struct Server { - /// Address of the websocket server. + /// Address of the server. addr: SocketAddr, - /// TODO + /// Message buffer capacity for the server. capacity: usize, - /// TODO + /// Message sender for broadcasting internal events to clients connected to + /// to the server. evt_notifier: Notifier, - /// Message sender for broadcasting to clients connected to the - /// websocket server. + /// Message sender for broadcasting workflow-related events to clients + /// connected to to the server. workflow_msg_notifier: Notifier, - /// Receiver timeout for the websocket server. + /// Receiver timeout for the server when communicating with the [Runner]. + /// + /// [Runner]: crate::Runner receiver_timeout: Duration, - /// TODO + /// General timeout for the server. webserver_timeout: Duration, } +/// Server fields. #[cfg(not(feature = "websocket-notify"))] #[derive(Clone, Debug)] pub(crate) struct Server { - /// Address of the websocket server. + /// Address of the server. addr: SocketAddr, - /// TODO + /// Message buffer capacity for the server. capacity: usize, - /// Receiver timeout for the websocket server. + /// Receiver timeout for the server when communicating with the [Runner]. + /// + /// [Runner]: crate::Runner receiver_timeout: Duration, - /// TODO + /// General timeout for the server. webserver_timeout: Duration, } impl Server { /// Setup bounded, MPMC channel for runtime to send and received messages - /// through the websocket connection(s). + /// through the WebSocket connection(s). #[cfg(feature = "websocket-notify")] fn setup_channel( capacity: usize, @@ -111,6 +120,8 @@ impl Server { broadcast::channel(capacity) } + /// Set up a new [Server] instance, which acts as both a + /// WebSocket and HTTP server. #[cfg(feature = "websocket-notify")] pub(crate) fn new(settings: &settings::Webserver) -> Result { let (evt_sender, _receiver) = Self::setup_channel(settings.websocket_capacity); @@ -136,6 +147,7 @@ impl Server { }) } + /// Set up a new [Server] instance, which only acts as an HTTP server. #[cfg(not(feature = "websocket-notify"))] pub(crate) fn new(settings: &settings::Webserver) -> Result { let host = IpAddr::from_str(&settings.host.to_string())?; @@ -157,7 +169,7 @@ impl Server { }) } - /// Start the websocket server. + /// Instantiates the [JsonRpc] module, and starts the server. #[cfg(feature = "websocket-notify")] pub(crate) async fn start( &self, @@ -176,7 +188,7 @@ impl Server { self.start_inner(module).await } - /// Start the websocket server. + /// Instantiates the [JsonRpc] module, and starts the server. #[cfg(not(feature = "websocket-notify"))] pub(crate) async fn start( &self, @@ -192,23 +204,29 @@ impl Server { self.start_inner(module).await } - /// Get websocket message sender for broadcasting messages to websocket + /// Return the WebSocket event sender for broadcasting messages to connected /// clients. #[cfg(feature = "websocket-notify")] pub(crate) fn evt_notifier(&self) -> Notifier { self.evt_notifier.clone() } - /// Get websocket message sender for broadcasting messages to websocket - /// clients. + /// Get WebSocket message sender for broadcasting workflow-related messages + /// to connected clients. #[cfg(feature = "websocket-notify")] pub(crate) fn workflow_msg_notifier(&self) -> Notifier { self.workflow_msg_notifier.clone() } + /// Shared start logic for both WebSocket and HTTP servers. async fn start_inner(&self, module: JsonRpc) -> Result { let addr = self.addr; - info!("webserver listening on {}", addr); + info!( + subject = "webserver.start", + category = "webserver", + "webserver listening on {}", + addr + ); let cors = CorsLayer::new() // Allow `POST` when accessing the resource diff --git a/homestar-runtime/src/network/webserver/listener.rs b/homestar-runtime/src/network/webserver/listener.rs index 47a37c7a..9423ce20 100644 --- a/homestar-runtime/src/network/webserver/listener.rs +++ b/homestar-runtime/src/network/webserver/listener.rs @@ -1,3 +1,5 @@ +//! Listener for incoming requests types. + use faststr::FastStr; use homestar_core::{ipld::DagJson, Workflow}; use homestar_wasm::io::Arg; @@ -5,7 +7,7 @@ use names::{Generator, Name}; use serde::{de, Deserialize, Deserializer, Serialize}; use serde_json::value::RawValue; -/// A [Workflow] run command via a websocket channel. +/// A [Workflow] run command via a WebSocket channel. /// /// Note: We leverage the [RawValue] type in order to use our [DagJson] /// implementation, which is not a direct [Deserialize] implementation. diff --git a/homestar-runtime/src/network/webserver/notifier.rs b/homestar-runtime/src/network/webserver/notifier.rs index dc7abd6a..b1ad4f86 100644 --- a/homestar-runtime/src/network/webserver/notifier.rs +++ b/homestar-runtime/src/network/webserver/notifier.rs @@ -6,7 +6,7 @@ use libipld::Cid; use std::{fmt, sync::Arc}; use tokio::sync::broadcast; -/// Type-wrapper for websocket sender. +/// Type-wrapper for WebSocket sender. #[derive(Debug)] pub(crate) struct Notifier(Arc>); @@ -37,7 +37,7 @@ where self.0 } - /// Send a message to all connected websocket clients. + /// Send a message to all connected WebSocket clients. pub(crate) fn notify(&self, msg: T) -> Result<()> { let _ = self.0.send(msg)?; Ok(()) @@ -52,7 +52,7 @@ pub(crate) enum SubscriptionTyp { Cid(Cid), } -/// A header for a message to be sent to a websocket client. +/// A header for a message to be sent to a WebSocket client. #[derive(Debug, Clone)] pub(crate) struct Header { pub(crate) subscription: SubscriptionTyp, @@ -69,7 +69,7 @@ impl Header { } } -/// A message to be sent to a websocket client, with a header and payload. +/// A message to be sent to a WebSocket client, with a header and payload. #[derive(Debug, Clone)] pub(crate) struct Message { pub(crate) header: Header, @@ -82,13 +82,13 @@ impl Message { Self { header, payload } } - /// TODO + /// Get a reference to the [Header] of a [Message]. #[allow(dead_code)] pub(crate) fn header(&self) -> &Header { &self.header } - /// TODO + /// Get a reference to the payload of a [Message]. #[allow(dead_code)] pub(crate) fn payload(&self) -> &[u8] { &self.payload diff --git a/homestar-runtime/src/network/webserver/rpc.rs b/homestar-runtime/src/network/webserver/rpc.rs index c56b3c65..51b72eb8 100644 --- a/homestar-runtime/src/network/webserver/rpc.rs +++ b/homestar-runtime/src/network/webserver/rpc.rs @@ -1,3 +1,5 @@ +//! JSON-RPC module for registering methods and subscriptions. + #[cfg(feature = "websocket-notify")] use super::notifier::{self, Header, Notifier, SubscriptionTyp}; #[allow(unused_imports)] @@ -34,10 +36,10 @@ use tokio::sync::oneshot; use tokio::{runtime::Handle, select}; #[cfg(feature = "websocket-notify")] use tokio_stream::wrappers::BroadcastStream; -#[allow(unused_imports)] -use tracing::warn; #[cfg(feature = "websocket-notify")] -use tracing::{debug, error, info}; +use tracing::debug; +#[allow(unused_imports)] +use tracing::{error, warn}; /// Health endpoint. pub(crate) const HEALTH_ENDPOINT: &str = "health"; @@ -56,7 +58,7 @@ pub(crate) const SUBSCRIBE_NETWORK_EVENTS_ENDPOINT: &str = "subscribe_network_ev #[cfg(feature = "websocket-notify")] pub(crate) const UNSUBSCRIBE_NETWORK_EVENTS_ENDPOINT: &str = "unsubscribe_network_events"; -/// TODO +/// Context for RPC methods. #[cfg(feature = "websocket-notify")] pub(crate) struct Context { metrics_hdl: PrometheusHandle, @@ -67,7 +69,7 @@ pub(crate) struct Context { workflow_listeners: Arc, (Cid, FastStr)>>, } -/// TODO +/// Context for RPC methods. #[allow(dead_code)] #[cfg(not(feature = "websocket-notify"))] pub(crate) struct Context { @@ -77,7 +79,7 @@ pub(crate) struct Context { } impl Context { - /// TODO + /// Create a new [Context] instance. #[cfg(feature = "websocket-notify")] #[cfg_attr(docsrs, doc(cfg(feature = "websocket-notify")))] pub(crate) fn new( @@ -97,7 +99,7 @@ impl Context { } } - /// TODO + /// Create a new [Context] instance. #[cfg(not(feature = "websocket-notify"))] pub(crate) fn new( metrics_hdl: PrometheusHandle, @@ -150,7 +152,12 @@ impl JsonRpc { Ok(serde_json::json!({ "healthy": true, "nodeInfo": { "static": static_info, "dynamic": dyn_info}})) } else { - warn!(sub = HEALTH_ENDPOINT, "did not acknowledge message in time"); + error!( + subject = "call.health", + category = "jsonrpc.call", + sub = HEALTH_ENDPOINT, + "did not acknowledge message in time" + ); Err(internal_err("failed to get node information".to_string())) } })?; @@ -229,7 +236,9 @@ impl JsonRpc { let stream = BroadcastStream::new(rx); Self::handle_workflow_subscription(sink, stream, ctx).await?; } else { - warn!( + error!( + subject = "subscription.workflow.err", + category = "jsonrpc.subscription", sub = SUBSCRIBE_RUN_WORKFLOW_ENDPOINT, workflow_name = name.to_string(), "did not acknowledge message in time" @@ -243,7 +252,10 @@ impl JsonRpc { } } Err(err) => { - warn!("failed to parse run workflow params: {}", err); + warn!(subject = "subscription.workflow.err", + category = "jsonrpc.subscription", + err=?err, + "failed to parse run workflow params"); let _ = pending.reject(err).await; } } @@ -278,7 +290,10 @@ impl JsonRpc { })) if evt == subscription_type => payload, Some(Ok(_)) => continue, Some(Err(err)) => { - error!("subscription stream error: {}", err); + error!(subject = "subscription.event.err", + category = "jsonrpc.subscription", + err=?err, + "subscription stream error"); break Err(err.into()); } None => break Ok(()), @@ -290,7 +305,9 @@ impl JsonRpc { break Err(anyhow!("subscription sink closed")); } Err(TrySendError::Full(_)) => { - info!("subscription sink full"); + error!(subject = "subscription.event.err", + category = "jsonrpc.subscription", + "subscription sink full"); } } } @@ -326,9 +343,11 @@ impl JsonRpc { .and_then(|v| { let (v_cid, v_name) = v.value(); if v_cid == &cid && (Some(v_name) == ident.as_ref() || ident.is_none()) { - debug!(cid = cid.to_string(), - ident = ident.clone().unwrap_or( - "undefined".into()).to_string(), "received message"); + debug!( + subject = "subscription.workflow", + category = "jsonrpc.subscription", + cid = cid.to_string(), + ident = ident.clone().unwrap_or("undefined".into()).to_string(), "received message"); Some(payload) } else { None @@ -359,7 +378,9 @@ impl JsonRpc { break Err(anyhow!("subscription sink closed")); } Err(TrySendError::Full(_)) => { - info!("subscription sink full"); + error!(subject = "subscription.workflow.err", + category = "jsonrpc.subscription", + "subscription sink full"); } } } diff --git a/homestar-runtime/src/receipt.rs b/homestar-runtime/src/receipt.rs index bf65fa35..c8dcd1f1 100644 --- a/homestar-runtime/src/receipt.rs +++ b/homestar-runtime/src/receipt.rs @@ -1,4 +1,7 @@ -//! Output of an invocation, referenced by its invocation pointer. +//! Runtime, extended representation of a [Receipt] for [Invocation]s and storage. +//! +//! [Receipt]: homestar_core::workflow::Receipt +//! [Invocation]: homestar_core::workflow::Invocation use anyhow::anyhow; use diesel::{ diff --git a/homestar-runtime/src/runner.rs b/homestar-runtime/src/runner.rs index ed6217a6..7c5470ab 100644 --- a/homestar-runtime/src/runner.rs +++ b/homestar-runtime/src/runner.rs @@ -38,7 +38,7 @@ use tokio::{ time, }; use tokio_util::time::{delay_queue, DelayQueue}; -use tracing::{error, info, warn}; +use tracing::{debug, error, info, warn}; mod error; pub(crate) mod file; @@ -74,13 +74,13 @@ pub(crate) type RpcReceiver = AsyncChannelReceiver<( Option>, )>; -/// [AsyncChannelSender] for sending messages websocket server clients. +/// [AsyncChannelSender] for sending messages WebSocket server clients. pub(crate) type WsSender = AsyncChannelSender<( webserver::Message, Option>, )>; -/// [AsyncChannelReceiver] for receiving messages from websocket server clients. +/// [AsyncChannelReceiver] for receiving messages from WebSocket server clients. pub(crate) type WsReceiver = AsyncChannelReceiver<( webserver::Message, Option>, @@ -129,7 +129,7 @@ impl Runner { } /// MPSC channel for sending and receiving messages through to/from - /// websocket server clients. + /// WebSocket server clients. pub(crate) fn setup_ws_mpsc_channel(capacity: usize) -> (WsSender, WsReceiver) { AsyncChannel::with(capacity) } @@ -262,21 +262,27 @@ impl Runner { Ok(ControlFlow::Break(())) => break now.elapsed(), Ok(ControlFlow::Continue(rpc::ServerMessage::Skip)) => {}, Ok(ControlFlow::Continue(msg @ rpc::ServerMessage::RunAck(_))) => { - info!("sending message to rpc server"); + debug!(subject = "rpc.ack", + category = "rpc", + "sending message to rpc server"); let _ = oneshot_tx.send_async(msg).await; }, Err(err) => { - error!(err=?err, "error handling rpc message"); + error!(subject = "rpc.err", + category = "rpc", + err=?err, + "error handling rpc message"); let _ = oneshot_tx.send_async(rpc::ServerMessage::RunErr(err.into())).await; }, _ => {} } } Ok(msg) = ws_receiver.recv_async() => { - println!("ws message: {:?}", msg); match msg { (webserver::Message::RunWorkflow((name, workflow)), Some(oneshot_tx)) => { - info!("running workflow: {}", name); + info!(subject = "workflow", + category = "workflow.run", + "running workflow: {}", name); // TODO: Parse this from the workflow data itself. let workflow_settings = workflow::Settings::default(); match self.run_worker( @@ -287,18 +293,25 @@ impl Runner { db.clone(), ).await { Ok(data) => { - info!("sending message to rpc server"); + debug!(subject = "jsonrpc.ack", + category = "jsonrpc", + "sending message to jsonrpc server"); let _ = oneshot_tx.send_async(webserver::Message::AckWorkflow((data.info.cid, data.name))).await; } Err(err) => { - error!(err=?err, "error handling ws message"); + error!(subject = "jsonrpc.err", + category = "jsonrpc", + err=?err, + "error handling ws message"); let _ = oneshot_tx.send_async(webserver::Message::RunErr(err.into())).await; } } } (webserver::Message::GetNodeInfo, Some(oneshot_tx)) => { - info!("getting node info"); + debug!(subject = "jsonrpc.nodeinfo", + category = "jsonrpc", + "getting node info"); let (tx, rx) = AsyncChannel::oneshot(); let _ = self.event_sender.send_async(Event::GetListeners(tx)).await; let dyn_node_info = if let Ok(listeners) = rx.recv_deadline(Instant::now() + self.settings.node.network.webserver.timeout) { @@ -331,12 +344,16 @@ impl Runner { Err(_) => Poll::Pending, } ) => { - info!("worker expired, aborting"); + info!(subject = "worker.expired", + category = "worker", + "worker expired, aborting"); let _ = self.abort_worker(*expired.get_ref()); }, // Handle shutdown signal. _ = Self::shutdown_signal() => { - info!("gracefully shutting down runner"); + info!(subject = "shutdown", + category = "homestar.shutdown", + "gracefully shutting down runner"); let now = time::Instant::now(); let drain_timeout = now + shutdown_timeout; @@ -347,7 +364,9 @@ impl Runner { }, // Force shutdown upon drain timeout. _ = time::sleep_until(drain_timeout) => { - info!("shutdown timeout reached, shutting down runner anyway"); + info!(subject = "shutdown", + category = "homestar.shutdown", + "shutdown timeout reached, shutting down runner anyway"); break now.elapsed(); } } @@ -360,7 +379,11 @@ impl Runner { if shutdown_time_left < shutdown_timeout { self.runtime .shutdown_timeout(shutdown_timeout - shutdown_time_left); - info!("runner shutdown complete"); + info!( + subject = "shutdown", + category = "homestar.shutdown", + "runner shutdown complete" + ); } Ok(()) @@ -488,9 +511,18 @@ impl Runner { let mut sigterm = signal(SignalKind::terminate())?; select! { - _ = tokio::signal::ctrl_c() => info!("CTRL-C received, shutting down"), - _ = sigint.recv() => info!("SIGINT received, shutting down"), - _ = sigterm.recv() => info!("SIGTERM received, shutting down"), + _ = tokio::signal::ctrl_c() => + info!(subject = "shutdown", + category = "homestar.shutdown", + "CTRL-C received, shutting down"), + _ = sigint.recv() => + info!(subject = "shutdown", + category = "homestar.shutdown", + "SIGINT received, shutting down"), + _ = sigterm.recv() => + info!(subject = "shutdown", + category = "homestar.shutdown", + "SIGTERM received, shutting down"), } Ok(()) } @@ -503,10 +535,22 @@ impl Runner { let mut sighup = windows::ctrl_break()?; select! { - _ = tokio::signal::ctrl_c() => info!("CTRL-C received, shutting down"), - _ = sigint.recv() => info!("SIGINT received, shutting down"), - _ = sigterm.recv() => info!("SIGTERM received, shutting down"), - _ = sighup.recv() => info!("SIGHUP received, shutting down") + _ = tokio::signal::ctrl_c() => + info!(subject = "shutdown", + category = "homestar.shutdown", + "CTRL-C received, shutting down"), + _ = sigint.recv() => + info!(subject = "shutdown", + category = "homestar.shutdown", + "SIGINT received, shutting down"), + _ = sigterm.recv() => + info!(subject = "shutdown", + category = "homestar.shutdown", + "SIGTERM received, shutting down"), + _ = sighup.recv() => + info!(subject = "shutdown", + category = "homestar.shutdown", + "SIGHUP received, shutting down") } Ok(()) } @@ -526,7 +570,11 @@ impl Runner { .await; let _ = shutdown_receiver; - info!("shutting down webserver"); + info!( + subject = "shutdown", + category = "homestar.shutdown", + "shutting down webserver" + ); let _ = ws_hdl.stop(); ws_hdl.clone().stopped().await; @@ -553,7 +601,11 @@ impl Runner { ) -> Result> { match msg { rpc::ServerMessage::ShutdownCmd => { - info!("RPC shutdown signal received, shutting down runner"); + info!( + subject = "rpc.command", + category = "rpc", + "RPC shutdown signal received, shutting down runner" + ); let drain_timeout = now + self.settings.node.shutdown_timeout; select! { // we can unwrap here b/c we know we have a sender based @@ -562,7 +614,9 @@ impl Runner { Ok(ControlFlow::Break(())) }, _ = time::sleep_until(drain_timeout) => { - info!("shutdown timeout reached, shutting down runner anyway"); + info!(subject = "shutdown", + category = "homestar.shutdown", + "shutdown timeout reached, shutting down runner anyway"); Ok(ControlFlow::Break(())) } } @@ -582,7 +636,12 @@ impl Runner { )))) } msg => { - warn!("received unexpected message: {:?}", msg); + warn!( + subject = "rpc.command", + category = "rpc", + "received unexpected message: {:?}", + msg + ); Ok(ControlFlow::Continue(rpc::ServerMessage::Skip)) } } @@ -619,8 +678,11 @@ impl Runner { // Spawn worker, which initializees the scheduler and runs // the workflow. info!( + subject = "workflow.run", + category = "workflow", cid = worker.workflow_info.cid.to_string(), - "running workflow with settings: {:#?}", worker.workflow_settings + "running workflow with settings: {:#?}", + worker.workflow_settings ); // Provide workflow to network. diff --git a/homestar-runtime/src/runner/nodeinfo.rs b/homestar-runtime/src/runner/nodeinfo.rs index 58e9484a..aa14be84 100644 --- a/homestar-runtime/src/runner/nodeinfo.rs +++ b/homestar-runtime/src/runner/nodeinfo.rs @@ -1,33 +1,38 @@ +//! Node information. + use libp2p::{Multiaddr, PeerId}; use serde::{Deserialize, Serialize}; -/// TODO +/// Static node information available at startup. #[derive(Debug, Clone, Serialize, Deserialize)] pub(crate) struct StaticNodeInfo { + /// The [PeerId] of a node. pub(crate) peer_id: PeerId, } impl StaticNodeInfo { - /// TODO + /// Create an instance of [StaticNodeInfo]. pub(crate) fn new(peer_id: PeerId) -> Self { Self { peer_id } } - /// TODO + /// Get a reference to the [PeerId] of a node. #[allow(dead_code)] pub(crate) fn peer_id(&self) -> &PeerId { &self.peer_id } } -/// TODO +/// Dynamic node information available through events +/// at runtime. #[derive(Debug, Clone, Serialize, Deserialize)] pub(crate) struct DynamicNodeInfo { + /// Listeners for the node. pub(crate) listeners: Vec, } impl DynamicNodeInfo { - /// TODO + /// Create an instance of [DynamicNodeInfo]. pub(crate) fn new(listeners: Vec) -> Self { Self { listeners } } diff --git a/homestar-runtime/src/runner/response.rs b/homestar-runtime/src/runner/response.rs index c6b8b781..8a5928c4 100644 --- a/homestar-runtime/src/runner/response.rs +++ b/homestar-runtime/src/runner/response.rs @@ -1,5 +1,5 @@ //! Responses for display/return to the user for -//! Client requests. +//! client requests. use crate::{ cli::show::{self, ApplyStyle}, diff --git a/homestar-runtime/src/scheduler.rs b/homestar-runtime/src/scheduler.rs index 1970bc0a..de364ee1 100644 --- a/homestar-runtime/src/scheduler.rs +++ b/homestar-runtime/src/scheduler.rs @@ -19,7 +19,7 @@ use indexmap::IndexMap; use libipld::Cid; use std::{ops::ControlFlow, str::FromStr, sync::Arc}; use tokio::sync::RwLock; -use tracing::info; +use tracing::debug; /// Type alias for a [Dag] set of batched nodes. /// @@ -150,7 +150,11 @@ impl<'a> TaskScheduler<'a> { } } Err(_) => { - info!("receipt not available in the database"); + debug!( + subject = "receipt.db.check", + category = "scheduler.run", + "receipt not available in the database" + ); continue; } } @@ -199,7 +203,9 @@ impl<'a> TaskScheduler<'a> { } } - /// TODO + /// Get the number of tasks that have already ran in the [Workflow]. + /// + /// [Workflow]: homestar_core::Workflow #[allow(dead_code)] pub(crate) fn ran_length(&self) -> usize { self.ran @@ -208,7 +214,9 @@ impl<'a> TaskScheduler<'a> { .unwrap_or_default() } - /// TODO + /// Get the number of tasks left to run in the [Workflow]. + /// + /// [Workflow]: homestar_core::Workflow #[allow(dead_code)] pub(crate) fn run_length(&self) -> usize { self.run.iter().flatten().collect::>().len() diff --git a/homestar-runtime/src/settings.rs b/homestar-runtime/src/settings.rs index f82c97ea..fb06d9a7 100644 --- a/homestar-runtime/src/settings.rs +++ b/homestar-runtime/src/settings.rs @@ -1,4 +1,4 @@ -//! Settings / Configuration. +//! General runtime settings / configuration. use config::{Config, ConfigError, Environment, File}; use http::Uri; @@ -162,7 +162,7 @@ pub(crate) struct Webserver { pub(crate) host: Uri, /// Webserver-server port. pub(crate) port: u16, - /// TODO + /// Webserver timeout. #[serde_as(as = "DurationSeconds")] pub(crate) timeout: Duration, /// Number of *bounded* clients to send messages to, used for a diff --git a/homestar-runtime/src/settings/libp2p_config.rs b/homestar-runtime/src/settings/libp2p_config.rs index b561cb4b..3df3aba9 100644 --- a/homestar-runtime/src/settings/libp2p_config.rs +++ b/homestar-runtime/src/settings/libp2p_config.rs @@ -1,4 +1,4 @@ -//! Libp2p setttings +//! [libp2p] configuration. use http::Uri; use serde::{Deserialize, Serialize}; diff --git a/homestar-runtime/src/settings/pubkey_config.rs b/homestar-runtime/src/settings/pubkey_config.rs index 171dcb87..a8c87ec6 100644 --- a/homestar-runtime/src/settings/pubkey_config.rs +++ b/homestar-runtime/src/settings/pubkey_config.rs @@ -1,3 +1,5 @@ +//! Pubkey configuration. + use anyhow::{anyhow, Context}; use libp2p::{identity, identity::secp256k1}; use rand::{Rng, SeedableRng}; @@ -54,7 +56,11 @@ impl PubkeyConfig { pub(crate) fn keypair(&self) -> anyhow::Result { match self { PubkeyConfig::Random => { - info!("generating random ed25519 key"); + info!( + subject = "pubkey_config.random", + category = "pubkey_config", + "generating random ed25519 key" + ); Ok(identity::Keypair::generate_ed25519()) } PubkeyConfig::GenerateFromSeed(RNGSeed { key_type, seed }) => { @@ -64,14 +70,22 @@ impl PubkeyConfig { match key_type { KeyType::Ed25519 => { - info!("generating random ed25519 key from seed"); + info!( + subject = "pubkey_config.random_seed.ed25519", + category = "pubkey_config", + "generating random ed25519 key from seed" + ); identity::Keypair::ed25519_from_bytes(new_key).map_err(|e| { anyhow!("failed to generate ed25519 key from random: {:?}", e) }) } KeyType::Secp256k1 => { - info!("generating random secp256k1 key from seed"); + info!( + subject = "pubkey_config.random_seed.secp256k1", + category = "pubkey_config", + "generating random secp256k1 key from seed" + ); let sk = secp256k1::SecretKey::try_from_bytes(&mut new_key).map_err(|e| { @@ -94,7 +108,12 @@ impl PubkeyConfig { KeyType::Ed25519 => { const PEM_HEADER: &str = "PRIVATE KEY"; - info!("importing ed25519 key from: {}", path.display()); + info!( + subject = "pubkey_config.path.ed25519", + category = "pubkey_config", + "importing ed25519 key from: {}", + path.display() + ); let (tag, mut key) = sec1::der::pem::decode_vec(&buf) .map_err(|e| anyhow!("key file must be PEM formatted: {:#?}", e))?; @@ -107,7 +126,12 @@ impl PubkeyConfig { .with_context(|| "imported key material was invalid for ed25519") } KeyType::Secp256k1 => { - info!("importing secp256k1 key from: {}", path.display()); + info!( + subject = "pubkey_config.path.secp256k1", + category = "pubkey_config", + "importing secp256k1 key from: {}", + path.display() + ); let sk = match path.extension().and_then(|ext| ext.to_str()) { Some("der") => sec1::EcPrivateKey::from_der(buf.as_slice()).map_err(|e| anyhow!("failed to parse DER encoded secp256k1 key: {e:#?}")), diff --git a/homestar-runtime/src/tasks.rs b/homestar-runtime/src/tasks.rs index caf8ca77..c054c877 100644 --- a/homestar-runtime/src/tasks.rs +++ b/homestar-runtime/src/tasks.rs @@ -1,5 +1,3 @@ -#![allow(missing_docs)] - //! Module for working with task-types and task-specific functionality. use anyhow::{anyhow, Result}; diff --git a/homestar-runtime/src/tasks/fetch.rs b/homestar-runtime/src/tasks/fetch.rs index 87922afd..d56df0f5 100644 --- a/homestar-runtime/src/tasks/fetch.rs +++ b/homestar-runtime/src/tasks/fetch.rs @@ -14,7 +14,7 @@ use std::sync::Arc; pub(crate) struct Fetch; #[cfg(any(test, feature = "test-utils"))] -const WASM_CID: &str = "bafybeiczefaiu7464ehupezpzulnti5jvcwnvdalqrdliugnnwcdz6ljia"; +const WASM_CID: &str = "bafybeig6u35v6t3f4j3zgz2jvj4erd45fbkeolioaddu3lmu6uxm3ilb7a"; #[cfg(any(test, feature = "test-utils"))] const CAT_CID: &str = "bafybeiejevluvtoevgk66plh5t6xiy3ikyuuxg3vgofuvpeckb6eadresm"; @@ -34,6 +34,8 @@ impl Fetch { for rsc in resources.iter() { let task = tryhard::retry_fn(|| async { tracing::info!( + subject = "fetch_rsc", + category = "fetch", rsc = rsc.to_string(), "attempting to fetch resource from IPFS" ); @@ -47,20 +49,32 @@ impl Fetch { async move { if attempts < retries { tracing::warn!( + subject = "fetch_rsc.err", + category = "fetch", err = err, attempts = attempts, "retrying fetch after error @ {}ms", next_delay.map(|d| d.as_millis()).unwrap_or(0) ); } else { - tracing::warn!(err = err, attempts = attempts, "maxed out # of retries"); + tracing::warn!( + subject = "fetch_rsc.err", + category = "fetch", + err = err, + attempts = attempts, + "maxed out # of retries" + ); } } }); tasks.push(task); } - tracing::info!("fetching necessary resources from IPFS"); + tracing::info!( + subject = "fetch_rscs", + category = "fetch", + "fetching necessary resources from IPFS" + ); if let Ok(vec) = tasks.try_collect::>().await { vec.into_iter() .try_fold(IndexMap::default(), |mut acc, res| { diff --git a/homestar-runtime/src/tasks/wasm.rs b/homestar-runtime/src/tasks/wasm.rs index b55f3a37..7ba5fd70 100644 --- a/homestar-runtime/src/tasks/wasm.rs +++ b/homestar-runtime/src/tasks/wasm.rs @@ -1,3 +1,7 @@ +//! Functionality around Wasm-based [tasks]. +//! +//! [tasks]: homestar_core::workflow::Task + use super::FileLoad; use async_trait::async_trait; use homestar_core::workflow::input::Args; diff --git a/homestar-runtime/src/test_utils/worker_builder.rs b/homestar-runtime/src/test_utils/worker_builder.rs index f9c8e24c..14e0d255 100644 --- a/homestar-runtime/src/test_utils/worker_builder.rs +++ b/homestar-runtime/src/test_utils/worker_builder.rs @@ -25,26 +25,45 @@ use homestar_wasm::io::Arg; use indexmap::IndexMap; use libipld::Cid; -/// TODO +/// Utility structure for building out [Worker]s for testing purposes. +/// +/// [Worker]: crate::Worker #[cfg(feature = "ipfs")] pub(crate) struct WorkerBuilder<'a> { + /// In-memory database for testing. db: MemoryDb, + /// Event channel sender. event_sender: AsyncChannelSender, + /// [IPFS client]. + /// + /// [IPFS client]: crate::network::IpfsCli ipfs: IpfsCli, + /// Runner channel sender. runner_sender: AsyncChannelSender, + /// Name of the workflow. name: Option, + /// [Workflow] to run. workflow: Workflow<'a, Arg>, + /// [Workflow] settings. workflow_settings: workflow::Settings, } -/// TODO +/// Utility structure for building out [Worker]s for testing purposes. +/// +/// [Worker]: crate::Worker #[cfg(not(feature = "ipfs"))] pub(crate) struct WorkerBuilder<'a> { + /// In-memory database for testing. db: MemoryDb, + /// Event channel sender. event_sender: AsyncChannelSender, + /// Runner channel sender. runner_sender: AsyncChannelSender, + /// Name of the workflow. name: Option, + /// [Workflow] to run. workflow: Workflow<'a, Arg>, + /// [Workflow] settings. workflow_settings: workflow::Settings, } @@ -113,7 +132,10 @@ impl<'a> WorkerBuilder<'a> { .unwrap() } - /// TODO + /// Fetch-function closure for the [Worker]/[Scheduler] to use. + /// + /// [Worker]: crate::Worker + /// [Scheduler]: crate::TaskScheduler #[cfg(feature = "ipfs")] #[allow(dead_code)] pub(crate) fn fetch_fn( @@ -129,7 +151,10 @@ impl<'a> WorkerBuilder<'a> { fetch_fn } - /// TODO + /// Fetch-function closure for the [Worker]/[Scheduler] to use. + /// + /// [Worker]: crate::Worker + /// [Scheduler]: crate::TaskScheduler #[cfg(not(feature = "ipfs"))] #[allow(dead_code)] pub(crate) fn fetch_fn( diff --git a/homestar-runtime/src/worker.rs b/homestar-runtime/src/worker.rs index 05f55af3..50bbbe71 100644 --- a/homestar-runtime/src/worker.rs +++ b/homestar-runtime/src/worker.rs @@ -2,7 +2,7 @@ //! sends [Event]'s to the [EventHandler]. //! //! [Workflow]: homestar_core::Workflow -//! [EventHandler]: crate::event_handler::EventHandler +//! [EventHandler]: crate::EventHandler #[cfg(feature = "websocket-notify")] use crate::event_handler::event::Replay; @@ -16,10 +16,10 @@ use crate::{ }, network::swarm::CapsuleTag, runner::{ModifiedSet, RunningTaskSet}, - scheduler::{ExecutionGraph, TaskScheduler}, + scheduler::ExecutionGraph, tasks::{RegisteredTasks, WasmContext}, workflow::{self, Resource}, - Db, Receipt, + Db, Receipt, TaskScheduler, }; use anyhow::{anyhow, Context, Result}; use chrono::NaiveDateTime; @@ -51,8 +51,12 @@ use tracing::{debug, error, info}; #[allow(dead_code)] pub(crate) type TaskSet = JoinSet>; +/// Messages sent to [Worker] from [Runner]. +/// +/// [Runner]: crate::Runner #[derive(Debug, Clone, PartialEq)] pub(crate) enum WorkerMessage { + /// Signal that the [Worker] has been dropped for a workflow run. Dropped(Cid), } @@ -60,13 +64,25 @@ pub(crate) enum WorkerMessage { #[allow(dead_code)] #[allow(missing_debug_implementations)] pub(crate) struct Worker<'a, DB: Database> { + /// [ExecutionGraph] of the [Workflow] to run. pub(crate) graph: Arc>, + /// [EventHandler] channel to send [Event]s to. + /// + /// [EventHandler]: crate::EventHandler pub(crate) event_sender: Arc>, + /// [Runner] channel to send [WorkerMessage]s to. + /// + /// [Runner]: crate::Runner pub(crate) runner_sender: AsyncChannelSender, + /// [Database] pool to pull connections from for the [Worker] run. pub(crate) db: DB, + /// Local name of the [Workflow] being run. pub(crate) workflow_name: FastStr, + /// [Workflow] information. pub(crate) workflow_info: Arc, + /// [Workflow] settings. pub(crate) workflow_settings: Arc, + /// [NaiveDateTime] of when the [Workflow] was started. pub(crate) workflow_started: NaiveDateTime, } @@ -152,7 +168,10 @@ where { Ok(ctx) => self.run_queue(ctx.scheduler, running_tasks).await, Err(err) => { - error!(err=?err, "error initializing scheduler"); + error!(subject = "worker.init.err", + category = "worker.run", + err=?err, + "error initializing scheduler"); Err(anyhow!("error initializing scheduler")) } } @@ -184,16 +203,28 @@ where event_sender: Arc>, ) -> Result, ResolveError> { info!( + subject = "worker.resolve_cid", + category = "worker.run", workflow_cid = workflow_cid.to_string(), cid = cid.to_string(), "attempting to resolve cid in workflow" ); if let Some(result) = linkmap.read().await.get(&cid) { - debug!(cid = cid.to_string(), "found in in-memory linkmap"); + debug!( + subject = "worker.resolve_cid", + category = "worker.run", + cid = cid.to_string(), + "found CID in in-memory linkmap" + ); Ok(result.to_owned()) } else if let Some(bytes) = resources.read().await.get(&Resource::Cid(cid)) { - debug!(cid = cid.to_string(), "found in resources"); + debug!( + subject = "worker.resolve_cid", + category = "worker.run", + cid = cid.to_string(), + "found CID in map of resources" + ); Ok(InstructionResult::Ok(Arg::Ipld(Ipld::Bytes( bytes.to_vec(), )))) @@ -202,7 +233,11 @@ where match Db::find_instruction_by_cid(cid, conn) { Ok(found) => Ok(found.output_as_arg()), Err(_) => { - debug!("no related instruction receipt found in the DB"); + debug!( + subject = "worker.resolve_cid", + category = "worker.run", + "no related instruction receipt found in the DB" + ); let (tx, rx) = AsyncChannel::oneshot(); let _ = event_sender .send_async(Event::FindRecord(QueryRecord::with( @@ -251,6 +286,8 @@ where { if scheduler.ran_length() > 0 { info!( + subject = "worker.replay", + category = "worker.run", workflow_cid = self.workflow_info.cid.to_string(), "{} tasks left to run, sending last batch for workflow", scheduler.run_length() @@ -350,7 +387,10 @@ where let resolved = match resolved.await { Ok(inst_result) => inst_result, Err(err) => { - error!(err=?err, "error resolving cid"); + error!(subject = "worker.resolve_cid.err", + category = "worker.run", + err=?err, + "error resolving cid"); return Err(anyhow!("error resolving cid: {err}")) .with_context(|| { format!("could not spawn task for cid: {workflow_cid}") @@ -367,13 +407,15 @@ where Err(err) => Err( anyhow!("cannot execute wasm module: {err}")) .with_context(|| { - format!("not able to run fn {fun} for promised cid: {instruction_ptr}, in workflow {workflow_cid}") + format!("not able to run fn {fun} for cid: {instruction_ptr}, in workflow {workflow_cid}") }), } }); handles.push(handle); } None => error!( + subject = "worker.run.task.err", + category = "worker.run", "no valid task/instruction-type referenced by operation: {}", instruction.op() ), @@ -387,11 +429,17 @@ where { Ok(Ok(data)) => data, Ok(Err(err)) => { - error!(err=?err, "error in running task"); + error!(subject = "worker.run.task.err", + category = "worker.run", + err=?err, + "error in running task"); break; } Err(err) => { - error!(err=?err, "error in running task"); + error!(subject = "worker.run.task.err", + category = "worker.run", + err=?err, + "error in running task"); break; } }; @@ -426,6 +474,8 @@ where Db::commit_receipt(self.workflow_info.cid, receipt, &mut self.db.conn()?)?; debug!( + subject = "db.commit_receipt", + category = "worker.run", cid = self.workflow_info.cid.to_string(), "commited to database" ); diff --git a/homestar-runtime/src/workflow.rs b/homestar-runtime/src/workflow.rs index a01b48ce..b4669a9a 100644 --- a/homestar-runtime/src/workflow.rs +++ b/homestar-runtime/src/workflow.rs @@ -156,7 +156,12 @@ impl<'a> Builder<'a> { (Dag::default(), vec![], vec![], IndexMap::new()), |(mut dag, mut unawaits, mut awaited, mut resources), (i, task)| { let instr_cid = task.instruction_cid()?; - debug!("instruction cid: {}", instr_cid); + debug!( + subject = "task.instruction", + category = "aot.information", + "instruction cid of task: {}", + instr_cid + ); // Clone as we're owning the struct going backward. let ptr: Pointer = Invocation::::from(task.clone()).try_into()?; diff --git a/homestar-runtime/src/workflow/info.rs b/homestar-runtime/src/workflow/info.rs index 7110a64d..e4b7e351 100644 --- a/homestar-runtime/src/workflow/info.rs +++ b/homestar-runtime/src/workflow/info.rs @@ -44,11 +44,29 @@ const RESOURCES_KEY: &str = "resources"; #[derive(Debug, Clone, PartialEq, Queryable, Insertable, Identifiable, Selectable)] #[diesel(table_name = crate::db::schema::workflows, primary_key(cid))] pub struct Stored { + /// Wrapped-[Cid] of [Workflow]. + /// + /// [Workflow]: homestar_core::Workflow pub(crate) cid: Pointer, + /// Local name of [Workflow]. + /// + /// [Workflow]: homestar_core::Workflow pub(crate) name: Option, + /// Number of tasks in [Workflow]. + /// + /// [Workflow]: homestar_core::Workflow pub(crate) num_tasks: i32, + /// Map of [Instruction] [Cid]s to resources. + /// + /// [Instruction]: homestar_core::workflow::Instruction pub(crate) resources: IndexedResources, + /// Local timestamp of [Workflow] creation. + /// + /// [Workflow]: homestar_core::Workflow pub(crate) created_at: NaiveDateTime, + /// Local timestamp of [Workflow] completion. + /// + /// [Workflow]: homestar_core::Workflow pub(crate) completed_at: Option, } @@ -275,6 +293,8 @@ impl Info { Ok((_, info)) => Ok((info, timestamp)), Err(_err) => { info!( + subject = "workflow.init.db.check", + category = "workflow", cid = workflow_cid.to_string(), "workflow information not available in the database" ); @@ -322,6 +342,8 @@ impl Info { Some((_name, workflow_info)) => Ok(workflow_info), None => { info!( + subject = "workflow.gather.db.check", + category = "workflow", cid = workflow_cid.to_string(), "workflow information not available in the database" ); diff --git a/homestar-runtime/src/workflow/settings.rs b/homestar-runtime/src/workflow/settings.rs index 9ea3e682..b5eae6de 100644 --- a/homestar-runtime/src/workflow/settings.rs +++ b/homestar-runtime/src/workflow/settings.rs @@ -7,10 +7,15 @@ use std::time::Duration; /// Workflow settings. #[derive(Debug, Clone, PartialEq)] pub struct Settings { + /// Number of retries for a given workflow. pub(crate) retries: u32, + /// Maximum delay between retries. pub(crate) retry_max_delay: Duration, + /// Initial delay between retries. pub(crate) retry_initial_delay: Duration, + /// Timeout for P2P/DHT operations. pub(crate) p2p_timeout: Duration, + /// Timeout for a given workflow. pub(crate) timeout: Duration, } diff --git a/homestar-runtime/tests/cli.rs b/homestar-runtime/tests/cli.rs index 550b0263..e7fce91c 100644 --- a/homestar-runtime/tests/cli.rs +++ b/homestar-runtime/tests/cli.rs @@ -200,7 +200,7 @@ fn test_workflow_run_serial() -> Result<()> { .assert() .success() .stdout(predicate::str::contains( - "ipfs://bafybeiczefaiu7464ehupezpzulnti5jvcwnvdalqrdliugnnwcdz6ljia", + "ipfs://bafybeig6u35v6t3f4j3zgz2jvj4erd45fbkeolioaddu3lmu6uxm3ilb7a", )) .stdout(predicate::str::contains("num_tasks")) .stdout(predicate::str::contains("progress_count")); @@ -215,7 +215,7 @@ fn test_workflow_run_serial() -> Result<()> { .assert() .success() .stdout(predicate::str::contains( - "ipfs://bafybeiczefaiu7464ehupezpzulnti5jvcwnvdalqrdliugnnwcdz6ljia", + "ipfs://bafybeig6u35v6t3f4j3zgz2jvj4erd45fbkeolioaddu3lmu6uxm3ilb7a", )) .stdout(predicate::str::contains("num_tasks")) .stdout(predicate::str::contains("progress_count")); diff --git a/homestar-runtime/tests/fixtures/test-workflow-add-one.json b/homestar-runtime/tests/fixtures/test-workflow-add-one.json index 05aa7c3c..5b83a711 100644 --- a/homestar-runtime/tests/fixtures/test-workflow-add-one.json +++ b/homestar-runtime/tests/fixtures/test-workflow-add-one.json @@ -17,7 +17,7 @@ }, "nnc": "", "op": "wasm/run", - "rsc": "ipfs://bafybeiczefaiu7464ehupezpzulnti5jvcwnvdalqrdliugnnwcdz6ljia" + "rsc": "ipfs://bafybeig6u35v6t3f4j3zgz2jvj4erd45fbkeolioaddu3lmu6uxm3ilb7a" } }, { @@ -33,7 +33,7 @@ "args": [ { "await/ok": { - "/": "bafyrmid5morwzj3lz436g44usvu35xcfyn4ommfe4ozulmnrsohybdez3a" + "/": "bafyrmie3vqqilbt6ghxqkvzieials254hwqr23fl6ecktt4kdmftbxejfu" } } ], @@ -41,7 +41,7 @@ }, "nnc": "", "op": "wasm/run", - "rsc": "ipfs://bafybeiczefaiu7464ehupezpzulnti5jvcwnvdalqrdliugnnwcdz6ljia" + "rsc": "ipfs://bafybeig6u35v6t3f4j3zgz2jvj4erd45fbkeolioaddu3lmu6uxm3ilb7a" } } ] diff --git a/homestar-runtime/tests/fixtures/test-workflow-image-pipeline.json b/homestar-runtime/tests/fixtures/test-workflow-image-pipeline.json index 5d252963..a9362b96 100644 --- a/homestar-runtime/tests/fixtures/test-workflow-image-pipeline.json +++ b/homestar-runtime/tests/fixtures/test-workflow-image-pipeline.json @@ -22,7 +22,7 @@ }, "nnc": "", "op": "wasm/run", - "rsc": "ipfs://bafybeiczefaiu7464ehupezpzulnti5jvcwnvdalqrdliugnnwcdz6ljia" + "rsc": "ipfs://bafybeig6u35v6t3f4j3zgz2jvj4erd45fbkeolioaddu3lmu6uxm3ilb7a" } }, { @@ -37,7 +37,7 @@ "args": [ { "await/ok": { - "/": "bafyrmid35kzcxbn5xhsewyzzja5gngm54peve5i45vdj3bxgokmuvj2wwy" + "/": "bafyrmiaadxb2oauwkak5ugyvgmi4jtw5bck2e3rvoznlegahniv654b3l4" } } ], @@ -45,7 +45,7 @@ }, "nnc": "", "op": "wasm/run", - "rsc": "ipfs://bafybeiczefaiu7464ehupezpzulnti5jvcwnvdalqrdliugnnwcdz6ljia" + "rsc": "ipfs://bafybeig6u35v6t3f4j3zgz2jvj4erd45fbkeolioaddu3lmu6uxm3ilb7a" } }, { @@ -60,7 +60,7 @@ "args": [ { "await/ok": { - "/": "bafyrmicubn76iynz6hjice6x47p7hgdlhht6qoizhuu746uea6e674jap4" + "/": "bafyrmigat3k3pbwavivjg3fldsnlwaxpznechmuibnhhldfhfiwmbnbyrq" } } ], @@ -68,7 +68,7 @@ }, "nnc": "", "op": "wasm/run", - "rsc": "ipfs://bafybeiczefaiu7464ehupezpzulnti5jvcwnvdalqrdliugnnwcdz6ljia" + "rsc": "ipfs://bafybeig6u35v6t3f4j3zgz2jvj4erd45fbkeolioaddu3lmu6uxm3ilb7a" } } ] diff --git a/homestar-runtime/tests/fixtures/test-workflow-jco.json b/homestar-runtime/tests/fixtures/test-workflow-jco.json index 85bbaeb3..abc2d206 100644 --- a/homestar-runtime/tests/fixtures/test-workflow-jco.json +++ b/homestar-runtime/tests/fixtures/test-workflow-jco.json @@ -9,13 +9,36 @@ "prf": [], "run": { "input": { - "args": ["hello", 10], + "args": ["helloworld", 10], "func": "sum" }, "nnc": "", "op": "wasm/run", "rsc": "ipfs://bafybeibawnb3pytqmky4ph37hj7y7qosqcneofjextqq55zhxfniiletfu" } + }, + { + "cause": null, + "meta": { + "memory": 4294967296, + "time": 100000 + }, + "prf": [], + "run": { + "input": { + "args": [ + { + "await/ok": { + "/": "bafyrmigltroc4nrdaskjselusvfcuooc52k2rl7nmrf72edbauyptmx5bq" + } + } + ], + "func": "hashbytes" + }, + "nnc": "", + "op": "wasm/run", + "rsc": "ipfs://bafybeifrgndv3blrxucig2pei6uaoyiyipk5vvzds7ps47ec6pzf4ax2d4" + } } ] } diff --git a/homestar-runtime/tests/fixtures/test-workflow-no-awaits1.json b/homestar-runtime/tests/fixtures/test-workflow-no-awaits1.json index cc0407fc..b8569a09 100644 --- a/homestar-runtime/tests/fixtures/test-workflow-no-awaits1.json +++ b/homestar-runtime/tests/fixtures/test-workflow-no-awaits1.json @@ -22,7 +22,7 @@ }, "nnc": "", "op": "wasm/run", - "rsc": "ipfs://bafybeiczefaiu7464ehupezpzulnti5jvcwnvdalqrdliugnnwcdz6ljia" + "rsc": "ipfs://bafybeig6u35v6t3f4j3zgz2jvj4erd45fbkeolioaddu3lmu6uxm3ilb7a" } } ] diff --git a/homestar-runtime/tests/fixtures/test-workflow-no-awaits2.json b/homestar-runtime/tests/fixtures/test-workflow-no-awaits2.json index d7d6a2e2..e82e7f63 100644 --- a/homestar-runtime/tests/fixtures/test-workflow-no-awaits2.json +++ b/homestar-runtime/tests/fixtures/test-workflow-no-awaits2.json @@ -22,7 +22,7 @@ }, "nnc": "", "op": "wasm/run", - "rsc": "ipfs://bafybeiczefaiu7464ehupezpzulnti5jvcwnvdalqrdliugnnwcdz6ljia" + "rsc": "ipfs://bafybeig6u35v6t3f4j3zgz2jvj4erd45fbkeolioaddu3lmu6uxm3ilb7a" } }, { @@ -47,7 +47,7 @@ }, "nnc": "", "op": "wasm/run", - "rsc": "ipfs://bafybeiczefaiu7464ehupezpzulnti5jvcwnvdalqrdliugnnwcdz6ljia" + "rsc": "ipfs://bafybeig6u35v6t3f4j3zgz2jvj4erd45fbkeolioaddu3lmu6uxm3ilb7a" } } ] diff --git a/homestar-runtime/tests/network.rs b/homestar-runtime/tests/network.rs index c4d78acc..c52eb19f 100644 --- a/homestar-runtime/tests/network.rs +++ b/homestar-runtime/tests/network.rs @@ -456,7 +456,7 @@ fn test_libp2p_connect_rendezvous_discovery_serial() -> Result<()> { } // Wait for registration to complete - // TODO When we have websocket push events, listen on a registration event instead of using an arbitrary sleep + // TODO When we have WebSocket push events, listen on a registration event instead of using an arbitrary sleep thread::sleep(Duration::from_secs(2)); // Start a peer that will discover the registrant through the rendezvous server @@ -762,7 +762,7 @@ fn test_libp2p_disconnect_rendezvous_discovery_serial() -> Result<()> { } // Wait for registration to complete. - // TODO When we have websocket push events, listen on a registration event instead of using an arbitrary sleep. + // TODO When we have WebSocket push events, listen on a registration event instead of using an arbitrary sleep. thread::sleep(Duration::from_secs(2)); // Start a peer that will discover the registrant through the rendezvous server @@ -1036,7 +1036,7 @@ fn test_libp2p_rendezvous_rediscover_on_expiration_serial() -> Result<()> { } // Wait for registration to complete. - // TODO When we have websocket push events, listen on a registration event instead of using an arbitrary sleep. + // TODO When we have WebSocket push events, listen on a registration event instead of using an arbitrary sleep. thread::sleep(Duration::from_secs(2)); // Start a peer that will discover with the rendezvous server when diff --git a/homestar-runtime/tests/webserver.rs b/homestar-runtime/tests/webserver.rs index 1aebda3f..6e3bb537 100644 --- a/homestar-runtime/tests/webserver.rs +++ b/homestar-runtime/tests/webserver.rs @@ -20,6 +20,7 @@ use std::{ static BIN: Lazy = Lazy::new(|| assert_cmd::cargo::cargo_bin(BIN_NAME)); const SUBSCRIBE_RUN_WORKFLOW_ENDPOINT: &str = "subscribe_run_workflow"; const UNSUBSCRIBE_RUN_WORKFLOW_ENDPOINT: &str = "unsubscribe_run_workflow"; +const AWAIT_CID: &str = "bafyrmih5bwjinspvn5ktpcaxqvvxkmhxrznhuu3qqmu45jnpmo3ab72vaq"; #[test] #[file_serial] @@ -75,7 +76,7 @@ fn test_workflow_run_serial() -> Result<()> { let json: serde_json::Value = serde_json::from_slice(&msg.unwrap().unwrap()).unwrap(); let check = json.get("metadata").unwrap(); - let expected = serde_json::json!({"name": "test", "replayed": false, "workflow": {"/": "bafyrmihfhdhxmhotbgn5digt6n7vgz2ukisafhjozki2e6nwtvunep3mrm"}}); + let expected = serde_json::json!({"name": "test", "replayed": false, "workflow": {"/": format!("{AWAIT_CID}")}}); assert_eq!(check, &expected); received_cids += 1; } else { @@ -103,7 +104,7 @@ fn test_workflow_run_serial() -> Result<()> { let json: serde_json::Value = serde_json::from_slice(&msg.unwrap().unwrap()).unwrap(); let check = json.get("metadata").unwrap(); - let expected = serde_json::json!({"name": "test", "replayed": true, "workflow": {"/": "bafyrmihfhdhxmhotbgn5digt6n7vgz2ukisafhjozki2e6nwtvunep3mrm"}}); + let expected = serde_json::json!({"name": "test", "replayed": true, "workflow": {"/": format!("{AWAIT_CID}")}}); assert_eq!(check, &expected); received_cids += 1; } else { @@ -137,7 +138,7 @@ fn test_workflow_run_serial() -> Result<()> { let json: serde_json::Value = serde_json::from_slice(&msg.unwrap().unwrap()).unwrap(); let check = json.get("metadata").unwrap(); - let expected = serde_json::json!({"name": "test", "replayed": true, "workflow": {"/": "bafyrmihfhdhxmhotbgn5digt6n7vgz2ukisafhjozki2e6nwtvunep3mrm"}}); + let expected = serde_json::json!({"name": "test", "replayed": true, "workflow": {"/": format!("{AWAIT_CID}")}}); assert_eq!(check, &expected); received_cids += 1; } else { @@ -172,7 +173,7 @@ fn test_workflow_run_serial() -> Result<()> { let json: serde_json::Value = serde_json::from_slice(&msg.unwrap().unwrap()).unwrap(); let check = json.get("metadata").unwrap(); - let expected = serde_json::json!({"name": "another_test", "replayed": true, "workflow": {"/": "bafyrmihfhdhxmhotbgn5digt6n7vgz2ukisafhjozki2e6nwtvunep3mrm"}}); + let expected = serde_json::json!({"name": "another_test", "replayed": true, "workflow": {"/": format!("{AWAIT_CID}")}}); assert_eq!(check, &expected); received_cids += 1; } else { diff --git a/homestar-wasm/fixtures/example_add.wasm b/homestar-wasm/fixtures/example_add.wasm index 2f3d643ad13ee8ea7279d27590360382da2245da..47bea7528a208bc000f958acfddac932f26ede02 100755 GIT binary patch delta 2018 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 diff --git a/homestar-wasm/fixtures/example_add.wat b/homestar-wasm/fixtures/example_add.wat index 868c0d5e..042ab78d 100644 --- a/homestar-wasm/fixtures/example_add.wat +++ b/homestar-wasm/fixtures/example_add.wat @@ -402,7 +402,7 @@ block ;; label = @6 i32.const 0 i32.load offset=1048984 - local.tee 7 + local.tee 6 i32.const 16 local.get 0 i32.const 11 @@ -432,51 +432,51 @@ i32.and local.get 1 i32.add - local.tee 2 + local.tee 1 i32.const 3 i32.shl - local.tee 5 + local.tee 2 i32.const 1048728 i32.add i32.load local.tee 0 i32.const 8 i32.add - local.tee 6 + local.tee 7 i32.load - local.tee 1 - local.get 5 + local.tee 5 + local.get 2 i32.const 1048720 i32.add - local.tee 5 + local.tee 2 i32.eq br_if 0 (;@8;) - local.get 1 local.get 5 + local.get 2 i32.store offset=12 + local.get 2 local.get 5 - local.get 1 i32.store offset=8 br 1 (;@7;) end i32.const 0 - local.get 7 + local.get 6 i32.const -2 - local.get 2 + local.get 1 i32.rotl i32.and i32.store offset=1048984 end local.get 0 - local.get 2 + local.get 1 i32.const 3 i32.shl - local.tee 2 + local.tee 1 i32.const 3 i32.or i32.store offset=4 local.get 0 - local.get 2 + local.get 1 i32.add local.tee 0 local.get 0 @@ -484,7 +484,7 @@ i32.const 1 i32.or i32.store offset=4 - local.get 6 + local.get 7 return end local.get 2 @@ -513,7 +513,7 @@ i32.const 1048576 i32.add i32.load - local.tee 6 + local.tee 7 i32.load offset=4 i32.const -8 i32.and @@ -522,11 +522,11 @@ local.set 5 block ;; label = @13 block ;; label = @14 - local.get 6 + local.get 7 i32.load offset=16 local.tee 0 br_if 0 (;@14;) - local.get 6 + local.get 7 i32.const 20 i32.add i32.load @@ -544,7 +544,7 @@ local.tee 8 local.get 5 i32.lt_u - local.set 7 + local.set 6 block ;; label = @15 local.get 0 i32.load offset=16 @@ -558,47 +558,47 @@ end local.get 8 local.get 5 - local.get 7 + local.get 6 select local.set 5 local.get 0 - local.get 6 local.get 7 + local.get 6 select - local.set 6 + local.set 7 local.get 1 local.set 0 local.get 1 br_if 0 (;@14;) end end - local.get 6 + local.get 7 call 7 local.get 5 i32.const 16 i32.lt_u br_if 2 (;@10;) - local.get 6 + local.get 7 local.get 2 i32.const 3 i32.or i32.store offset=4 - local.get 6 + local.get 7 local.get 2 i32.add - local.tee 2 + local.tee 1 local.get 5 i32.const 1 i32.or i32.store offset=4 - local.get 2 + local.get 1 local.get 5 i32.add local.get 5 i32.store i32.const 0 i32.load offset=1048992 - local.tee 7 + local.tee 6 br_if 1 (;@11;) br 5 (;@7;) end @@ -623,7 +623,7 @@ local.tee 1 i32.const 3 i32.shl - local.tee 6 + local.tee 7 i32.const 1048728 i32.add i32.load @@ -633,22 +633,22 @@ local.tee 8 i32.load local.tee 5 - local.get 6 + local.get 7 i32.const 1048720 i32.add - local.tee 6 + local.tee 7 i32.eq br_if 0 (;@13;) local.get 5 - local.get 6 + local.get 7 i32.store offset=12 - local.get 6 + local.get 7 local.get 5 i32.store offset=8 br 1 (;@12;) end i32.const 0 - local.get 7 + local.get 6 i32.const -2 local.get 1 i32.rotl @@ -663,34 +663,34 @@ local.get 0 local.get 2 i32.add - local.tee 7 + local.tee 6 local.get 1 i32.const 3 i32.shl - local.tee 1 + local.tee 5 local.get 2 i32.sub - local.tee 2 + local.tee 1 i32.const 1 i32.or i32.store offset=4 local.get 0 - local.get 1 + local.get 5 i32.add - local.get 2 + local.get 1 i32.store i32.const 0 i32.load offset=1048992 - local.tee 5 + local.tee 2 br_if 2 (;@9;) br 3 (;@8;) end - local.get 7 + local.get 6 i32.const -8 i32.and i32.const 1048720 i32.add - local.set 1 + local.set 2 i32.const 0 i32.load offset=1049000 local.set 0 @@ -700,42 +700,41 @@ i32.load offset=1048984 local.tee 8 i32.const 1 - local.get 7 + local.get 6 i32.const 3 i32.shr_u i32.shl - local.tee 7 + local.tee 6 i32.and - i32.eqz br_if 0 (;@12;) - local.get 1 - i32.load offset=8 - local.set 7 + i32.const 0 + local.get 8 + local.get 6 + i32.or + i32.store offset=1048984 + local.get 2 + local.set 6 br 1 (;@11;) end - i32.const 0 - local.get 8 - local.get 7 - i32.or - i32.store offset=1048984 - local.get 1 - local.set 7 + local.get 2 + i32.load offset=8 + local.set 6 end - local.get 1 + local.get 2 local.get 0 i32.store offset=8 - local.get 7 + local.get 6 local.get 0 i32.store offset=12 local.get 0 - local.get 1 + local.get 2 i32.store offset=12 local.get 0 - local.get 7 + local.get 6 i32.store offset=8 br 3 (;@7;) end - local.get 6 + local.get 7 local.get 5 local.get 2 i32.add @@ -743,7 +742,7 @@ i32.const 3 i32.or i32.store offset=4 - local.get 6 + local.get 7 local.get 0 i32.add local.tee 0 @@ -754,12 +753,12 @@ i32.store offset=4 br 3 (;@6;) end - local.get 5 + local.get 2 i32.const -8 i32.and i32.const 1048720 i32.add - local.set 1 + local.set 5 i32.const 0 i32.load offset=1049000 local.set 0 @@ -767,59 +766,58 @@ block ;; label = @10 i32.const 0 i32.load offset=1048984 - local.tee 6 + local.tee 7 i32.const 1 - local.get 5 + local.get 2 i32.const 3 i32.shr_u i32.shl - local.tee 5 + local.tee 2 i32.and - i32.eqz br_if 0 (;@10;) - local.get 1 - i32.load offset=8 - local.set 5 + i32.const 0 + local.get 7 + local.get 2 + i32.or + i32.store offset=1048984 + local.get 5 + local.set 2 br 1 (;@9;) end - i32.const 0 - local.get 6 local.get 5 - i32.or - i32.store offset=1048984 - local.get 1 - local.set 5 + i32.load offset=8 + local.set 2 end - local.get 1 + local.get 5 local.get 0 i32.store offset=8 - local.get 5 + local.get 2 local.get 0 i32.store offset=12 local.get 0 - local.get 1 + local.get 5 i32.store offset=12 local.get 0 - local.get 5 + local.get 2 i32.store offset=8 end i32.const 0 - local.get 7 + local.get 6 i32.store offset=1049000 i32.const 0 - local.get 2 + local.get 1 i32.store offset=1048992 local.get 8 return end i32.const 0 - local.get 2 + local.get 1 i32.store offset=1049000 i32.const 0 local.get 5 i32.store offset=1048992 end - local.get 6 + local.get 7 i32.const 8 i32.add return @@ -858,21 +856,30 @@ br_if 1 (;@3;) end loop ;; label = @4 + local.get 0 + local.get 6 local.get 0 i32.load offset=4 i32.const -8 i32.and local.tee 5 local.get 2 - i32.ge_u - local.get 5 - local.get 2 i32.sub local.tee 8 local.get 1 i32.lt_u - i32.and + local.tee 4 + select + local.set 3 + local.get 5 + local.get 2 + i32.lt_u local.set 7 + local.get 8 + local.get 1 + local.get 4 + select + local.set 8 block ;; label = @5 local.get 0 i32.load offset=16 @@ -884,13 +891,13 @@ i32.load local.set 5 end - local.get 0 local.get 6 + local.get 3 local.get 7 select local.set 6 - local.get 8 local.get 1 + local.get 8 local.get 7 select local.set 1 @@ -958,12 +965,12 @@ i32.and i32.const 1048720 i32.add - local.set 2 + local.set 5 block ;; label = @5 block ;; label = @6 i32.const 0 i32.load offset=1048984 - local.tee 5 + local.tee 2 i32.const 1 local.get 1 i32.const 3 @@ -971,29 +978,28 @@ i32.shl local.tee 1 i32.and - i32.eqz br_if 0 (;@6;) + i32.const 0 local.get 2 - i32.load offset=8 + local.get 1 + i32.or + i32.store offset=1048984 + local.get 5 local.set 1 br 1 (;@5;) end - i32.const 0 local.get 5 - local.get 1 - i32.or - i32.store offset=1048984 - local.get 2 + i32.load offset=8 local.set 1 end - local.get 2 + local.get 5 local.get 0 i32.store offset=8 local.get 1 local.get 0 i32.store offset=12 local.get 0 - local.get 2 + local.get 5 i32.store offset=12 local.get 0 local.get 1 @@ -1031,887 +1037,884 @@ block ;; label = @7 block ;; label = @8 block ;; label = @9 + i32.const 0 + i32.load offset=1048992 + local.tee 0 + local.get 2 + i32.ge_u + br_if 0 (;@9;) block ;; label = @10 + i32.const 0 + i32.load offset=1048996 + local.tee 0 + local.get 2 + i32.gt_u + br_if 0 (;@10;) + i32.const 0 + local.set 1 + local.get 2 + i32.const 65583 + i32.add + local.tee 5 + i32.const 16 + i32.shr_u + memory.grow + local.tee 0 + i32.const -1 + i32.eq + local.tee 7 + br_if 9 (;@1;) + local.get 0 + i32.const 16 + i32.shl + local.tee 6 + i32.eqz + br_if 9 (;@1;) + i32.const 0 + i32.const 0 + i32.load offset=1049008 + i32.const 0 + local.get 5 + i32.const -65536 + i32.and + local.get 7 + select + local.tee 8 + i32.add + local.tee 0 + i32.store offset=1049008 + i32.const 0 + i32.const 0 + i32.load offset=1049012 + local.tee 1 + local.get 0 + local.get 1 + local.get 0 + i32.gt_u + select + i32.store offset=1049012 block ;; label = @11 - i32.const 0 - i32.load offset=1048992 - local.tee 0 - local.get 2 - i32.ge_u - br_if 0 (;@11;) block ;; label = @12 + block ;; label = @13 + i32.const 0 + i32.load offset=1049004 + local.tee 1 + i32.eqz + br_if 0 (;@13;) + i32.const 1048704 + local.set 0 + loop ;; label = @14 + local.get 0 + i32.load + local.tee 5 + local.get 0 + i32.load offset=4 + local.tee 7 + i32.add + local.get 6 + i32.eq + br_if 2 (;@12;) + local.get 0 + i32.load offset=8 + local.tee 0 + br_if 0 (;@14;) + br 3 (;@11;) + end + end + block ;; label = @13 + block ;; label = @14 + i32.const 0 + i32.load offset=1049020 + local.tee 0 + i32.eqz + br_if 0 (;@14;) + local.get 0 + local.get 6 + i32.le_u + br_if 1 (;@13;) + end + i32.const 0 + local.get 6 + i32.store offset=1049020 + end i32.const 0 - i32.load offset=1048996 - local.tee 0 - local.get 2 - i32.gt_u - br_if 0 (;@12;) + i32.const 4095 + i32.store offset=1049024 i32.const 0 - local.set 1 - local.get 2 - i32.const 65583 - i32.add - local.tee 5 - i32.const 16 - i32.shr_u - memory.grow - local.tee 0 - i32.const -1 - i32.eq - local.tee 6 - br_if 11 (;@1;) - local.get 0 - i32.const 16 - i32.shl - local.tee 7 - i32.eqz - br_if 11 (;@1;) + local.get 8 + i32.store offset=1048708 i32.const 0 + local.get 6 + i32.store offset=1048704 i32.const 0 - i32.load offset=1049008 + i32.const 1048720 + i32.store offset=1048732 i32.const 0 - local.get 5 - i32.const -65536 - i32.and - local.get 6 - select - local.tee 8 - i32.add - local.tee 0 - i32.store offset=1049008 + i32.const 1048728 + i32.store offset=1048740 i32.const 0 + i32.const 1048720 + i32.store offset=1048728 i32.const 0 - i32.load offset=1049012 - local.tee 1 - local.get 0 - local.get 1 - local.get 0 - i32.gt_u - select - i32.store offset=1049012 - block ;; label = @13 - block ;; label = @14 - block ;; label = @15 - i32.const 0 - i32.load offset=1049004 - local.tee 1 - i32.eqz - br_if 0 (;@15;) - i32.const 1048704 - local.set 0 - loop ;; label = @16 - local.get 0 - i32.load - local.tee 5 - local.get 0 - i32.load offset=4 - local.tee 6 - i32.add - local.get 7 - i32.eq - br_if 2 (;@14;) - local.get 0 - i32.load offset=8 - local.tee 0 - br_if 0 (;@16;) - br 3 (;@13;) - end - end - i32.const 0 - i32.load offset=1049020 - local.tee 0 - i32.eqz - br_if 4 (;@10;) - local.get 0 - local.get 7 - i32.gt_u - br_if 4 (;@10;) - br 11 (;@3;) - end - local.get 0 - i32.load offset=12 - br_if 0 (;@13;) - local.get 5 - local.get 1 - i32.gt_u - br_if 0 (;@13;) - local.get 1 - local.get 7 - i32.lt_u - br_if 4 (;@9;) - end + i32.const 1048736 + i32.store offset=1048748 + i32.const 0 + i32.const 1048728 + i32.store offset=1048736 + i32.const 0 + i32.const 1048744 + i32.store offset=1048756 + i32.const 0 + i32.const 1048736 + i32.store offset=1048744 + i32.const 0 + i32.const 1048752 + i32.store offset=1048764 + i32.const 0 + i32.const 1048744 + i32.store offset=1048752 + i32.const 0 + i32.const 1048760 + i32.store offset=1048772 + i32.const 0 + i32.const 1048752 + i32.store offset=1048760 + i32.const 0 + i32.const 1048768 + i32.store offset=1048780 + i32.const 0 + i32.const 1048760 + i32.store offset=1048768 + i32.const 0 + i32.const 1048776 + i32.store offset=1048788 + i32.const 0 + i32.const 1048768 + i32.store offset=1048776 + i32.const 0 + i32.const 0 + i32.store offset=1048716 + i32.const 0 + i32.const 1048784 + i32.store offset=1048796 + i32.const 0 + i32.const 1048776 + i32.store offset=1048784 + i32.const 0 + i32.const 1048784 + i32.store offset=1048792 + i32.const 0 + i32.const 1048792 + i32.store offset=1048804 + i32.const 0 + i32.const 1048792 + i32.store offset=1048800 + i32.const 0 + i32.const 1048800 + i32.store offset=1048812 + i32.const 0 + i32.const 1048800 + i32.store offset=1048808 + i32.const 0 + i32.const 1048808 + i32.store offset=1048820 + i32.const 0 + i32.const 1048808 + i32.store offset=1048816 + i32.const 0 + i32.const 1048816 + i32.store offset=1048828 + i32.const 0 + i32.const 1048816 + i32.store offset=1048824 + i32.const 0 + i32.const 1048824 + i32.store offset=1048836 + i32.const 0 + i32.const 1048824 + i32.store offset=1048832 + i32.const 0 + i32.const 1048832 + i32.store offset=1048844 + i32.const 0 + i32.const 1048832 + i32.store offset=1048840 + i32.const 0 + i32.const 1048840 + i32.store offset=1048852 + i32.const 0 + i32.const 1048840 + i32.store offset=1048848 + i32.const 0 + i32.const 1048848 + i32.store offset=1048860 + i32.const 0 + i32.const 1048856 + i32.store offset=1048868 + i32.const 0 + i32.const 1048848 + i32.store offset=1048856 + i32.const 0 + i32.const 1048864 + i32.store offset=1048876 + i32.const 0 + i32.const 1048856 + i32.store offset=1048864 + i32.const 0 + i32.const 1048872 + i32.store offset=1048884 + i32.const 0 + i32.const 1048864 + i32.store offset=1048872 + i32.const 0 + i32.const 1048880 + i32.store offset=1048892 + i32.const 0 + i32.const 1048872 + i32.store offset=1048880 + i32.const 0 + i32.const 1048888 + i32.store offset=1048900 + i32.const 0 + i32.const 1048880 + i32.store offset=1048888 + i32.const 0 + i32.const 1048896 + i32.store offset=1048908 i32.const 0 + i32.const 1048888 + i32.store offset=1048896 i32.const 0 - i32.load offset=1049020 + i32.const 1048904 + i32.store offset=1048916 + i32.const 0 + i32.const 1048896 + i32.store offset=1048904 + i32.const 0 + i32.const 1048912 + i32.store offset=1048924 + i32.const 0 + i32.const 1048904 + i32.store offset=1048912 + i32.const 0 + i32.const 1048920 + i32.store offset=1048932 + i32.const 0 + i32.const 1048912 + i32.store offset=1048920 + i32.const 0 + i32.const 1048928 + i32.store offset=1048940 + i32.const 0 + i32.const 1048920 + i32.store offset=1048928 + i32.const 0 + i32.const 1048936 + i32.store offset=1048948 + i32.const 0 + i32.const 1048928 + i32.store offset=1048936 + i32.const 0 + i32.const 1048944 + i32.store offset=1048956 + i32.const 0 + i32.const 1048936 + i32.store offset=1048944 + i32.const 0 + i32.const 1048952 + i32.store offset=1048964 + i32.const 0 + i32.const 1048944 + i32.store offset=1048952 + i32.const 0 + i32.const 1048960 + i32.store offset=1048972 + i32.const 0 + i32.const 1048952 + i32.store offset=1048960 + i32.const 0 + i32.const 1048968 + i32.store offset=1048980 + i32.const 0 + i32.const 1048960 + i32.store offset=1048968 + i32.const 0 + local.get 6 + i32.store offset=1049004 + i32.const 0 + i32.const 1048968 + i32.store offset=1048976 + i32.const 0 + local.get 8 + i32.const -40 + i32.add local.tee 0 - local.get 7 + i32.store offset=1048996 + local.get 6 + local.get 0 + i32.const 1 + i32.or + i32.store offset=4 + local.get 6 local.get 0 - local.get 7 - i32.lt_u - select - i32.store offset=1049020 - local.get 7 - local.get 8 i32.add - local.set 5 - i32.const 1048704 - local.set 0 + i32.const 40 + i32.store offset=4 + i32.const 0 + i32.const 2097152 + i32.store offset=1049016 + br 10 (;@2;) + end + local.get 0 + i32.load offset=12 + br_if 0 (;@11;) + local.get 5 + local.get 1 + i32.gt_u + br_if 0 (;@11;) + local.get 1 + local.get 6 + i32.lt_u + br_if 3 (;@8;) + end + i32.const 0 + i32.const 0 + i32.load offset=1049020 + local.tee 0 + local.get 6 + local.get 0 + local.get 6 + i32.lt_u + select + i32.store offset=1049020 + local.get 6 + local.get 8 + i32.add + local.set 5 + i32.const 1048704 + local.set 0 + block ;; label = @11 + block ;; label = @12 block ;; label = @13 - block ;; label = @14 - block ;; label = @15 - loop ;; label = @16 - local.get 0 - i32.load - local.get 5 - i32.eq - br_if 1 (;@15;) - local.get 0 - i32.load offset=8 - local.tee 0 - br_if 0 (;@16;) - br 2 (;@14;) - end - end - local.get 0 - i32.load offset=12 - i32.eqz - br_if 1 (;@13;) - end - i32.const 1048704 - local.set 0 - block ;; label = @14 - loop ;; label = @15 - block ;; label = @16 - local.get 0 - i32.load - local.tee 5 - local.get 1 - i32.gt_u - br_if 0 (;@16;) - local.get 5 - local.get 0 - i32.load offset=4 - i32.add - local.tee 5 - local.get 1 - i32.gt_u - br_if 2 (;@14;) - end - local.get 0 - i32.load offset=8 - local.set 0 - br 0 (;@15;) - end - end - i32.const 0 - local.get 7 - i32.store offset=1049004 - i32.const 0 - local.get 8 - i32.const -40 - i32.add - local.tee 0 - i32.store offset=1048996 - local.get 7 - local.get 0 - i32.const 1 - i32.or - i32.store offset=4 - local.get 7 - local.get 0 - i32.add - i32.const 40 - i32.store offset=4 - i32.const 0 - i32.const 2097152 - i32.store offset=1049016 - local.get 1 - local.get 5 - i32.const -32 - i32.add - i32.const -8 - i32.and - i32.const -8 - i32.add - local.tee 0 - local.get 0 - local.get 1 - i32.const 16 - i32.add - i32.lt_u - select - local.tee 6 - i32.const 27 - i32.store offset=4 - i32.const 0 - i64.load offset=1048704 align=4 - local.set 9 - local.get 6 - i32.const 16 - i32.add - i32.const 0 - i64.load offset=1048712 align=4 - i64.store align=4 - local.get 6 - local.get 9 - i64.store offset=8 align=4 - i32.const 0 - local.get 8 - i32.store offset=1048708 - i32.const 0 - local.get 7 - i32.store offset=1048704 - i32.const 0 - local.get 6 - i32.const 8 - i32.add - i32.store offset=1048712 - i32.const 0 - i32.const 0 - i32.store offset=1048716 - local.get 6 - i32.const 28 - i32.add - local.set 0 loop ;; label = @14 local.get 0 - i32.const 7 - i32.store + i32.load + local.get 5 + i32.eq + br_if 1 (;@13;) local.get 0 - i32.const 4 - i32.add + i32.load offset=8 local.tee 0 - local.get 5 - i32.lt_u br_if 0 (;@14;) + br 2 (;@12;) end - local.get 6 - local.get 1 - i32.eq - br_if 11 (;@2;) - local.get 6 - local.get 6 - i32.load offset=4 - i32.const -2 - i32.and - i32.store offset=4 - local.get 1 - local.get 6 - local.get 1 - i32.sub - local.tee 0 - i32.const 1 - i32.or - i32.store offset=4 - local.get 6 - local.get 0 - i32.store + end + local.get 0 + i32.load offset=12 + i32.eqz + br_if 1 (;@11;) + end + i32.const 1048704 + local.set 0 + block ;; label = @12 + loop ;; label = @13 block ;; label = @14 local.get 0 - i32.const 256 - i32.lt_u - br_if 0 (;@14;) + i32.load + local.tee 5 local.get 1 - local.get 0 - call 8 - br 12 (;@2;) - end - local.get 0 - i32.const -8 - i32.and - i32.const 1048720 - i32.add - local.set 5 - block ;; label = @14 - block ;; label = @15 - i32.const 0 - i32.load offset=1048984 - local.tee 7 - i32.const 1 - local.get 0 - i32.const 3 - i32.shr_u - i32.shl - local.tee 0 - i32.and - i32.eqz - br_if 0 (;@15;) - local.get 5 - i32.load offset=8 - local.set 0 - br 1 (;@14;) - end - i32.const 0 - local.get 7 - local.get 0 - i32.or - i32.store offset=1048984 + i32.gt_u + br_if 0 (;@14;) local.get 5 - local.set 0 + local.get 0 + i32.load offset=4 + i32.add + local.tee 5 + local.get 1 + i32.gt_u + br_if 2 (;@12;) end - local.get 5 - local.get 1 - i32.store offset=8 - local.get 0 - local.get 1 - i32.store offset=12 - local.get 1 - local.get 5 - i32.store offset=12 - local.get 1 local.get 0 - i32.store offset=8 - br 11 (;@2;) + i32.load offset=8 + local.set 0 + br 0 (;@13;) end + end + i32.const 0 + local.get 6 + i32.store offset=1049004 + i32.const 0 + local.get 8 + i32.const -40 + i32.add + local.tee 0 + i32.store offset=1048996 + local.get 6 + local.get 0 + i32.const 1 + i32.or + i32.store offset=4 + local.get 6 + local.get 0 + i32.add + i32.const 40 + i32.store offset=4 + i32.const 0 + i32.const 2097152 + i32.store offset=1049016 + local.get 1 + local.get 5 + i32.const -32 + i32.add + i32.const -8 + i32.and + i32.const -8 + i32.add + local.tee 0 + local.get 0 + local.get 1 + i32.const 16 + i32.add + i32.lt_u + select + local.tee 7 + i32.const 27 + i32.store offset=4 + i32.const 0 + i64.load offset=1048704 align=4 + local.set 9 + local.get 7 + i32.const 16 + i32.add + i32.const 0 + i64.load offset=1048712 align=4 + i64.store align=4 + local.get 7 + local.get 9 + i64.store offset=8 align=4 + i32.const 0 + local.get 8 + i32.store offset=1048708 + i32.const 0 + local.get 6 + i32.store offset=1048704 + i32.const 0 + local.get 7 + i32.const 8 + i32.add + i32.store offset=1048712 + i32.const 0 + i32.const 0 + i32.store offset=1048716 + local.get 7 + i32.const 28 + i32.add + local.set 0 + loop ;; label = @12 local.get 0 - local.get 7 + i32.const 7 i32.store local.get 0 + i32.const 4 + i32.add + local.tee 0 + local.get 5 + i32.lt_u + br_if 0 (;@12;) + end + local.get 7 + local.get 1 + i32.eq + br_if 9 (;@2;) + local.get 7 + local.get 7 + i32.load offset=4 + i32.const -2 + i32.and + i32.store offset=4 + local.get 1 + local.get 7 + local.get 1 + i32.sub + local.tee 0 + i32.const 1 + i32.or + i32.store offset=4 + local.get 7 + local.get 0 + i32.store + block ;; label = @12 + local.get 0 + i32.const 256 + i32.lt_u + br_if 0 (;@12;) + local.get 1 local.get 0 - i32.load offset=4 - local.get 8 - i32.add - i32.store offset=4 - local.get 7 - local.get 2 - i32.const 3 - i32.or - i32.store offset=4 - local.get 5 - local.get 7 - local.get 2 - i32.add - local.tee 0 - i32.sub - local.set 2 + call 8 + br 10 (;@2;) + end + local.get 0 + i32.const -8 + i32.and + i32.const 1048720 + i32.add + local.set 5 + block ;; label = @12 block ;; label = @13 - local.get 5 - i32.const 0 - i32.load offset=1049004 - i32.eq - br_if 0 (;@13;) - local.get 5 i32.const 0 - i32.load offset=1049000 - i32.eq - br_if 5 (;@8;) - local.get 5 - i32.load offset=4 - local.tee 1 + i32.load offset=1048984 + local.tee 6 + i32.const 1 + local.get 0 i32.const 3 + i32.shr_u + i32.shl + local.tee 0 i32.and - i32.const 1 - i32.ne - br_if 8 (;@5;) - block ;; label = @14 - block ;; label = @15 - local.get 1 - i32.const -8 - i32.and - local.tee 6 - i32.const 256 - i32.lt_u - br_if 0 (;@15;) - local.get 5 - call 7 - br 1 (;@14;) - end - block ;; label = @15 - local.get 5 - i32.const 12 - i32.add - i32.load - local.tee 8 - local.get 5 - i32.const 8 - i32.add - i32.load - local.tee 4 - i32.eq - br_if 0 (;@15;) - local.get 4 - local.get 8 - i32.store offset=12 - local.get 8 - local.get 4 - i32.store offset=8 - br 1 (;@14;) - end - i32.const 0 - i32.const 0 - i32.load offset=1048984 - i32.const -2 - local.get 1 - i32.const 3 - i32.shr_u - i32.rotl - i32.and - i32.store offset=1048984 - end + br_if 0 (;@13;) + i32.const 0 local.get 6 - local.get 2 - i32.add - local.set 2 + local.get 0 + i32.or + i32.store offset=1048984 local.get 5 - local.get 6 - i32.add - local.tee 5 - i32.load offset=4 - local.set 1 - br 8 (;@5;) + local.set 0 + br 1 (;@12;) end - i32.const 0 - local.get 0 - i32.store offset=1049004 - i32.const 0 - i32.const 0 - i32.load offset=1048996 - local.get 2 - i32.add - local.tee 2 - i32.store offset=1048996 - local.get 0 - local.get 2 - i32.const 1 - i32.or - i32.store offset=4 - br 8 (;@4;) + local.get 5 + i32.load offset=8 + local.set 0 end - i32.const 0 - local.get 0 - local.get 2 - i32.sub - local.tee 1 - i32.store offset=1048996 - i32.const 0 - i32.const 0 - i32.load offset=1049004 - local.tee 0 - local.get 2 - i32.add - local.tee 5 - i32.store offset=1049004 local.get 5 local.get 1 - i32.const 1 - i32.or - i32.store offset=4 + i32.store offset=8 local.get 0 - local.get 2 - i32.const 3 - i32.or - i32.store offset=4 + local.get 1 + i32.store offset=12 + local.get 1 + local.get 5 + i32.store offset=12 + local.get 1 local.get 0 - i32.const 8 - i32.add - local.set 1 - br 10 (;@1;) + i32.store offset=8 + br 9 (;@2;) end - i32.const 0 - i32.load offset=1049000 - local.set 1 local.get 0 + local.get 6 + i32.store + local.get 0 + local.get 0 + i32.load offset=4 + local.get 8 + i32.add + i32.store offset=4 + local.get 6 local.get 2 + i32.const 3 + i32.or + i32.store offset=4 + local.get 5 + local.get 6 + local.get 2 + i32.add + local.tee 0 i32.sub - local.tee 5 - i32.const 16 - i32.lt_u - br_if 3 (;@7;) + local.set 1 + local.get 5 i32.const 0 + i32.load offset=1049004 + i32.eq + br_if 3 (;@7;) local.get 5 - i32.store offset=1048992 i32.const 0 - local.get 1 - local.get 2 - i32.add - local.tee 7 - i32.store offset=1049000 - local.get 7 + i32.load offset=1049000 + i32.eq + br_if 4 (;@6;) + block ;; label = @11 + local.get 5 + i32.load offset=4 + local.tee 2 + i32.const 3 + i32.and + i32.const 1 + i32.ne + br_if 0 (;@11;) + block ;; label = @12 + block ;; label = @13 + local.get 2 + i32.const -8 + i32.and + local.tee 7 + i32.const 256 + i32.lt_u + br_if 0 (;@13;) + local.get 5 + call 7 + br 1 (;@12;) + end + block ;; label = @13 + local.get 5 + i32.const 12 + i32.add + i32.load + local.tee 8 + local.get 5 + i32.const 8 + i32.add + i32.load + local.tee 4 + i32.eq + br_if 0 (;@13;) + local.get 4 + local.get 8 + i32.store offset=12 + local.get 8 + local.get 4 + i32.store offset=8 + br 1 (;@12;) + end + i32.const 0 + i32.const 0 + i32.load offset=1048984 + i32.const -2 + local.get 2 + i32.const 3 + i32.shr_u + i32.rotl + i32.and + i32.store offset=1048984 + end + local.get 7 + local.get 1 + i32.add + local.set 1 + local.get 5 + local.get 7 + i32.add + local.tee 5 + i32.load offset=4 + local.set 2 + end local.get 5 + local.get 2 + i32.const -2 + i32.and + i32.store offset=4 + local.get 0 + local.get 1 i32.const 1 i32.or i32.store offset=4 + local.get 0 + local.get 1 + i32.add + local.get 1 + i32.store + block ;; label = @11 + local.get 1 + i32.const 256 + i32.lt_u + br_if 0 (;@11;) + local.get 0 + local.get 1 + call 8 + br 8 (;@3;) + end + local.get 1 + i32.const -8 + i32.and + i32.const 1048720 + i32.add + local.set 5 + block ;; label = @11 + block ;; label = @12 + i32.const 0 + i32.load offset=1048984 + local.tee 2 + i32.const 1 + local.get 1 + i32.const 3 + i32.shr_u + i32.shl + local.tee 1 + i32.and + br_if 0 (;@12;) + i32.const 0 + local.get 2 + local.get 1 + i32.or + i32.store offset=1048984 + local.get 5 + local.set 1 + br 1 (;@11;) + end + local.get 5 + i32.load offset=8 + local.set 1 + end + local.get 5 + local.get 0 + i32.store offset=8 local.get 1 local.get 0 - i32.add + i32.store offset=12 + local.get 0 local.get 5 - i32.store + i32.store offset=12 + local.get 0 local.get 1 - local.get 2 - i32.const 3 - i32.or - i32.store offset=4 - br 4 (;@6;) + i32.store offset=8 + br 7 (;@3;) end i32.const 0 - local.get 7 - i32.store offset=1049020 - br 6 (;@3;) + local.get 0 + local.get 2 + i32.sub + local.tee 1 + i32.store offset=1048996 + i32.const 0 + i32.const 0 + i32.load offset=1049004 + local.tee 0 + local.get 2 + i32.add + local.tee 5 + i32.store offset=1049004 + local.get 5 + local.get 1 + i32.const 1 + i32.or + i32.store offset=4 + local.get 0 + local.get 2 + i32.const 3 + i32.or + i32.store offset=4 + local.get 0 + i32.const 8 + i32.add + local.set 1 + br 8 (;@1;) end + i32.const 0 + i32.load offset=1049000 + local.set 1 local.get 0 - local.get 6 - local.get 8 - i32.add - i32.store offset=4 + local.get 2 + i32.sub + local.tee 5 + i32.const 16 + i32.lt_u + br_if 3 (;@5;) i32.const 0 - i32.load offset=1049004 + local.get 5 + i32.store offset=1048992 i32.const 0 - i32.load offset=1048996 - local.get 8 + local.get 1 + local.get 2 i32.add - call 9 - br 6 (;@2;) + local.tee 6 + i32.store offset=1049000 + local.get 6 + local.get 5 + i32.const 1 + i32.or + i32.store offset=4 + local.get 1 + local.get 0 + i32.add + local.get 5 + i32.store + local.get 1 + local.get 2 + i32.const 3 + i32.or + i32.store offset=4 + br 4 (;@4;) end - i32.const 0 local.get 0 - i32.store offset=1049000 - i32.const 0 - i32.const 0 - i32.load offset=1048992 - local.get 2 + local.get 7 + local.get 8 i32.add - local.tee 2 - i32.store offset=1048992 - local.get 0 - local.get 2 - i32.const 1 - i32.or i32.store offset=4 - local.get 0 - local.get 2 + i32.const 0 + i32.load offset=1049004 + i32.const 0 + i32.load offset=1048996 + local.get 8 i32.add - local.get 2 - i32.store - br 3 (;@4;) + call 9 + br 5 (;@2;) end i32.const 0 - i32.const 0 - i32.store offset=1049000 + local.get 0 + i32.store offset=1049004 i32.const 0 i32.const 0 - i32.store offset=1048992 - local.get 1 - local.get 0 - i32.const 3 - i32.or - i32.store offset=4 + i32.load offset=1048996 local.get 1 - local.get 0 i32.add - local.tee 0 + local.tee 1 + i32.store offset=1048996 local.get 0 - i32.load offset=4 - i32.const 1 - i32.or - i32.store offset=4 - end - local.get 1 - i32.const 8 - i32.add - return - end - local.get 5 - local.get 1 - i32.const -2 - i32.and - i32.store offset=4 - local.get 0 - local.get 2 - i32.const 1 - i32.or - i32.store offset=4 - local.get 0 - local.get 2 - i32.add - local.get 2 - i32.store - block ;; label = @5 - local.get 2 - i32.const 256 - i32.lt_u - br_if 0 (;@5;) - local.get 0 - local.get 2 - call 8 - br 1 (;@4;) - end - local.get 2 - i32.const -8 - i32.and - i32.const 1048720 - i32.add - local.set 1 - block ;; label = @5 - block ;; label = @6 - i32.const 0 - i32.load offset=1048984 - local.tee 5 - i32.const 1 - local.get 2 - i32.const 3 - i32.shr_u - i32.shl - local.tee 2 - i32.and - i32.eqz - br_if 0 (;@6;) - local.get 1 - i32.load offset=8 - local.set 2 - br 1 (;@5;) - end - i32.const 0 - local.get 5 - local.get 2 - i32.or - i32.store offset=1048984 - local.get 1 - local.set 2 - end - local.get 1 - local.get 0 - i32.store offset=8 - local.get 2 - local.get 0 - i32.store offset=12 - local.get 0 - local.get 1 - i32.store offset=12 - local.get 0 - local.get 2 - i32.store offset=8 - end - local.get 7 - i32.const 8 - i32.add - return - end - i32.const 0 - i32.const 4095 - i32.store offset=1049024 - i32.const 0 - local.get 8 - i32.store offset=1048708 - i32.const 0 - local.get 7 - i32.store offset=1048704 - i32.const 0 - i32.const 1048720 - i32.store offset=1048732 - i32.const 0 - i32.const 1048728 - i32.store offset=1048740 - i32.const 0 - i32.const 1048720 - i32.store offset=1048728 - i32.const 0 - i32.const 1048736 - i32.store offset=1048748 - i32.const 0 - i32.const 1048728 - i32.store offset=1048736 - i32.const 0 - i32.const 1048744 - i32.store offset=1048756 - i32.const 0 - i32.const 1048736 - i32.store offset=1048744 - i32.const 0 - i32.const 1048752 - i32.store offset=1048764 - i32.const 0 - i32.const 1048744 - i32.store offset=1048752 - i32.const 0 - i32.const 1048760 - i32.store offset=1048772 - i32.const 0 - i32.const 1048752 - i32.store offset=1048760 - i32.const 0 - i32.const 1048768 - i32.store offset=1048780 - i32.const 0 - i32.const 1048760 - i32.store offset=1048768 - i32.const 0 - i32.const 1048776 - i32.store offset=1048788 - i32.const 0 - i32.const 1048768 - i32.store offset=1048776 - i32.const 0 - i32.const 0 - i32.store offset=1048716 - i32.const 0 - i32.const 1048784 - i32.store offset=1048796 - i32.const 0 - i32.const 1048776 - i32.store offset=1048784 - i32.const 0 - i32.const 1048784 - i32.store offset=1048792 - i32.const 0 - i32.const 1048792 - i32.store offset=1048804 - i32.const 0 - i32.const 1048792 - i32.store offset=1048800 - i32.const 0 - i32.const 1048800 - i32.store offset=1048812 - i32.const 0 - i32.const 1048800 - i32.store offset=1048808 - i32.const 0 - i32.const 1048808 - i32.store offset=1048820 - i32.const 0 - i32.const 1048808 - i32.store offset=1048816 - i32.const 0 - i32.const 1048816 - i32.store offset=1048828 - i32.const 0 - i32.const 1048816 - i32.store offset=1048824 - i32.const 0 - i32.const 1048824 - i32.store offset=1048836 - i32.const 0 - i32.const 1048824 - i32.store offset=1048832 - i32.const 0 - i32.const 1048832 - i32.store offset=1048844 - i32.const 0 - i32.const 1048832 - i32.store offset=1048840 - i32.const 0 - i32.const 1048840 - i32.store offset=1048852 - i32.const 0 - i32.const 1048840 - i32.store offset=1048848 - i32.const 0 - i32.const 1048848 - i32.store offset=1048860 - i32.const 0 - i32.const 1048856 - i32.store offset=1048868 - i32.const 0 - i32.const 1048848 - i32.store offset=1048856 - i32.const 0 - i32.const 1048864 - i32.store offset=1048876 - i32.const 0 - i32.const 1048856 - i32.store offset=1048864 - i32.const 0 - i32.const 1048872 - i32.store offset=1048884 - i32.const 0 - i32.const 1048864 - i32.store offset=1048872 - i32.const 0 - i32.const 1048880 - i32.store offset=1048892 - i32.const 0 - i32.const 1048872 - i32.store offset=1048880 - i32.const 0 - i32.const 1048888 - i32.store offset=1048900 - i32.const 0 - i32.const 1048880 - i32.store offset=1048888 - i32.const 0 - i32.const 1048896 - i32.store offset=1048908 - i32.const 0 - i32.const 1048888 - i32.store offset=1048896 - i32.const 0 - i32.const 1048904 - i32.store offset=1048916 - i32.const 0 - i32.const 1048896 - i32.store offset=1048904 - i32.const 0 - i32.const 1048912 - i32.store offset=1048924 - i32.const 0 - i32.const 1048904 - i32.store offset=1048912 - i32.const 0 - i32.const 1048920 - i32.store offset=1048932 - i32.const 0 - i32.const 1048912 - i32.store offset=1048920 - i32.const 0 - i32.const 1048928 - i32.store offset=1048940 - i32.const 0 - i32.const 1048920 - i32.store offset=1048928 - i32.const 0 - i32.const 1048936 - i32.store offset=1048948 - i32.const 0 - i32.const 1048928 - i32.store offset=1048936 - i32.const 0 - i32.const 1048944 - i32.store offset=1048956 - i32.const 0 - i32.const 1048936 - i32.store offset=1048944 - i32.const 0 - i32.const 1048952 - i32.store offset=1048964 - i32.const 0 - i32.const 1048944 - i32.store offset=1048952 - i32.const 0 - i32.const 1048960 - i32.store offset=1048972 - i32.const 0 - i32.const 1048952 - i32.store offset=1048960 - i32.const 0 - i32.const 1048968 - i32.store offset=1048980 - i32.const 0 - i32.const 1048960 - i32.store offset=1048968 - i32.const 0 - local.get 7 - i32.store offset=1049004 - i32.const 0 - i32.const 1048968 - i32.store offset=1048976 - i32.const 0 - local.get 8 - i32.const -40 - i32.add - local.tee 0 - i32.store offset=1048996 - local.get 7 - local.get 0 - i32.const 1 - i32.or - i32.store offset=4 - local.get 7 - local.get 0 + local.get 1 + i32.const 1 + i32.or + i32.store offset=4 + br 3 (;@3;) + end + i32.const 0 + local.get 0 + i32.store offset=1049000 + i32.const 0 + i32.const 0 + i32.load offset=1048992 + local.get 1 + i32.add + local.tee 1 + i32.store offset=1048992 + local.get 0 + local.get 1 + i32.const 1 + i32.or + i32.store offset=4 + local.get 0 + local.get 1 + i32.add + local.get 1 + i32.store + br 2 (;@3;) + end + i32.const 0 + i32.const 0 + i32.store offset=1049000 + i32.const 0 + i32.const 0 + i32.store offset=1048992 + local.get 1 + local.get 0 + i32.const 3 + i32.or + i32.store offset=4 + local.get 1 + local.get 0 + i32.add + local.tee 0 + local.get 0 + i32.load offset=4 + i32.const 1 + i32.or + i32.store offset=4 + end + local.get 1 + i32.const 8 + i32.add + return + end + local.get 6 + i32.const 8 i32.add - i32.const 40 - i32.store offset=4 - i32.const 0 - i32.const 2097152 - i32.store offset=1049016 + return end i32.const 0 local.set 1 @@ -2054,92 +2057,99 @@ i32.and i32.store offset=1048984 end - block ;; label = @3 - local.get 2 - i32.load offset=4 - local.tee 3 - i32.const 2 - i32.and - i32.eqz - br_if 0 (;@3;) - local.get 2 - local.get 3 - i32.const -2 - i32.and - i32.store offset=4 - local.get 0 - local.get 1 - i32.const 1 - i32.or - i32.store offset=4 - local.get 0 - local.get 1 - i32.add - local.get 1 - i32.store - br 2 (;@1;) - end block ;; label = @3 block ;; label = @4 - local.get 2 - i32.const 0 - i32.load offset=1049004 - i32.eq - br_if 0 (;@4;) - local.get 2 - i32.const 0 - i32.load offset=1049000 - i32.eq - br_if 1 (;@3;) - local.get 3 - i32.const -8 - i32.and - local.tee 4 - local.get 1 - i32.add - local.set 1 block ;; label = @5 - block ;; label = @6 - local.get 4 - i32.const 256 - i32.lt_u - br_if 0 (;@6;) - local.get 2 - call 7 - br 1 (;@5;) - end - block ;; label = @6 - local.get 2 - i32.const 12 - i32.add - i32.load - local.tee 4 - local.get 2 - i32.const 8 - i32.add - i32.load - local.tee 2 - i32.eq - br_if 0 (;@6;) - local.get 2 - local.get 4 - i32.store offset=12 - local.get 4 - local.get 2 - i32.store offset=8 - br 1 (;@5;) - end + local.get 2 + i32.load offset=4 + local.tee 3 + i32.const 2 + i32.and + br_if 0 (;@5;) + local.get 2 i32.const 0 + i32.load offset=1049004 + i32.eq + br_if 2 (;@3;) + local.get 2 i32.const 0 - i32.load offset=1048984 - i32.const -2 + i32.load offset=1049000 + i32.eq + br_if 4 (;@1;) local.get 3 - i32.const 3 - i32.shr_u - i32.rotl + i32.const -8 i32.and - i32.store offset=1048984 + local.tee 4 + local.get 1 + i32.add + local.set 1 + block ;; label = @6 + block ;; label = @7 + local.get 4 + i32.const 256 + i32.lt_u + br_if 0 (;@7;) + local.get 2 + call 7 + br 1 (;@6;) + end + block ;; label = @7 + local.get 2 + i32.const 12 + i32.add + i32.load + local.tee 4 + local.get 2 + i32.const 8 + i32.add + i32.load + local.tee 2 + i32.eq + br_if 0 (;@7;) + local.get 2 + local.get 4 + i32.store offset=12 + local.get 4 + local.get 2 + i32.store offset=8 + br 1 (;@6;) + end + i32.const 0 + i32.const 0 + i32.load offset=1048984 + i32.const -2 + local.get 3 + i32.const 3 + i32.shr_u + i32.rotl + i32.and + i32.store offset=1048984 + end + local.get 0 + local.get 1 + i32.const 1 + i32.or + i32.store offset=4 + local.get 0 + local.get 1 + i32.add + local.get 1 + i32.store + local.get 0 + i32.const 0 + i32.load offset=1049000 + i32.ne + br_if 1 (;@4;) + i32.const 0 + local.get 1 + i32.store offset=1048992 + return end + local.get 2 + local.get 3 + i32.const -2 + i32.and + i32.store offset=4 local.get 0 local.get 1 i32.const 1 @@ -2150,123 +2160,112 @@ i32.add local.get 1 i32.store + end + block ;; label = @4 + local.get 1 + i32.const 256 + i32.lt_u + br_if 0 (;@4;) local.get 0 - i32.const 0 - i32.load offset=1049000 - i32.ne - br_if 3 (;@1;) - i32.const 0 local.get 1 - i32.store offset=1048992 - br 2 (;@2;) + call 8 + return end - i32.const 0 - local.get 0 - i32.store offset=1049004 - i32.const 0 - i32.const 0 - i32.load offset=1048996 local.get 1 + i32.const -8 + i32.and + i32.const 1048720 i32.add - local.tee 1 - i32.store offset=1048996 + local.set 2 + block ;; label = @4 + block ;; label = @5 + i32.const 0 + i32.load offset=1048984 + local.tee 3 + i32.const 1 + local.get 1 + i32.const 3 + i32.shr_u + i32.shl + local.tee 1 + i32.and + br_if 0 (;@5;) + i32.const 0 + local.get 3 + local.get 1 + i32.or + i32.store offset=1048984 + local.get 2 + local.set 1 + br 1 (;@4;) + end + local.get 2 + i32.load offset=8 + local.set 1 + end + local.get 2 local.get 0 + i32.store offset=8 local.get 1 - i32.const 1 - i32.or - i32.store offset=4 local.get 0 - i32.const 0 - i32.load offset=1049000 - i32.ne - br_if 1 (;@2;) - i32.const 0 - i32.const 0 - i32.store offset=1048992 - i32.const 0 - i32.const 0 - i32.store offset=1049000 + i32.store offset=12 + local.get 0 + local.get 2 + i32.store offset=12 + local.get 0 + local.get 1 + i32.store offset=8 return end i32.const 0 local.get 0 - i32.store offset=1049000 + i32.store offset=1049004 i32.const 0 i32.const 0 - i32.load offset=1048992 + i32.load offset=1048996 local.get 1 i32.add local.tee 1 - i32.store offset=1048992 + i32.store offset=1048996 local.get 0 local.get 1 i32.const 1 i32.or i32.store offset=4 local.get 0 - local.get 1 - i32.add - local.get 1 - i32.store - return - end - return - end - block ;; label = @1 - local.get 1 - i32.const 256 - i32.lt_u - br_if 0 (;@1;) - local.get 0 - local.get 1 - call 8 - return - end - local.get 1 - i32.const -8 - i32.and - i32.const 1048720 - i32.add - local.set 2 - block ;; label = @1 - block ;; label = @2 i32.const 0 - i32.load offset=1048984 - local.tee 3 - i32.const 1 - local.get 1 - i32.const 3 - i32.shr_u - i32.shl - local.tee 1 - i32.and - i32.eqz + i32.load offset=1049000 + i32.ne br_if 0 (;@2;) - local.get 2 - i32.load offset=8 - local.set 1 - br 1 (;@1;) + i32.const 0 + i32.const 0 + i32.store offset=1048992 + i32.const 0 + i32.const 0 + i32.store offset=1049000 end - i32.const 0 - local.get 3 - local.get 1 - i32.or - i32.store offset=1048984 - local.get 2 - local.set 1 + return end - local.get 2 + i32.const 0 local.get 0 - i32.store offset=8 + i32.store offset=1049000 + i32.const 0 + i32.const 0 + i32.load offset=1048992 local.get 1 + i32.add + local.tee 1 + i32.store offset=1048992 local.get 0 - i32.store offset=12 - local.get 0 - local.get 2 - i32.store offset=12 + local.get 1 + i32.const 1 + i32.or + i32.store offset=4 local.get 0 local.get 1 - i32.store offset=8 + i32.add + local.get 1 + i32.store ) (func (;7;) (type 4) (param i32) (local i32 i32 i32 i32 i32) @@ -2470,45 +2469,44 @@ i32.add local.set 3 block ;; label = @1 + block ;; label = @2 + i32.const 0 + i32.load offset=1048988 + local.tee 4 + i32.const 1 + local.get 2 + i32.shl + local.tee 5 + i32.and + br_if 0 (;@2;) + i32.const 0 + local.get 4 + local.get 5 + i32.or + i32.store offset=1048988 + local.get 3 + local.get 0 + i32.store + local.get 0 + local.get 3 + i32.store offset=24 + br 1 (;@1;) + end block ;; label = @2 block ;; label = @3 block ;; label = @4 - block ;; label = @5 - i32.const 0 - i32.load offset=1048988 - local.tee 4 - i32.const 1 - local.get 2 - i32.shl - local.tee 5 - i32.and - i32.eqz - br_if 0 (;@5;) - local.get 3 - i32.load - local.tee 4 - i32.load offset=4 - i32.const -8 - i32.and - local.get 1 - i32.ne - br_if 1 (;@4;) - local.get 4 - local.set 2 - br 2 (;@3;) - end - i32.const 0 - local.get 4 - local.get 5 - i32.or - i32.store offset=1048988 local.get 3 - local.get 0 - i32.store - local.get 0 - local.get 3 - i32.store offset=24 - br 3 (;@1;) + i32.load + local.tee 4 + i32.load offset=4 + i32.const -8 + i32.and + local.get 1 + i32.ne + br_if 0 (;@4;) + local.get 4 + local.set 2 + br 1 (;@3;) end local.get 1 i32.const 0 @@ -2643,137 +2641,118 @@ local.set 3 block ;; label = @1 block ;; label = @2 - local.get 2 - i32.const 1 - i32.and - br_if 0 (;@2;) - local.get 2 - i32.const 3 - i32.and - i32.eqz - br_if 1 (;@1;) - local.get 1 - i32.load - local.tee 2 - local.get 0 - i32.add - local.set 0 - block ;; label = @3 - local.get 1 - local.get 2 - i32.sub - local.tee 1 - i32.const 0 - i32.load offset=1049000 - i32.ne - br_if 0 (;@3;) - local.get 3 - i32.load offset=4 - i32.const 3 - i32.and - i32.const 3 - i32.ne - br_if 1 (;@2;) - i32.const 0 - local.get 0 - i32.store offset=1048992 - local.get 3 - local.get 3 - i32.load offset=4 - i32.const -2 - i32.and - i32.store offset=4 - local.get 1 - local.get 0 - i32.const 1 - i32.or - i32.store offset=4 - local.get 3 - local.get 0 - i32.store - return - end - block ;; label = @3 - local.get 2 - i32.const 256 - i32.lt_u - br_if 0 (;@3;) - local.get 1 - call 7 - br 1 (;@2;) - end - block ;; label = @3 - local.get 1 - i32.const 12 - i32.add - i32.load - local.tee 4 - local.get 1 - i32.const 8 - i32.add - i32.load - local.tee 5 - i32.eq - br_if 0 (;@3;) - local.get 5 - local.get 4 - i32.store offset=12 - local.get 4 - local.get 5 - i32.store offset=8 - br 1 (;@2;) - end - i32.const 0 - i32.const 0 - i32.load offset=1048984 - i32.const -2 - local.get 2 - i32.const 3 - i32.shr_u - i32.rotl - i32.and - i32.store offset=1048984 - end - block ;; label = @2 - block ;; label = @3 - local.get 3 - i32.load offset=4 - local.tee 2 - i32.const 2 - i32.and - i32.eqz - br_if 0 (;@3;) - local.get 3 - local.get 2 - i32.const -2 - i32.and - i32.store offset=4 - local.get 1 - local.get 0 - i32.const 1 - i32.or - i32.store offset=4 - local.get 1 - local.get 0 - i32.add - local.get 0 - i32.store - br 1 (;@2;) - end block ;; label = @3 + block ;; label = @4 + local.get 2 + i32.const 1 + i32.and + br_if 0 (;@4;) + local.get 2 + i32.const 3 + i32.and + i32.eqz + br_if 1 (;@3;) + local.get 1 + i32.load + local.tee 2 + local.get 0 + i32.add + local.set 0 + block ;; label = @5 + local.get 1 + local.get 2 + i32.sub + local.tee 1 + i32.const 0 + i32.load offset=1049000 + i32.ne + br_if 0 (;@5;) + local.get 3 + i32.load offset=4 + i32.const 3 + i32.and + i32.const 3 + i32.ne + br_if 1 (;@4;) + i32.const 0 + local.get 0 + i32.store offset=1048992 + local.get 3 + local.get 3 + i32.load offset=4 + i32.const -2 + i32.and + i32.store offset=4 + local.get 1 + local.get 0 + i32.const 1 + i32.or + i32.store offset=4 + local.get 3 + local.get 0 + i32.store + return + end + block ;; label = @5 + local.get 2 + i32.const 256 + i32.lt_u + br_if 0 (;@5;) + local.get 1 + call 7 + br 1 (;@4;) + end + block ;; label = @5 + local.get 1 + i32.const 12 + i32.add + i32.load + local.tee 4 + local.get 1 + i32.const 8 + i32.add + i32.load + local.tee 5 + i32.eq + br_if 0 (;@5;) + local.get 5 + local.get 4 + i32.store offset=12 + local.get 4 + local.get 5 + i32.store offset=8 + br 1 (;@4;) + end + i32.const 0 + i32.const 0 + i32.load offset=1048984 + i32.const -2 + local.get 2 + i32.const 3 + i32.shr_u + i32.rotl + i32.and + i32.store offset=1048984 + end block ;; label = @4 block ;; label = @5 block ;; label = @6 + local.get 3 + i32.load offset=4 + local.tee 2 + i32.const 2 + i32.and + br_if 0 (;@6;) local.get 3 i32.const 0 i32.load offset=1049004 i32.eq - br_if 0 (;@6;) + br_if 2 (;@4;) local.get 3 i32.const 0 i32.load offset=1049000 i32.eq - br_if 1 (;@5;) + br_if 5 (;@1;) local.get 2 i32.const -8 i32.and @@ -2837,133 +2816,126 @@ i32.const 0 i32.load offset=1049000 i32.ne - br_if 4 (;@2;) + br_if 1 (;@5;) i32.const 0 local.get 0 i32.store offset=1048992 return end - i32.const 0 - local.get 1 - i32.store offset=1049004 - i32.const 0 - i32.const 0 - i32.load offset=1048996 - local.get 0 - i32.add - local.tee 0 - i32.store offset=1048996 + local.get 3 + local.get 2 + i32.const -2 + i32.and + i32.store offset=4 local.get 1 local.get 0 i32.const 1 i32.or i32.store offset=4 local.get 1 - i32.const 0 - i32.load offset=1049000 - i32.eq - br_if 1 (;@4;) - br 2 (;@3;) + local.get 0 + i32.add + local.get 0 + i32.store end - i32.const 0 + local.get 0 + i32.const 256 + i32.lt_u + br_if 2 (;@2;) local.get 1 - i32.store offset=1049000 + local.get 0 + call 8 i32.const 0 i32.const 0 - i32.load offset=1048992 - local.get 0 + i32.load offset=1049024 + i32.const -1 i32.add - local.tee 0 - i32.store offset=1048992 - local.get 1 - local.get 0 - i32.const 1 - i32.or - i32.store offset=4 + local.tee 1 + i32.store offset=1049024 local.get 1 - local.get 0 - i32.add - local.get 0 - i32.store + br_if 1 (;@3;) + call 11 return end i32.const 0 + local.get 1 + i32.store offset=1049004 i32.const 0 - i32.store offset=1048992 i32.const 0 + i32.load offset=1048996 + local.get 0 + i32.add + local.tee 0 + i32.store offset=1048996 + local.get 1 + local.get 0 + i32.const 1 + i32.or + i32.store offset=4 + block ;; label = @4 + local.get 1 + i32.const 0 + i32.load offset=1049000 + i32.ne + br_if 0 (;@4;) + i32.const 0 + i32.const 0 + i32.store offset=1048992 + i32.const 0 + i32.const 0 + i32.store offset=1049000 + end + local.get 0 i32.const 0 - i32.store offset=1049000 - end - local.get 0 - i32.const 0 - i32.load offset=1049016 - i32.le_u - br_if 1 (;@1;) - i32.const 0 - i32.load offset=1049004 - local.tee 0 - i32.eqz - br_if 1 (;@1;) - block ;; label = @3 + i32.load offset=1049016 + i32.le_u + br_if 0 (;@3;) i32.const 0 - i32.load offset=1048996 - i32.const 41 - i32.lt_u + i32.load offset=1049004 + local.tee 0 + i32.eqz br_if 0 (;@3;) - i32.const 1048704 - local.set 1 - loop ;; label = @4 - block ;; label = @5 + block ;; label = @4 + i32.const 0 + i32.load offset=1048996 + i32.const 41 + i32.lt_u + br_if 0 (;@4;) + i32.const 1048704 + local.set 1 + loop ;; label = @5 + block ;; label = @6 + local.get 1 + i32.load + local.tee 3 + local.get 0 + i32.gt_u + br_if 0 (;@6;) + local.get 3 + local.get 1 + i32.load offset=4 + i32.add + local.get 0 + i32.gt_u + br_if 2 (;@4;) + end local.get 1 - i32.load - local.tee 3 - local.get 0 - i32.gt_u + i32.load offset=8 + local.tee 1 br_if 0 (;@5;) - local.get 3 - local.get 1 - i32.load offset=4 - i32.add - local.get 0 - i32.gt_u - br_if 2 (;@3;) end - local.get 1 - i32.load offset=8 - local.tee 1 - br_if 0 (;@4;) end + call 11 + i32.const 0 + i32.load offset=1048996 + i32.const 0 + i32.load offset=1049016 + i32.le_u + br_if 0 (;@3;) + i32.const 0 + i32.const -1 + i32.store offset=1049016 end - call 11 - i32.const 0 - i32.load offset=1048996 - i32.const 0 - i32.load offset=1049016 - i32.le_u - br_if 1 (;@1;) - i32.const 0 - i32.const -1 - i32.store offset=1049016 - return - end - block ;; label = @2 - local.get 0 - i32.const 256 - i32.lt_u - br_if 0 (;@2;) - local.get 1 - local.get 0 - call 8 - i32.const 0 - i32.const 0 - i32.load offset=1049024 - i32.const -1 - i32.add - local.tee 1 - i32.store offset=1049024 - local.get 1 - br_if 1 (;@1;) - call 11 return end local.get 0 @@ -2984,19 +2956,18 @@ i32.shl local.tee 0 i32.and - i32.eqz br_if 0 (;@3;) + i32.const 0 + local.get 2 + local.get 0 + i32.or + i32.store offset=1048984 local.get 3 - i32.load offset=8 local.set 0 br 1 (;@2;) end - i32.const 0 - local.get 2 - local.get 0 - i32.or - i32.store offset=1048984 local.get 3 + i32.load offset=8 local.set 0 end local.get 3 @@ -3011,7 +2982,28 @@ local.get 1 local.get 0 i32.store offset=8 + return end + i32.const 0 + local.get 1 + i32.store offset=1049000 + i32.const 0 + i32.const 0 + i32.load offset=1048992 + local.get 0 + i32.add + local.tee 0 + i32.store offset=1048992 + local.get 1 + local.get 0 + i32.const 1 + i32.or + i32.store offset=4 + local.get 1 + local.get 0 + i32.add + local.get 0 + i32.store ) (func (;11;) (type 0) (local i32 i32) @@ -3497,8 +3489,8 @@ block ;; label = @1 block ;; label = @2 local.get 2 - i32.const 15 - i32.gt_u + i32.const 16 + i32.ge_u br_if 0 (;@2;) local.get 0 local.set 3 diff --git a/homestar-wasm/fixtures/example_add_component.wasm b/homestar-wasm/fixtures/example_add_component.wasm index b6094828d732dc005cefb4f67c96e2e098a6c7d6..4d0f6c696a3b832f2b2c6727e2439bd468638b79 100644 GIT binary patch delta 2066 zcmb_dONbm*6n*d3=T&!gO*i9=^Qm#>RT;F6llTE^CPUCEowN=~3_@1wMi7-A19mcK zK*@AaQPhR`nE?u|~&wa0YK7u7o{8wo~BaD(0t+Dydg_(J*V=_>5F~QZIj4F z8Fh2Tt3Xrvsy)xClL(D{K@CkhC@5;kxD+}pPigs7ub?x};{8|)ZR6qRctJjIySdP! zO@1luiE74(ii3Cfg~N$?+`lzNHorXC)3lZav`oty2d!Knr84LJIUv34r$@sy3lLFh zER>cRf+r%5wQ9Z4@Am<(Y^^4Q=Tv3Ks+&ukyVO{98M@d^pNHn2pe>c@1rxa%a#fD2 zVyfH zdLH%?z|)Zqp_YB9DGQ;srZz=c7;iv+0r_>ve+}qiYl2E}O^^|YnYP%}K#9X0t0wpJ4? z@s5205@xSjIm8^DGuVo2d8FlP?x&f@6j$K^9BAw#TIP$bJb+_FImR~50*J3YtbNXz5y&)R44=+8s!0 zr8UyG8KIa#5ae(pb@NnXKs~~%B-X2M1 zIJhwKwTDhPJ$ii%T`K2=rdPjup;1TYwW(oyR35}1{Ir^Q(3ja((OFI$r&U-TnqO+i z5b@snF3bPbXcbM3AvknDweG{U9Fggk- zTmivwTLHzefac9uKV0MK1b{3tMDaD_^C8M`Tm-yEv#e!2GjQcYu1Y6?-8OrqllZjE zruQ*3yI(Ox`PbCV=&pOyqVpIAk(M#$FncFv76ko3R-gn>8oK@0hVO&fC^( z6VLhBTKaewTlmuO>6|>*Tk)-9;{~W#{c+XLl95!2CVs z*bVW4-TA3m_E^~`e1^=~bdPy`dcK92P@x+9NNnPCR};J{d)cNpx2F%VCgny!pE8ly z{Fv_EZ)L*3*tE2lSxbXVl;L9w1^G*0C2pX9JTAMmgQ{u1pI#W;HFL$I){#M5FOs*o H_m+PEU)hL) delta 2260 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 F{|D@VwJHDr diff --git a/homestar-wasm/fixtures/example_test_component.wasm b/homestar-wasm/fixtures/example_test_component.wasm index 41b38a4f694e0b6e383b389f9ee14a7b1e9cc10c..971680099477ca27557f4f8762d350dc7eb2da07 100644 GIT binary patch delta 29352 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 delta 27845 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