Iwould like to implement a rest service using Akka and Asp.net. Following the example here
I create my AkkaService containing the FooActor ref and a controller who transform the http request to a RunProcess message which is sent to the FooActor.
[Route("api/[controller]")]
[ApiController]
public class MyController : Controller
{
private readonly ILogger<MyController> _logger;
private readonly IAkkaService Service;
public RebalancingController(ILogger<MyController> logger, IAkkaService bridge)
{
_logger = logger;
Service = bridge;
}
[HttpGet]
public async Task<ProcessTerminated> Get()
{
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(60));
return await Service.RunProcess(cts.Token);
}
}
public class AkkaService : IAkkaService, IHostedService
{
private ActorSystem ActorSystem { get; set; }
public IActorRef FooActor { get; private set; }
private readonly IServiceProvider ServiceProvider;
public AkkaService(IServiceProvider sp)
{
ServiceProvider = sp;
}
public async Task StartAsync(CancellationToken cancellationToken)
{
var hocon = ConfigurationFactory.ParseString(await File.ReadAllTextAsync("app.conf", cancellationToken));
var bootstrap = BootstrapSetup.Create().WithConfig(hocon);
var di = DependencyResolverSetup.Create(ServiceProvider);
var actorSystemSetup = bootstrap.And(di);
ActorSystem = ActorSystem.Create("AkkaSandbox", actorSystemSetup);
// </AkkaServiceSetup>
// <ServiceProviderFor>
// props created via IServiceProvider dependency injection
var fooProps = DependencyResolver.For(ActorSystem).Props<FooActor>();
FooActor = ActorSystem.ActorOf(rebalProps.WithRouter(FromConfig.Instance), "foo");
// </ServiceProviderFor>
await Task.CompletedTask;
}
public async Task<ProcessTerminated> RunProcess(CancellationToken token)
{
return await FooActor.Ask<ProcessTerminated>(new RunProcess(), token);
}
public FooActor(IServiceProvider sp)
{
_scope = sp.CreateScope();
Receive<RunProcess>(x =>
{
var basketActor = Context.ActorOf(Props.Create<BarActor>(sp), "BarActor");
basketActor.Tell(new BarRequest());
_log.Info($"Sending a request to Bar Actor ");
});
Receive<BarResponse>(x =>
{
...... Here I need to send back a ProcessTerminated message to the controller
});
}
Now, let's imagine the FooActor send a message to the BarActor telling him to perform a given task and wait the BarResponse. How could I send back the ProcessTerminated message to the controller?
Few points to take into considerations:
- I want to ensure no coupling between BarActor and FooActor. By example, I could add the original sender ActorRef to the BarRequest and BarResponse. But the BarActor musn't know about the fooActor and MyController. The structure of the messages an how the barActor respond should not be dependent of what the FooActor do with the BarResponse.
- In the example I only use BarActor, but we can imagine to have many different actors exchanging messages before returning the final result to the controller.
CodePudding user response:
Nitpick: you should use Akka.Hosting and avoid creating this mock wrapper service around the ActorSystem
. That will allow you to pass in the ActorRegistry
directly into your controller, which you can use to then access FooActor
without the need for additional boilerplate. See "Introduction to Akka.Hosting - HOCONless, "Pit of Success" Akka.NET Runtime and Configuration" video for a fuller explanation.
Next: to send the ProcessTerminated
message back to your controller you need to save the Sender
(the IActorRef
that points to the temporary actor created by Ask<T>
, in this instance) during your Receive<RunProcess>
and make sure that this value is available inside your Receive<BarResponse>
.
The simple ways to accomplish that:
- Store the
Sender
in a field on theFooActor
, use behavior-switching while you wait for theBarActor
to respond, and then revert back to your original behavior. - Build a
Dictionary<RunProcess, IActorRef>
(the key should probably actually be some unique ID shared byRunProcess
andBarResponse
- a "correlation id") and reply to the correspondingIActorRef
stored in the dictionary whenBarResponse
is received. Remove the entry after processing. - Propagate the
Sender
in theBarRequest
andBarResponse
message payloads themselves.
All three of those would work. If I thought there were going to be a large number of RunProcess
requests running in parallel I'd opt for option 2.
CodePudding user response:
Another way of doing it is by simply forwarding the next message to the next actor. The Tell
operation have a second parameter that can be used to override the message sender. If you're sure that all path has to respond back to the original Ask
inside the Http controller, you can do this inside the FooActor
:
Receive<RunProcess>(x =>
{
var basketActor = Context.ActorOf(Props.Create<BarActor>(sp), "BarActor");
basketActor.Tell(new BarRequest(), Sender);
_log.Info($"Sending a request to Bar Actor ");
});
This way, the original Ask
actor is considered as the sender of the new BarRequest
message instead of the FooActor
, and if BarActor
decide to reply by doing a Sender.Tell(new ProcessTerminated())
. the ProcessTerminated
message will be sent to the Http controller.