dapr / dotnet-sdk

Dapr SDK for .NET
Apache License 2.0
1.11k stars 335 forks source link

Lifetime of constructor injected services for an Actor #988

Closed hittaKAFFE closed 1 week ago

hittaKAFFE commented 1 year ago

I have a few question about the lifetime of contructor injected services for IRemindable Actors.

Background

We are going to be using an Actor for our .NET application with a long lifetime (several days), which is going to be reminded to activate once every day for a few days.

The Actor will be dependent on a service that in turn is dependent on an EF Core DbContext. We want the context to be as short lived as possible, and ensure that it doesn't share the same lifetime as the Actor (which will be several days).

It will look something like this:

public class MyActor : Actor, IMyActor, IRemindable
{
    public MyActor(ActorHost host, IMyRepository) // IMyRepository contains an EF Core DbContext
        : base(host)
    {
        ...
    }

    ...

    public async Task ReceiveReminderAsync(string reminderName, byte[] state, TimeSpan dueTime, TimeSpan period)
    {
        ...
    }
}

Questions

From the documentation:

Each actor instance has its own dependency injection scope and remains in memory for some time after performing an operation. During that time, the dependency injection scope associated with the actor is also considered live. The scope will be released when the actor is deactivated.

When is the Actor deactivated? And is the Actor only reactivated when it receives a reminder? Is the Actor, and/or its services, disposed on deactivation?

Do you see any issues with using constructor injected services for long-running IRemindable Actors (several days)?

Should we perhaps use an IServiceProvider.CreateScope() instead and create a new scope everytime the Actor is reminded, to ensure that services are disposed of properly? Like so:

public async Task ReceiveReminderAsync(string reminderName, byte[] state, TimeSpan dueTime, TimeSpan period)
    {
        using (var scope = _serviceScopeFactory.CreateScope()) // Constructor injected IServiceScopeFactory
        var myRepository = scope.ServiceProvider.GetRequiredService<IMyRepository>();
        ...
    }
WhitWaldo commented 1 week ago

@hittaKAFFE I'm sorry this was missed and no one wrote back to you for so long.

Actors are virtual - it'll be activated whenever a turn starts for its identifier (e.g. when a method is invoked on it or a reminder is triggered) and will be deactivated at some unknown point beyond that (up to the runtime), but re-activated whenever another turn is triggered.

I don't anticipate issues with any constructor-injected services so long as the DI registrations aren't themselves subject to change (e.g. you're creating a scoped or transient EF context from a connection string in Key Vault that you delete out of the blue at some point).

Creating your own scope isn't necessary.

WhitWaldo commented 1 week ago

@hittaKAFFE I was reading through other issues this morning and also noticed this response which might be of some use to you. Here, the user did find value in creating their own scope to gain access to a correlation identifier that would be consistent across the scoped request, so there may indeed be some limited value in doing that, but I struggle to think of any other similar situations off the top of my head.

hittaKAFFE commented 1 week ago

Cool, thanks @WhitWaldo.