elfo icon indicating copy to clipboard operation
elfo copied to clipboard

Is it available some kind of init actor

Open lucky-pucka opened this issue 10 months ago • 5 comments

Hello!

Thanks a lot for such great actor framework and your work guys!

But I have an issue how to make some kind of init stage. For now I have some actors and one of them read config during routing and spawn all necessary actors. It becomes not convenient when I have more then 3 logical actors. Is there some way to make something like in Docker with init container but with actor in Elfo?

Thanks for your reply in advance!

lucky-pucka avatar Feb 12 '25 23:02 lucky-pucka

Hello,

one of them read config during routing

Can you provide more details here? Do you mean MapRouter::with_state()?


In general, there are several options for how to implement the "init" actor.

Firstly, if a group routes UpdateConfig to some key with Unicast or Multicast, that actor is started on startup (actoromicon). If a group router isn't installed, the default one routes everything (including UpdateConfig) to a single actor, so it's always started. Thus, you can mount a group with ActorGroup::new().exec(|mut ctx| ..) and use it as some sort of "init" actor, which can then send messages (e.g. a custom Start) to start actors in other groups. In this case, you can provide configuration to this actor only.

Secondly, you can provide configuration to the destination group and use MapRouter::with_state() (actoromicon), which has access to the configuration and can route UpdateConfig according to that configuration. If multiple groups share the same configuration, you can use [common] section in the toml config.

Thirdly, you can mark your group as .entrypoint() in the topology (system.configurers is usually the only group that is marked). In this case, that group will receive elfo::messages::StartEntrypoint. Note that multiple entry points should work but haven't been tested, so I cannot recommend this way for now. Also, writing entry points is not easy because the actor is started before other groups are configured. Writing custom entrypoints (i.e. if you want to replace elfo-confiturer totally) isn't documented yet and is considered unstable.


Anyway, if you would like to provide more information, I can improve the acromion (probably start filling the "Patterns" section to show how to implement some common use cases).

loyd avatar Feb 17 '25 07:02 loyd

@loyd thanks a lot for your response! There are a lot of interesting things that I even didn't know about common section and entrypoint group.

Before you replied I had a working version that was doing that I wanted but I'm not sure about usage of elfo in right way.

I have some kind of next code to make implementation:

#[derive(Debug, serde::Deserialize)]
struct SpawnerConfig {
    names: Vec<String>,
    value: u8,
}

pub fn spawn_spawner() -> Blueprint {
    ActorGroup::new()
        .config::<SpawnerConfig>()
        .router(MapRouter::with_state(
            |_, _| { "spawner".to_string() },
            |envelope, actor_name| {
                msg!(match envelope {
                    UpdateConfig => Outcome::Unicast(actor_name.clone()),
                    _ => Outcome::Default,
                })
            }
        ))
        .exec(move |ctx| {
            async move {
                Spawner::new(ctx).run().await
            }
        })
}

pub struct Spawner {
    ctx: Context<SpawnerConfig, String>,
}

impl Spawner {
    pub fn new(ctx: Context<SpawnerConfig, String>) -> Self {
        Self {
            ctx,
        }
    }

    async fn spawn_actors(&self) -> Result<()> {
        let config = self.ctx.config();
        for name in config.names.iter() {
            let spawn_actor = SpawnActor {
                name: name.clone(),
                value: config.value,
            };
            
             let spawned_response = self.ctx
                 .request(spawn_actor)
                 .resolve()
                 .await()?;

            tracing::info!("Spawned actor: {:?}", spawned_response);
     }

     async fn run(self) -> Result<()> {
          self.spawn_actors().await
     }

This actor runs that sends all necessary spawn messages, collects responses from actor groups and then terminates. For now it's something that I need but I'm not sure that I've made it right way

lucky-pucka avatar Feb 17 '25 15:02 lucky-pucka

@lucky-pucka This is exactly the first pattern described above. with_state looks useless here (but you probably use it in the full code via ctx.key() somewhere).

Note: your spawner actor will be started on every reconfiguration (e.g. try to send SIGHUP to the process) and send requests repeatedly, so destination actors should be ready to receive SpawnActor during their main cycle (while let Some(envelope) = ctx.recv().await { .. }).

loyd avatar Feb 18 '25 06:02 loyd

I even didn't know about common section

This is described in the usage example https://github.com/elfo-rs/elfo/blob/8b7ae1a904afad9d5f6d39d0b54edd33cb6d31f2/examples/usage/config.toml#L1-L21 But obviously should be documented in the actoromicon.


Let this issue be open until the first two patterns are documented in the actoromicon.

loyd avatar Feb 18 '25 06:02 loyd

@loyd do I understand correctly that .with_state I can have some custom logic and load some data from JSON file for example and have access to it via self.ctx somehow?

lucky-pucka avatar Feb 18 '25 15:02 lucky-pucka