Skip to content

Commit ff51076

Browse files
Merge pull request #25 from balena-io-experimental/improved-docs
Write documentation
2 parents 7694357 + 2958fa8 commit ff51076

39 files changed

Lines changed: 2558 additions & 666 deletions

Cargo.toml

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[package]
2-
name = "gustav"
2+
name = "mahler"
33
version = "0.14.0"
44
edition = "2021"
55
description = "An automated job orchestration library to build and execute dynamic workflows"
@@ -14,7 +14,7 @@ anyhow = "1.0.97"
1414
json-patch = "4"
1515
jsonptr = "0.7.1"
1616
matchit = "0.8.4"
17-
serde = { version = "1.0.197" }
17+
serde = "1.0.197"
1818
serde_json = "1.0.120"
1919
thiserror = "2"
2020
tokio = { version = "1.43.0", default-features = false, features = [
@@ -36,6 +36,8 @@ tracing-subscriber = { version = "0.3.19", default-features = false, optional =
3636
] }
3737

3838
[dev-dependencies]
39+
# configure mahler with the logging feature to ensure documentation tests pass
40+
mahler = { path = ".", features = ["logging"] }
3941
tracing-subscriber = { version = "0.3.19", default-features = false, features = [
4042
"ansi",
4143
"registry",
@@ -45,6 +47,13 @@ tracing-subscriber = { version = "0.3.19", default-features = false, features =
4547
dedent = "0.1.1"
4648
env_logger = "0.11.8"
4749
log = "0.4.25"
48-
tokio = { version = "1.36.0", features = ["rt", "time"] }
50+
tokio = { version = "1.36.0", features = ["rt-multi-thread", "time"] }
4951
pretty_assertions = "1.4.1"
5052
tokio-test = "0.4.4"
53+
54+
# docs.rs-specific configuration
55+
[package.metadata.docs.rs]
56+
# document all features
57+
all-features = true
58+
# defines the configuration attribute `docsrs`
59+
rustdoc-args = ["--cfg", "docsrs"]

README.md

Lines changed: 196 additions & 190 deletions
Large diffs are not rendered by default.

examples/composer/Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[package]
2-
name = "gustav-examples-composer"
2+
name = "mahler-examples-composer"
33
version = "0.0.0"
44
edition = "2021"
55
publish = false
@@ -8,7 +8,7 @@ publish = false
88
anyhow = "1.0.98"
99
bollard = { version = "0.18.1" }
1010
futures-util = { version = "0.3.31", default-features = false }
11-
gustav = { path = "../../", features = ["logging"] }
11+
mahler = { path = "../../", features = ["logging"] }
1212
log = "0.4.27"
1313
serde = { version = "1.0.219", features = ["derive"] }
1414
thiserror = "2.0.12"

examples/composer/README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Container composition example
2+
3+
Use mahler to manage container composition.
4+
5+
See the [library tests](https://github.com/balena-io-experimental/mahler-rs/blob/769435795c9bef779adab53efcb51812c34a0296/examples/composer/src/lib.rs#L770) for usage examples

examples/composer/src/lib.rs

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,16 @@ use anyhow::{anyhow, Context as AnyhowCtx};
22
use bollard::container::{CreateContainerOptions, StartContainerOptions};
33
use bollard::secret::{ContainerInspectResponse, ContainerState, ContainerStateStatusEnum};
44
use futures_util::stream::StreamExt;
5-
use gustav::worker::{Ready, Worker};
5+
use mahler::worker::{Ready, Worker};
66
use serde::{Deserialize, Serialize};
77
use std::collections::HashMap;
88
use thiserror::Error;
99

1010
use bollard::image::CreateImageOptions;
1111
use bollard::Docker;
1212

13-
use gustav::extract::{Args, Pointer, Res, System, Target, View};
14-
use gustav::task::prelude::*;
13+
use mahler::extract::{Args, Pointer, Res, System, Target, View};
14+
use mahler::task::prelude::*;
1515

1616
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Default)]
1717
pub enum ServiceStatus {
@@ -672,8 +672,8 @@ pub fn create_worker(project: Project) -> Worker<Project, Ready, TargetProject>
672672
#[cfg(test)]
673673
mod tests {
674674
use bollard::container::{ListContainersOptions, RemoveContainerOptions};
675-
use gustav::worker::SeekStatus;
676-
use gustav::{seq, Dag};
675+
use mahler::worker::SeekStatus;
676+
use mahler::{seq, Dag};
677677
use pretty_assertions::assert_eq;
678678
use serde_json::json;
679679
use tracing_subscriber::fmt::format::FmtSpan;
@@ -785,7 +785,7 @@ mod tests {
785785

786786
// Seeking the target must succeed
787787
let worker = worker.seek_target(target.clone()).await.unwrap();
788-
assert_eq!(worker.status(), &SeekStatus::TargetReached);
788+
assert_eq!(worker.status(), &SeekStatus::Success);
789789

790790
// The alpine image must exist now
791791
let docker = Docker::connect_with_defaults().unwrap();
@@ -943,7 +943,7 @@ mod tests {
943943

944944
// Seeking the target must succeed
945945
let worker = worker.seek_target(target).await.unwrap();
946-
assert_eq!(worker.status(), &SeekStatus::TargetReached);
946+
assert_eq!(worker.status(), &SeekStatus::Success);
947947

948948
// The alpine image must exist now
949949
let docker = Docker::connect_with_defaults().unwrap();
@@ -982,7 +982,7 @@ mod tests {
982982

983983
// Seeking the target must succeed
984984
let worker = worker.seek_target(target).await.unwrap();
985-
assert_eq!(worker.status(), &SeekStatus::TargetReached);
985+
assert_eq!(worker.status(), &SeekStatus::Success);
986986

987987
// The container ids should match
988988
let container = docker
@@ -1009,7 +1009,7 @@ mod tests {
10091009

10101010
// Seeking the target must succeed
10111011
let worker = worker.seek_target(target).await.unwrap();
1012-
assert_eq!(worker.status(), &SeekStatus::TargetReached);
1012+
assert_eq!(worker.status(), &SeekStatus::Success);
10131013

10141014
// The container ids should match
10151015
let container = docker
@@ -1030,7 +1030,7 @@ mod tests {
10301030

10311031
// Seeking the target must succeed
10321032
let worker = worker.seek_target(target).await.unwrap();
1033-
assert_eq!(worker.status(), &SeekStatus::TargetReached);
1033+
assert_eq!(worker.status(), &SeekStatus::Success);
10341034

10351035
// The container should no longer exist
10361036
let container = docker

examples/readme/Cargo.toml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
[package]
2+
name = "mahler-examples-readme"
3+
version = "0.0.0"
4+
edition = "2021"
5+
publish = false
6+
7+
[dependencies]
8+
anyhow = "1.0.98"
9+
env_logger = "0.11.8"
10+
log = "0.4.27"
11+
mahler = { path = "../../", features = ["logging"] }
12+
serde = "1.0.219"
13+
tokio = { version = "1.44.2", default-features = false, features = [
14+
"rt-multi-thread",
15+
"time",
16+
] }
17+
18+
[dev-dependencies]
19+
assert_cmd = "2.0.17"
20+
predicates = "3.1.3"

examples/readme/src/main.rs

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
use anyhow::{Context, Result};
2+
use serde::{Deserialize, Serialize};
3+
use std::collections::BTreeMap;
4+
use std::time::Duration;
5+
use tokio::time::sleep;
6+
7+
use mahler::extract::{Args, Target, View};
8+
use mahler::task::prelude::*;
9+
use mahler::worker::{init_logging, Worker};
10+
11+
// The state model needs to be Serializable and Deserializable
12+
// since the library uses JSON internally to access parts
13+
// of the state
14+
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
15+
struct Counters(BTreeMap<String, i32>);
16+
17+
// `plus_one` defines a job that updates a counter if it is below some target.
18+
// The job makes use of two extractors:
19+
// - `View`, that provides a mutable view into the system state. By modifying the view,
20+
// the job task can affect the global state
21+
// - `Target`, providing a read only view to the target being seeked by the planner
22+
fn plus_one(mut counter: View<i32>, Target(tgt): Target<i32>) -> Update<i32> {
23+
if *counter < tgt {
24+
// Modify the counter value if we are below the target
25+
*counter += 1;
26+
}
27+
28+
// The task is called at planning and at runtime, the `with_io` function
29+
// allows us to define what is returned by the function at each context.
30+
// The first argument of the function is what the planner receives,
31+
// the right side of the call is what will be executed at runtime if the
32+
// task is selected.
33+
with_io(counter, |counter| async {
34+
// The async call can be used to actually make changes to the underlying system.
35+
// It could be writing the counter to a database or a file. In this
36+
// case we just add some timer
37+
sleep(Duration::from_millis(10)).await;
38+
Ok(counter)
39+
})
40+
}
41+
42+
// `plus_two` is a compound job. Compound job do not modify the state directly
43+
// but return combination of sub-tasks that are applicable to a certain target
44+
fn plus_two(counter: View<i32>, Target(tgt): Target<i32>) -> Vec<Task> {
45+
// If the difference between the current state and target is >1
46+
if tgt - *counter > 1 {
47+
// Then return a sequence of two tasks with the same target
48+
return vec![plus_one.with_target(tgt), plus_one.with_target(tgt)];
49+
}
50+
51+
// Otherwise do nothing
52+
vec![]
53+
}
54+
55+
#[tokio::main]
56+
async fn main() -> Result<()> {
57+
// Convert tracing logs into `log` crate logs
58+
init_logging();
59+
60+
// Show logs on stdout
61+
env_logger::builder().format_target(false).init();
62+
63+
let worker = Worker::new()
64+
// The jobs are applicable to `UPDATE` operations
65+
// on individual counters
66+
.job(
67+
"/{counter}",
68+
update(plus_one)
69+
// we can add a description to the job for the logs
70+
.with_description(|Args(counter): Args<String>| format!("{counter}++")),
71+
)
72+
.job("/{counter}", update(plus_two))
73+
// We initialize the worker with two counters
74+
// `a` and `b` with value 0
75+
.initial_state(Counters(BTreeMap::from([
76+
("a".to_string(), 0),
77+
("b".to_string(), 0),
78+
])))
79+
.with_context(|| "failed to serialize initial state")?;
80+
81+
// Tell the worker to find a plan from the initial state (a:0, b:0)
82+
// to the target state (a:1, b:2) and execute it
83+
let worker = worker
84+
.seek_target(Counters(BTreeMap::from([
85+
("a".to_string(), 1),
86+
("b".to_string(), 2),
87+
])))
88+
.await
89+
.with_context(|| "failed to reach target state")?;
90+
91+
// Get the internal state from the Worker. The worker
92+
// is idle but the state may not be static so we need
93+
// to use an await to get the current state.
94+
let state = worker
95+
.state()
96+
.await
97+
.with_context(|| "failed to deserialize state")?;
98+
99+
assert_eq!(
100+
state,
101+
Counters(BTreeMap::from(
102+
[("a".to_string(), 1), ("b".to_string(), 2),]
103+
))
104+
);
105+
106+
println!("The system state is now {:?}", state);
107+
Ok(())
108+
}

examples/readme/tests/cli.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
use anyhow::Result;
2+
use assert_cmd::prelude::*; // Add methods on commands
3+
use predicates::prelude::*;
4+
use std::process::{Command, Output}; // Run programs
5+
6+
#[test]
7+
fn run_example() -> Result<()> {
8+
let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME"))?;
9+
let assert = cmd.env("RUST_LOG", "debug").assert();
10+
11+
let Output { stdout, stderr, .. } = cmd.unwrap();
12+
println!("{}", String::from_utf8_lossy(&stderr));
13+
println!("{}", String::from_utf8_lossy(&stdout));
14+
15+
assert.success().stdout(predicate::str::diff(
16+
"The system state is now Counters({\"a\": 1, \"b\": 2})\n",
17+
));
18+
19+
Ok(())
20+
}

examples/run-tests.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export RUST_LOG="${RUST_LOG},bollard=off,hyper=off"
99
find ./examples -name 'Cargo.toml' | while read -r file; do
1010
dir=$(dirname "$file")
1111
echo "Running tests in $dir"
12-
(cd "$dir" && cargo test -- --test-threads 1) || {
12+
(cd "$dir" && cargo test -- --nocapture --test-threads 1) || {
1313
echo "Tests failed in $dir"
1414
exit 1
1515
}

src/errors.rs

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1+
//! Global error type definitions
2+
13
use std::ops::Deref;
24

35
use thiserror::Error;
46

57
#[derive(Debug, Error)]
68
#[error("argument extraction failed: {0:?}")]
7-
/// Arguments to a task could not be extracted.
9+
/// Job argument extraction failed
10+
///
811
/// This is likely to be an error with the task definition.
912
pub struct ExtractionError(#[from] anyhow::Error);
1013

@@ -15,16 +18,17 @@ pub struct SerializationError(#[from] serde_json::Error);
1518

1619
#[derive(Debug, Error)]
1720
#[error("internal error, this may be a bug: {0:?}")]
18-
/// Some unexpected error happened during the worker operation, either
19-
/// planning or runtime. These errors should not happen, unless
20-
/// there is a bug in the implementation.
21+
/// Some unexpected error happened during the worker operation
22+
///
23+
/// These errors should not happen, unless there is a bug in the implementation.
2124
pub struct InternalError(#[from] anyhow::Error);
2225

2326
#[derive(Debug, Error)]
2427
#[error("method expansion failed: {0:?}")]
25-
/// An error happened while trying to expand the method
26-
/// task into its sub-tasks. This is likely an issue with the
27-
/// method definition
28+
/// An error happened while trying to expand the method into sub-tasks
29+
///
30+
/// This is likely an issue with the method definition or indicates a that a job on which the
31+
/// method depends has not been registered with the Worker.
2832
pub struct MethodError(Box<dyn std::error::Error + Send + Sync>);
2933

3034
impl MethodError {
@@ -36,6 +40,7 @@ impl MethodError {
3640
#[derive(Debug, Error)]
3741
#[error(transparent)]
3842
/// An error happened while executing the task within the workflow.
43+
///
3944
/// These errors only happen at runtime, never at the planning stage
4045
/// of the worker.
4146
pub struct IOError(Box<dyn std::error::Error + Send + Sync>);

0 commit comments

Comments
 (0)