World folder is the source of truth
- actor/<id>/ holds actor.wasm, actor.json, source/, public/, data/, versions/ - the broker boots by scanning actor/; a changed actor.wasm is an upgrade, a rejected one stays in versions/ with a reason and the files come back - /actor hides every data/ folder - subscriptions live in the subscriber's actor.json - cargo installs the SDK and points sausage-actor at it; no build limits - PROTOCOL.md
This commit is contained in:
+60
-16
@@ -6,22 +6,30 @@
|
||||
//!
|
||||
//! The broker knows nothing about Rust. This actor is one way to get a
|
||||
//! `.wasm` file. Other actors can add other languages.
|
||||
//!
|
||||
//! Actors depend on the SDK with `sausage-actor = "0.1"`. The SDK is not
|
||||
//! published: this actor copies the SDK of this host to `data/sdk/`, and
|
||||
//! points the dependency there in the build copy of `Cargo.toml`.
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
use std::time::Instant;
|
||||
|
||||
use anyhow::{Context, Result, anyhow, bail};
|
||||
use serde_json::{Value, json};
|
||||
use tokio::process::Command;
|
||||
use tokio::sync::{Mutex, mpsc};
|
||||
|
||||
use crate::broker::{Broker, Envelope, truncate};
|
||||
use include_dir::{Dir, include_dir};
|
||||
|
||||
use crate::broker::{Broker, Envelope};
|
||||
use crate::world::{self, World};
|
||||
|
||||
pub const ID: &str = "cargo";
|
||||
pub const TARGET: &str = "wasm32-wasip2";
|
||||
const BUILD_TIMEOUT: Duration = Duration::from_secs(900);
|
||||
|
||||
/// The source of the `sausage-actor` SDK.
|
||||
static SDK: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/../sdk");
|
||||
|
||||
pub struct BuildOutput {
|
||||
/// Host path of the published `.wasm` file.
|
||||
@@ -37,8 +45,10 @@ pub struct Cargo {
|
||||
}
|
||||
|
||||
impl Cargo {
|
||||
pub fn new(world: World) -> Self {
|
||||
Self { world, lock: Mutex::new(()) }
|
||||
/// Create the actor and install the SDK of this host into `data/sdk/`.
|
||||
pub fn new(world: World) -> Result<Self> {
|
||||
write_dir(&SDK, &world.data_dir(ID).join("sdk"))?;
|
||||
Ok(Self { world, lock: Mutex::new(()) })
|
||||
}
|
||||
|
||||
/// Build the crate in `source` (a host path) and publish the `.wasm` file
|
||||
@@ -53,12 +63,8 @@ impl Cargo {
|
||||
let key = format!("{:016x}", fxhash(source.to_string_lossy().as_bytes()));
|
||||
let build_dir = data.join("work").join(&key);
|
||||
world::replace_tree(source, &build_dir)?;
|
||||
// The crate must not join a parent workspace by accident.
|
||||
let manifest = build_dir.join("Cargo.toml");
|
||||
let text = std::fs::read_to_string(&manifest)?;
|
||||
if !text.contains("[workspace]") {
|
||||
std::fs::write(&manifest, format!("{text}\n[workspace]\n"))?;
|
||||
}
|
||||
std::fs::write(&manifest, prepare_manifest(&std::fs::read_to_string(&manifest)?, Some(&data.join("sdk")))?)?;
|
||||
|
||||
let mut cmd = Command::new("cargo");
|
||||
cmd.arg("build")
|
||||
@@ -70,13 +76,10 @@ impl Cargo {
|
||||
.arg(data.join("target"))
|
||||
.current_dir(&build_dir)
|
||||
.kill_on_drop(true);
|
||||
let out = tokio::time::timeout(BUILD_TIMEOUT, cmd.output())
|
||||
.await
|
||||
.map_err(|_| anyhow!("cargo build took longer than {BUILD_TIMEOUT:?}"))?
|
||||
.context("cannot run cargo (is it on PATH, with the wasm32-wasip2 target?)")?;
|
||||
let out = cmd.output().await.context("cannot run cargo (is it on PATH, with the wasm32-wasip2 target?)")?;
|
||||
let stderr = String::from_utf8_lossy(&out.stderr).to_string();
|
||||
if !out.status.success() {
|
||||
bail!("{}", truncate(&strip_progress(&stderr), 20_000));
|
||||
bail!("{}", strip_progress(&stderr));
|
||||
}
|
||||
let mut artifact = None;
|
||||
for line in String::from_utf8_lossy(&out.stdout).lines() {
|
||||
@@ -123,7 +126,7 @@ impl Cargo {
|
||||
"source": source_s,
|
||||
"built_by": ID,
|
||||
"seconds": (out.seconds * 10.0).round() / 10.0,
|
||||
"warnings": truncate(&out.warnings, 4000),
|
||||
"warnings": out.warnings,
|
||||
}))
|
||||
}
|
||||
Some("shutdown") => Ok(json!({"type": "ignored"})),
|
||||
@@ -132,6 +135,47 @@ impl Cargo {
|
||||
}
|
||||
}
|
||||
|
||||
/// Point a `sausage-actor` dependency at `sdk` (keeping its features), and
|
||||
/// add an empty `[workspace]`, so the crate never joins a parent workspace.
|
||||
fn prepare_manifest(text: &str, sdk: Option<&Path>) -> Result<String> {
|
||||
let mut doc: toml_edit::DocumentMut = text.parse()?;
|
||||
if let Some(sdk) = sdk {
|
||||
for section in ["dependencies", "dev-dependencies", "build-dependencies"] {
|
||||
let Some(dep) = doc.get_mut(section).and_then(|t| t.get_mut("sausage-actor")) else { continue };
|
||||
let mut table = dep.as_inline_table().cloned().unwrap_or_default();
|
||||
for key in ["version", "path", "git", "branch", "tag", "rev", "registry"] {
|
||||
table.remove(key);
|
||||
}
|
||||
table.insert("path", sdk.display().to_string().into());
|
||||
*dep = toml_edit::value(table);
|
||||
}
|
||||
}
|
||||
if doc.get("workspace").is_none() {
|
||||
doc["workspace"] = toml_edit::table();
|
||||
}
|
||||
Ok(doc.to_string())
|
||||
}
|
||||
|
||||
/// Write the files of `dir` below `to`. Skip files that did not change, so
|
||||
/// that cargo does not rebuild the SDK at every start.
|
||||
fn write_dir(dir: &Dir<'_>, to: &Path) -> Result<()> {
|
||||
for file in dir.files() {
|
||||
let dest = to.join(file.path());
|
||||
let contents = match file.path().file_name() {
|
||||
Some(n) if n == "Cargo.toml" => prepare_manifest(file.contents_utf8().context("Cargo.toml")?, None)?.into_bytes(),
|
||||
_ => file.contents().to_vec(),
|
||||
};
|
||||
if std::fs::read(&dest).ok() != Some(contents.clone()) {
|
||||
std::fs::create_dir_all(dest.parent().context("no parent")?)?;
|
||||
std::fs::write(&dest, contents)?;
|
||||
}
|
||||
}
|
||||
for sub in dir.dirs() {
|
||||
write_dir(sub, to)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn next_build_number(dir: &Path) -> Result<u64> {
|
||||
std::fs::create_dir_all(dir)?;
|
||||
let max = std::fs::read_dir(dir)?
|
||||
|
||||
Reference in New Issue
Block a user