Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FEATURE] Expose currently backgrounded factory tasks on DI scope #368

Open
Fube opened this issue Jan 28, 2025 · 11 comments
Open

[FEATURE] Expose currently backgrounded factory tasks on DI scope #368

Fube opened this issue Jan 28, 2025 · 11 comments

Comments

@Fube
Copy link

Fube commented Jan 28, 2025

Problem

Backgrounded factories are invisible.
If your (now backgrounded) factory depends on something in the DI scope, you have no way of preventing the scope from being disposed.
This can lead your factory to fail.

Solution

When backgrounding a task in MaybeBackgroundCompleteTimedOutFactory, would it not be possible to alert something like "hey this task is now running in the background, heads up!".

This would then allow you to prevent the scope from being disposed by doing await fusionCache.WaitForBackgroundedTasks() or something of the sort.

I see a Activities.EventNames.FactoryBackgroundMove event is fired, maybe we can create & add a FusionCacheEventsHub.BackgroundFactoryMove next to it?

Alternatives

None come to mind.

Additional context

I use FusionCache on a web server and I am fine with leaving the scope open for the backgrounded tasks once an HTTP response has been issued.
I imagine putting something in HttpContext.Response.OnCompleted to await the background factories.

@Fube
Copy link
Author

Fube commented Mar 13, 2025

If my question or proposal are not welcome, I can close the question and try to solve my issue by wrapping around IFusionCache and changing the factory method to wrap it with something like

var tsc = new TaskCompletionSource();
addToWaitGroup(tsc)

try { factory(...) }
finally { tsc.SetResult() }

@jodydonetti
Copy link
Collaborator

Hi @Fube

If my question or proposal are not welcome

Totally not the case, any and all ideas are more than welcome!

Sorry for the delay, but honestly it has just been a rough time lately with not that much time left: after the mega crunch I did to get out with v2, various daily work stuff including a quite long commute, preparing for the MVP Summit, some help I'm trying to give to another project and... life in general.

I'll try to dedicate some time to this in the next few days, will get back to you.

@Fube
Copy link
Author

Fube commented Mar 13, 2025

Hi @jodydonetti!

Thank you very much for your reply and your work on the library.
Looking forward to hearing back from you.

@jodydonetti
Copy link
Collaborator

Hi @Fube

Problem

Backgrounded factories are invisible. If your (now backgrounded) factory depends on something in the DI scope, you have no way of preventing the scope from being disposed. This can lead your factory to fail.

Yes, this has come up multiple times in the past, mostly because of EF DbContext in ASP.NET where, typically, that is scoped to the http request being processed.

Is this your case? Can you give me more context on what is your specific scenario, so I can maybe come up with something? It would be helpful.

Anyway, in the case of EF the solution was to simply switch from an automatically scoped context to an explicit one, via a IServiceScopeFactory or IDbContextFactory that can be used inside the factory to instantiate whatever you need. Would it work?

Here are some previous examples:

You can see that it worked well for them, so maybe you can start from there too? Let me kwnow.

Solution

When backgrounding a task in MaybeBackgroundCompleteTimedOutFactory, would it not be possible to alert something like "hey this task is now running in the background, heads up!".

Maybe, but I honestly feel like this is tackling the situation from the wrong perspective, because it then becomes a problem of events coordination, and passing contexts or similar around and... I don't know, it doesn't feel like it.
Anyway I'm open to discuss it further it you strongly feel it would be the right call.

This would then allow you to prevent the scope from being disposed by doing await fusionCache.WaitForBackgroundedTasks() or something of the sort.

Just FYI, I tried exploring a "deferred scope dispose" as a potential way, even directly with the EF team (see here), but you can see from their answers that it's probably not the right call.

In general I'd say that the simplest and more reasonable solution is to switch from an automatic scope disposal to a manual one, because in the end that is what you are trying to do, imho.

Also, doing await fusionCache.WaitForBackgroundedTasks() would be problematic because "which one" should you wait? All pending background factories? The last one? The first one?

I see a Activities.EventNames.FactoryBackgroundMove event is fired, maybe we can create & add a FusionCacheEventsHub.BackgroundFactoryMove next to it?

Events are not the right place anyway, since they are very thing, do not pass too much data around and the execution is generally not sync.

Additional context

I use FusionCache on a web server and I am fine with leaving the scope open for the backgrounded tasks once an HTTP response has been issued. I imagine putting something in HttpContext.Response.OnCompleted to await the background factories.

First try with the manual scope creation via the factory, I think it will work well.

Let me know, thanks!

@Fube
Copy link
Author

Fube commented Mar 16, 2025

Thank you for your thorough response @jodydonetti!

Is this your case?

I use FusionCache in an ASP.NET web application. I do not use EF, but do use NHibernate.

Can you give me more context on what is your specific scenario, so I can maybe come up with something? It would be helpful.

In fear of running into this issue, I have completely disabled background factories.
I have not run into this problem prior to opening this issue, rather I opened this issue to see if I have a way to enable background factories.

Anyway, in the case of EF the solution was to simply switch from an automatically scoped context to an explicit one, via a IServiceScopeFactory or IDbContextFactory that can be used inside the factory to instantiate whatever you need. Would it work?

Apologies, I should have been clearer in my initial question.
I have read through all previous discussions around this issue, including your proposal for a deferred scope.

Neither of these approaches are exactly what I am looking for.
Creating a scope outside of the factory and passing it to the factory suffers from the same coordination issue.

Creating a scope inside the factory, and only for the factory, makes it so that any scoped service will be recreated. So logic that works on assumptions that its scope is in a certain state will now fail as a completely new state is created for the factory.

I might be misunderstanding the deferred scope proposal, but I am uncertain as to how it would detect future, already planned, invocations on the DbContext
For example, what happens here:

var t1 = queryThingAsync();
// dispose is called, but gets blocked until t1 is finished
var t2 = queryOtherThing();
await Task.WhenAll(t1, t2);

If the deferred scope may only be blocked by queries that were made prior to its disposal being initiated, would this not introduce a race condition?

What I am really looking for is to hang scope disposal until the factories that were invoked from the FusionCache that was "resolved" from that scope (I put resolved in quotes as FusionCache, with its singleton lifetime, would be resolved from the root, and not the scope) finish running.

Maybe, but I honestly feel like this is tackling the situation from the wrong perspective, because it then becomes a problem of events coordination, and passing contexts or similar around and... I don't know, it doesn't feel like it.
Anyway I'm open to discuss it further it you strongly feel it would be the right call.

I agree. My events based coordination proposal is a bandaid fix at best.

In general I'd say that the simplest and more reasonable solution is to switch from an automatic scope disposal to a manual one, because in the end that is what you are trying to do, imho.

I 100% agree, I just have no way to convince ASP.NET to let me have full control of the scope.
It will always create a scope per request, and it will always associate the HttpContext to that scope.
Perhaps I have missed something in my research, but I could not find a way to get more control over the HTTP request associated scope other than blocking its disposal with something like WaitForBackgroundedTasks.

Also, doing await fusionCache.WaitForBackgroundedTasks() would be problematic because "which one" should you wait? All pending background factories? The last one? The first one?

I absolutely agree that this would be problematic if FusionCache was scope unaware as you would have no way to associate a task with the context in which it was spawned.
FusionCache being singleton in lifetime makes it scope unaware, which is why I opened this issue to see if there was any possible way to say "FusionCache was resolved from a scope, so associate this backgrounded factory to this scope" and then when invoking WaitForBackgroundedTasks it would only block for those associated to your scope.

I'm not suggesting this is easy (or even realistically possible), but as a proof-of-concept, I tried wrapping around FusionCache with the pseudo-code I wrote in this comment and accessing a scope-set wait-group.
I achieved this like so (this code is pretty much verbatim from ASP.NET's HttpContextAccessor:

public class FusionCacheWaitGroupAccessor
{
    private readonly AsyncLocal<Holder> _waitGroupHolder = new();

    public FusionCacheWaitGroup? WaitGroup
    {
        get => _waitGroupHolder.Value?.WaitGroup;
        set
        {
            var holder = _waitGroupHolder.Value;
            if (holder is not null)
            {
                holder.WaitGroup = null;
            }
            
            if (value is not null)
            {
                _waitGroupHolder.Value = new Holder { WaitGroup = value };
            }
        }
    }

    private sealed class Holder
    {
        public FusionCacheWaitGroup? WaitGroup;
    }
}

The issue with this approach would be that you need to remember to set the value of the WaitGroup everytime you create a scope, and without a way to intercept scope creation in Microsoft's DI engine, that's about where I ran out of ideas.

@Fube
Copy link
Author

Fube commented Mar 16, 2025

Another approach, though I have no idea how realistic it is for you, would be to somehow register a scoped IFusionCache.
The same way you can inject many named caches, maybe you can inject caches with different lifetimes?

I really am unsure if this is at all a viable path for you though as I don't know the inner-workings of the library enough to know how much of a pain this would be.

@jodydonetti
Copy link
Collaborator

Another approach, though I have no idea how realistic it is for you, would be to somehow register a scoped IFusionCache. The same way you can inject many named caches, maybe you can inject caches with different lifetimes?

Ahah, I was thinking precisely about this: FusionCache itself must be singleton, since it's a cache and the whole purpose is for it to share data globally, even among scopes.

But maybe a new, lightweight thing for scopes (as a "wrapper" of sort) may be a thing.

The problem I see is... what then: the whole problem with scopes is that dependencies are scoped as well (think about the DbContext), so in that scenario you'd have a (example) IScopedFusionCache that is scoped, and in the factory you reference a scoped DbContext. At the end of the http request the scope will be disposed, disposing also all scoped dependencies, and we would be back to square one.

How would you get into the scope disposal logic to wait for the rest?

@Fube
Copy link
Author

Fube commented Mar 16, 2025

How would you get into the scope disposal logic to wait for the rest?

If your scoped service call is within the factory, you just have to convince whoever disposes the scope, in this case ASP.NET's HTTP request pipeline, to wait until the factory is done, as the factory will always finish after whatever it ran.

So, for example (code might not compile, wrote off the top of my head):

private readonly IScopeService _scopedService;
private readonly IScopedFusionCache _scopedFusionCache;

public IActionResult Action() 
{
    var result = _scopedFusionCache.GetOrSet("key", _ => _scopedService.GetThingThatWillTakeLongerThanSoftTimeout());

    return Json(result);
}

Now imagine a IScopedFusionCache::WaitForBackgroundTasks() exists, in a middleware you could do:

app.Use((ctx, next) =>
{
    var fc = ctx.RequestServices.GetRequiredService<IScopeFusionCache>();
    ctx.Response.OnCompleted(async () => await fc.WaitForBackgroundTasks());
    await next(ctx);
});

This will allow the response to be sent and, before the framework looks to dispose, it will have to run OnCompleted which will block disposal.

Now because ASP.NET gives you the scope and disposes the scope, your IScopedService will be resolved from the same scope that the middleware is blocking.

@Fube
Copy link
Author

Fube commented Mar 16, 2025

As for non-ASP.NET HTTP request scenarios, where you create your own scope, it'd look like:

using(var scope = scopeFactory.CreateScope())
{
   // do your stuff
   
   await scope.ServiceProvider.GetRequiredService<IScopedFusionCache>().WaitForBackgroundTasks();
}

Preferably, you would be able to decorate IServiceScopeFactory so that you can run logic before the scope's disposal and block it there, but I could not find a reasonable way to do this with Microsoft's DI engine.

Alternatively, you could look into the ordering of disposal.
For example, if the ordering is deterministic and in a "first-resolved-first-disposed", you could have a dummy service that implements IDisposable and you make sure to resolve that service before everything else. Though this flow would be more counter-intuitive imo.

If you know of a way to hook onto the IServiceScopeFactory::CreateScope, or to run code right before the scope runs its dispose logic, I'd be very open to it.

@jodydonetti
Copy link
Collaborator

That's a lot of info, I like that 😬 Thanks for sharing!

I'll have to think carefully about it and maybe make some tests, will update.

@Fube
Copy link
Author

Fube commented Mar 16, 2025

Thank you!
Please let me know if you need more information or if I can assist in any way.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants