A group of devices
Spawn child actors on demand and route messages to them.
A building has many devices, grouped by room. In this step a group actor spawns a device actor for every device it hears about and routes readings and requests to it.
The group
struct Group;
impl Actor for Group {
type Message = GroupMessage;
type State = HashMap<&'static str, ActorRef<DeviceMessage>>;
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 devices: Self::State,
) -> Result<Control<Self::State>, Self::Error> {
let Incoming::Message(message) = incoming else {
unreachable!("the group watches no actor, hence never gets a terminated signal")
};
match message {
GroupMessage::Record {
device_name,
degrees,
} => {
let device = devices
.entry(device_name)
.or_insert_with(|| context.spawn(Device));
device.tell(DeviceMessage::Record(degrees));
Ok(Control::Continue(devices))
}
GroupMessage::Read {
device_name,
reply_to,
} => {
match devices.get(device_name) {
Some(device) => device.tell(DeviceMessage::Read(reply_to)),
None => reply_to.reply(None),
}
Ok(Control::Continue(devices))
}
}
}
}
enum GroupMessage {
Record {
device_name: &'static str,
degrees: f64,
},
Read {
device_name: &'static str,
reply_to: ReplyTo<Option<f64>>,
},
}The group's state maps device names to ActorRef<DeviceMessage>, the handles for sending messages to the devices. The device actor is the one from the previous step.
When a reading arrives for an unknown device, context.spawn(Device) creates a new device as a child of the group and returns its ActorRef. The group stores it and forwards the reading.
Read shows that a ReplyTo can travel. The group does not answer itself: it hands the request's ReplyTo on to the device, and the device replies directly to whoever asked. For an unknown device the group replies None itself.
Talking to the group
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let system = ActorSystem::new(Group);
let group = system.root();
group.tell(GroupMessage::Record {
device_name: "left",
degrees: 21.5,
});
group.tell(GroupMessage::Record {
device_name: "right",
degrees: 19.1,
});
group.tell(GroupMessage::Record {
device_name: "left",
degrees: 22.0,
});
for device_name in ["left", "right", "middle"] {
let last_reading = group
.ask(Duration::from_secs(1), |reply_to| GroupMessage::Read {
device_name,
reply_to,
})
.await
.context("asking for the last reading")?;
match last_reading {
Some(degrees) => println!("Last reading of {device_name}: {degrees:.1}"),
None => println!("No reading of {device_name}"),
}
}
system.stop();
system
.terminated()
.await
.context("awaiting actor system termination")
}Run it:
cargo run --quiet -p tellus --example getting_started_03_groupLast reading of left: 22.0
Last reading of right: 19.1
No reading of middleThe actor tree
Every actor except the root has a parent: the actor that spawned it. Together they form a tree. When an actor stops, tellus first stops its children, then the actor itself. So system.stop() at the end stops the devices first and the group last, and terminated resolves only once the whole tree is gone. You never have to stop children yourself.
What you learned
context.spawn creates child actors, ActorRef is how you send them messages, and a ReplyTo can be passed on so the right actor replies. Stopping an actor stops its children first. Next, a device retires, and the group needs to know when it is gone.