A device manager
Put a manager on top of the groups and route commands through the actor tree.
A building has more than one room. In this last step a device manager sits on top: it spawns a group per room on demand and routes every command to the right group. The groups, the devices and the query actor are those from the previous step, unchanged.
The manager
struct DeviceManager;
impl Actor for DeviceManager {
type Message = Command;
type State = HashMap<&'static str, ActorRef<GroupMessage>>;
type Error = Infallible;
fn init(&self, _: &ActorContext<Self::Message>) -> Result<Self::State, Self::Error> {
Ok(HashMap::new())
}
fn receive(
&self,
context: &ActorContext<Self::Message>,
incoming: Incoming<Self::Message>,
mut groups: Self::State,
) -> Result<Control<Self::State>, Self::Error> {
let Incoming::Message(command) = incoming else {
unreachable!("the manager watches no actor, hence never gets a terminated signal")
};
match command {
Command::Record {
group_name,
device_name,
degrees,
} => {
let group = groups
.entry(group_name)
.or_insert_with(|| context.spawn(Group));
group.tell(GroupMessage::Record {
device_name,
degrees,
});
Ok(Control::Continue(groups))
}
Command::Retire {
group_name,
device_name,
} => {
if let Some(group) = groups.get(group_name) {
group.tell(GroupMessage::Retire { device_name });
}
Ok(Control::Continue(groups))
}
Command::Average {
group_name,
reply_to,
} => {
match groups.get(group_name) {
Some(group) => group.tell(GroupMessage::Average(reply_to)),
None => reply_to.reply(None),
}
Ok(Control::Continue(groups))
}
}
}
}
enum Command {
Record {
group_name: &'static str,
device_name: &'static str,
degrees: f64,
},
Retire {
group_name: &'static str,
device_name: &'static str,
},
Average {
group_name: &'static str,
reply_to: ReplyTo<Option<f64>>,
},
}The manager follows the same pattern as the group one level below: a map from names to ActorRefs, filled on demand with context.spawn. Every command names its group, and the manager forwards the rest of it.
For Average, the request's ReplyTo now travels two levels down: from the manager to the group, and from the group into a new query actor. The query replies directly to whoever asked. For an unknown group the manager replies None itself.
Run it
#[tokio::main]
async fn main() -> anyhow::Result<()> {
init_tracing();
let system = ActorSystem::new(DeviceManager);
let manager = system.root();
let readings = [
("attic", "left", 21.5),
("attic", "right", 19.1),
("cellar", "main", 11.0),
("cellar", "main", f64::NAN),
];
for (group_name, device_name, degrees) in readings {
manager.tell(Command::Record {
group_name,
device_name,
degrees,
});
}
manager.tell(Command::Retire {
group_name: "attic",
device_name: "right",
});
for group_name in ["attic", "cellar", "garage"] {
let average = manager
.ask(Duration::from_secs(5), |reply_to| Command::Average {
group_name,
reply_to,
})
.await
.context("asking for the average temperature")?;
match average {
Some(degrees) => println!("Average temperature in {group_name}: {degrees:.2}"),
None => println!("No readings in {group_name}"),
}
}
system.stop();
system
.terminated()
.await
.context("awaiting actor system termination")
}RUST_LOG=tellus=debug cargo run --quiet -p tellus --example getting_started_07_device_managerAverage temperature in attic: 21.50
No readings in cellar
No readings in garageIn the attic, right retired, so only left counts. In the cellar, main failed on the invalid reading and restarted without its 11.0. The garage does not exist.
Stopping the system stops the whole tree bottom-up: devices first, then groups, then the manager.
What you learned
Actor trees compose. A manager, a group and a device each know only their direct children, and a ReplyTo lets the actor at the bottom answer the one at the top directly. This is tellus's take on the IoT device manager from the Apache Pekko documentation, if you have seen that before.
Where to go from here
You have used every core feature of tellus: actors with state, tell and ask, child actors, watch with its ordering guarantee, and supervision.
The next tutorials build on this one. Event-sourced actors makes the devices survive restarts, and Clustered actors spreads them across nodes.