Skip to content

2. Keeping state: Container and ContainerProcess

The Process in chapter 1 was an idempotent function that always returns the same result for the same arguments. But when you build parts for robots and IoT devices, you always end up with things you want to remember: a counter value, the current position of a motor, a connection to a camera. In this chapter you learn how juiz handles state, using a counter that remembers one integer as the example.

Keep using the example repository you cloned and built in chapter 1. The Container examples were built along with everything else in chapter 1. If you have not done that yet, follow Prepare the samples in chapter 1.

This chapter uses these three building blocks.

Role What it does
Container Holds one counter value
ContainerProcess increment Adds arg1 to the Container’s value and returns the new value
ContainerProcess get Returns the Container’s value as is

A Container is the state itself. You write only the box that holds the value and the function that creates it.

container/example_container/src/lib.rs

use juiz_sdk::prelude::*;
#[repr(Rust)]
pub struct ExampleContainer {
pub value: i64,
}
#[juiz_container]
fn example_container(initial_value: i64) -> JuizResult<Box<ExampleContainer>> {
Ok(Box::new(ExampleContainer {
value: initial_value,
}))
}

You write the struct ExampleContainer that holds the state, and a function that creates it. The function marked with #[juiz_container] describes how to create the Container, and the function name example_container becomes the Container’s type name.

The ContainerProcesses also use the ExampleContainer definition, so this crate’s Cargo.toml sets crate-type = ["cdylib", "rlib"] to make it usable both as a shared library and as a Rust library.

The Container has no code that changes or reads the value. A Container only describes “what to remember”.

Code that changes or reads the state goes in ContainerProcesses. A ContainerProcess is a Process that receives the Container as its first argument. You specify which Container it pairs with by the Container’s type name.

container/example_container_increment/src/lib.rs

use example_container::ExampleContainer;
use juiz_sdk::prelude::*;
#[juiz_container_process(
container_type = "example_container"
description = "Container Process for example_container. This process will add given value to container."
arguments = {
default = {
arg1 = 1
}
}
)]
fn example_container_increment(
container: &mut ContainerImpl<ExampleContainer>,
arg1: i64,
) -> JuizResult<Capsule> {
container.value = container.value + arg1;
return Ok(jvalue!(container.value).into());
}

Write the type name of the Container to bind, example_container, in container_type of #[juiz_container_process]. The first parameter container receives the Container, and you can read and write its contents like container.value. The remaining parameters (arg1) are passed when calling it, just like the Process in chapter 1.

container/example_container_get/src/lib.rs

use example_container::ExampleContainer;
use juiz_sdk::prelude::*;
#[juiz_container_process(container_type = "example_container")]
fn example_container_get(container: &mut ContainerImpl<ExampleContainer>) -> JuizResult<Capsule> {
println!("example_container_get() called");
return Ok(jvalue!(container.value).into());
}

get has no arguments other than the Container and only returns the value.

This is how you write state (the Container) and behavior (the ContainerProcesses) separately. Only the ContainerProcesses bound to a Container can change or read its state. When you want to know “where does this value change?”, you only need to look at the ContainerProcesses.

First, start juiz with the files, just like in chapter 1. Load a Container with --container and a ContainerProcess with --container_process.

Terminal window
juiz --container ./target/debug/libexample_container.so \
--container_process ./target/debug/libexample_container_increment.so -1 -d

On macOS the extension is .dylib.

With -1, juiz creates one instance from the loaded Container type and binds one ContainerProcess instance to it. As in chapter 1, instance names are the type name followed by 0: the Container is example_container0 and the ContainerProcess is example_container_increment0 (in Python and C++ the type names have a _python / _cpp suffix). The Container starts at 0, the argument’s default value.

Leave juiz running and open another terminal. First, list the running ContainerProcesses.

Terminal window
juiz container-process list
# => ["core://core/container_process/example_container_increment0::example_container_increment:example_container0"]

A ContainerProcess ID has the form “instance name::type name:Container instance name”, so the ID tells you which Container it is bound to. The list shows IDs in the core://core/... form, which points inside the broker itself. To call from outside, replace the beginning with http://127.0.0.1:8000, as in chapter 1.

Call increment twice with the same argument.

Terminal window
juiz container-process call "http://127.0.0.1:8000/container_process/example_container_increment0::example_container_increment:example_container0" '{"arg1": 5}' -p
# => CapsulePtr((value:Value(5), option: []))
juiz container-process call "http://127.0.0.1:8000/container_process/example_container_increment0::example_container_increment:example_container0" '{"arg1": 5}' -p
# => CapsulePtr((value:Value(10), option: []))

You passed the same {"arg1": 5}, but the first and second results differ. Unlike the Process in chapter 1, a ContainerProcess’s result depends not only on its arguments but also on the Container’s state.

Share a Container between two ContainerProcesses

Section titled “Share a Container between two ContainerProcesses”

Next, read the value changed by increment with get. The command line can load only one ContainerProcess, so use the configuration file example_container.conf, which says “which ContainerProcesses to bind to which Container”. Stop the running juiz with Ctrl+C, then restart it with the configuration file.

Terminal window
juiz -f example_container.conf -d

The configuration file says the following.

  • Which files to load the Container type and the ContainerProcess types from
  • Create one Container instance, container0
  • Bind to container0 an increment instance increment0 and a get instance get0

Chapter 3 covers how to write configuration files in detail. For now, just note the shape: “two ContainerProcesses hang off one Container”.

The list now shows two ContainerProcesses.

Terminal window
juiz container-process list
# => ["core://core/container_process/increment0::example_container_increment:container0", "core://core/container_process/get0::example_container_get:container0"]

The order may change from run to run.

Check the current value with get, call increment twice, then check the value with get again.

Terminal window
juiz container-process call "http://127.0.0.1:8000/container_process/get0::example_container_get:container0" '{}' -p
# => CapsulePtr((value:Value(0), option: []))
juiz container-process call "http://127.0.0.1:8000/container_process/increment0::example_container_increment:container0" '{"arg1": 5}' -p
# => CapsulePtr((value:Value(5), option: []))
juiz container-process call "http://127.0.0.1:8000/container_process/increment0::example_container_increment:container0" '{"arg1": 5}' -p
# => CapsulePtr((value:Value(10), option: []))
juiz container-process call "http://127.0.0.1:8000/container_process/get0::example_container_get:container0" '{}' -p
# => CapsulePtr((value:Value(10), option: []))

get, a different ContainerProcess, can read the value that increment changed. The two ContainerProcesses share the same Container.

As in chapter 1, you can also call them from Swagger UI (http://127.0.0.1:8000/docs/). To call a ContainerProcess, open PATCH /api/container_process/call and enter the core://core/... ID shown by juiz container-process list as the identifier, unchanged.

  • identifier: core://core/container_process/get0::example_container_get:container0
  • Request body: {}
  • Pass a negative number to increment (such as {"arg1": -3}) and confirm that the value goes down.
  • Stop juiz with Ctrl+C, start it again and call get. The value is back to the start. The state exists only inside the running Container.
  • Think about what a ContainerProcess that resets the value to 0 (reset) would look like. It only needs the Container as its argument, setting value to 0 and returning it. Running it requires adding it to the configuration file, so try it after reading chapter 3.
  • State goes in a Container. A Container only describes “what to remember” and contains no processing.
  • Only the ContainerProcesses bound to a Container read and write its state. A ContainerProcess receives the Container as its first argument.
  • One Container can have several ContainerProcesses with different roles. They share the same state.
  • A ContainerProcess’s result depends on the Container’s state as well as its arguments. Keeping idempotent Processes separate from state-handling ContainerProcesses makes it clear where state changes.

In the next chapter you learn how to write the configuration file used in this chapter, and start the Process from chapter 1 and the Container from this chapter together from one configuration file. Move on to Starting everything from a configuration file.