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

NET 6.0 ForcedScope Failed with "No Available Scope" on GET request #646

Open
fsalas opened this issue Mar 24, 2023 · 37 comments · May be fixed by #662 or #670
Open

NET 6.0 ForcedScope Failed with "No Available Scope" on GET request #646

fsalas opened this issue Mar 24, 2023 · 37 comments · May be fixed by #662 or #670
Labels

Comments

@fsalas
Copy link

fsalas commented Mar 24, 2023

First of all, great work all of you. I'm trying to port some project of mine to NET 6.0 and AspNetCore, which are new to me.
I've downloaded master, and using the Castle.Windsor.Extensions.Dependency.Injection.
When I run a simple Hello World I'm facing a "No Scope Available exception".

My initialization code is pretty simple, just trying to put all together:

private static void Main(string[] args)
{
    IApplicationInitializer initializer;
    var builder = WebApplication.CreateBuilder(args);
    builder.Host.UseTaxologicCoreInitialization("WebPortal", out initializer);
    initializer.Initialize();

    var app = builder.Build();

    app.MapGet("/", () => "Hello World!");

    app.Run();
}

IAplicationInitializer implementation does all the windsor registration.

UseTaxologicCoreInitialization is just

    public static IHostBuilder UseTaxologicCoreInitialization(this IHostBuilder hostBuilder, string _webSiteName, out IApplicationInitializer initializer)
    {
        var ioc = new WindsorContainer();
        var f = new WindsorServiceProviderFactory(ioc);
        ioc.Register(Component.For<IHostBuilder>().Instance(hostBuilder));

        initializer = new WindsorApplicationInitializer(ioc, new AppPathFinder(), _webSiteName);

        return hostBuilder.UseWindsorContainerServiceProvider(f);
    }

If I run this code, it fails at app.Run() when it gets it first request to / with an exception "No Scope Available" at line 27 of ExtensionContainerScopeCache ,
that comes fron ForcedScope constructor.

For what I can infere from the code, as I'm no expert in Castle internals, the ForcedScope stashes the current Scope, set as current the passed one and restores it on ForcedSope disposal. The problem is that when there is no current scope in the cache it fails when tries to get it to set previousScope in

internal ForcedScope(ExtensionContainerScopeBase scope)
		{
			previousScope = ExtensionContainerScopeCache.Current;
			this.scope = scope;
			ExtensionContainerScopeCache.Current = scope;
		}

I did a couple of small changes to prevent that, and the exception has gone.
These are the changes so you can evaluate them and include if you think that has value:

in ExtensionContainerScopeCache I added a getter to know if Current has value

internal static bool hasContext
		{
			get => current.Value is not null;
		}

in ForcedScope I changed the Constructor with this

internal ForcedScope(ExtensionContainerScopeBase scope)
		{
			if (ExtensionContainerScopeCache.hasContext)
				previousScope = ExtensionContainerScopeCache.Current;
			else
				previousScope = null;
			this.scope = scope;
			ExtensionContainerScopeCache.Current = scope;
		}

It just prevent the exception if there is no current scope

Looking forward to hear your comments about the changes, or any comments on how to prevent the exception by other means

Thanks in advance

Fernando

@AlexanderKot
Copy link

Hello all

Having similar problem trying to migrate working app from Windsor 5.1.2 -> 6.0.
App also targeted to .net6 (same behavior with .net7)
Exception happens on 1st request.
image
Previously in code we have UseWindsorContainerServiceProvider call
image
According to Windsor src this call must initialize ExtensionContainerScopeCache.Current, from where exception is thrown.
But it remains uninitialized.
Current is stored in AsyncLocal
internal static readonly AsyncLocal current = new AsyncLocal();

So it can be different Async context on initialization and 1st request

But why all works on previous version and do not work on Windsor 6?

@jonorossi
Copy link
Member

Looking at a diff (v5.1.2...v6.0.0), the src/Castle.Windsor.Extensions.DependencyInjection/**/* files were nearly all rewritten. Looks like most of that was in #577.

@xlegalles
Copy link

Same behavior: moving from 5.1.1 to 6.0, all tests are broken...

@jonorossi
Copy link
Member

To be clear, as fair as I'm aware no one is looking at fixing this, someone will need to step up to fix it. Make sure to add unit tests so it doesn't get broken again.

@xlegalles
Copy link

Do you mean that it's not considered as a bug? Weird...

@jonorossi jonorossi added the bug label Jul 27, 2023
@jonorossi
Copy link
Member

Do you mean that it's not considered as a bug? Weird...

No, I mean this is an open source project that needs contributors.

@xlegalles
Copy link

Do you mean that it's not considered as a bug? Weird...

No, I mean this is an open source project that needs contributors.

Ok, understood. We have reverted our code to 5.1.2 but as soon as I have a time slot, I will do my best to help on this one.

@rvdginste
Copy link
Contributor

I was involved with the original change for #577 (initial commit was a minimal change in Extensions.DI and was related to the root scope, not clear what changes were done afterwards) and will look at.

If someone can add a unit test that exposes the bug, that would be super useful!

@zhiweiv
Copy link

zhiweiv commented Aug 1, 2023

Is it possible revert #577 as a fix solution? Release 6.0 is not usable due to this issue.

I tried revert #577 in local and it works after that.

@rvdginste
Copy link
Contributor

Hi, I already had a look at it this weekend using the original commits on the branch (commits were squashed before merge) and from my tests it seems the initial commits were fine and the bug was introduced in later commits (as part of the same change). #577 was a fix for #563 and I'd really like to implement a fix instead of reverting everything.

My next steps are to first add an extra unit test. I could reproduce the error when actually setting up and running a minimal Asp.Net Core app (as reported by @fsalas ), but not without Asp.Net Core. I will add a test that uses the Asp.Net Core testing facility and see if that works for the test case. Then I will work on the fix itself and use the new test as a reference.

Is there a specific reason you need to upgrade to 6.0, or can you hold off for now and stick with 5.1.1/5.1.2? I plan to have a fix for this by the end of the coming weekend. I use this extension myself on several applications that are in active development, so it's in my own interest to have it fixed sooner rather than later.

@AGiorgetti
Copy link

AGiorgetti commented Aug 4, 2023

The "No Scope Available" exception might be caused by threads spawn from the Threadpool with UnsafeQueueUserWorkItem (which AspNetCore internally uses, just check the repo), these threads do not capture the ExecutionContext.

Looking at the code I think the problem lies in ExtensionContainerScopeCache which uses an AsyncLocal to track the current scope.

It's pretty straightforward setup an example that shows that AsyncLocal are uninitialized in threads spawn that way.

    internal static class Program {

        private static AsyncLocal<int> asyncLocalValue = new AsyncLocal<int>();

        public static void Main() {
            asyncLocalValue.Value = 42;

            ThreadPool.QueueUserWorkItem(state => {
                Console.WriteLine($"Value on ThreadPool thread: {asyncLocalValue.Value}"); 
                // Output: Value on ThreadPool thread: 42
            });

            ThreadPool.UnsafeQueueUserWorkItem(state => {
                Console.WriteLine($"Value on ThreadPool thread (unsafe): {asyncLocalValue.Value}"); 
                // Output: Value on ThreadPool thread: 0 !!!
            }, null);

            Task.Run(() => {
                Console.WriteLine($"Value on async/await-based task: {asyncLocalValue.Value}"); 
                // Output: Value on async/await-based task: 42
            });

            Console.WriteLine($"Value in Main thread: {asyncLocalValue.Value}"); 
            // Output: Value in Main thread: 42
        }
    }

previously this it was "working" because there was no null check for the Current Scope cache, null was an allowed value.

AGiorgetti added a commit to AGiorgetti/Windsor that referenced this issue Aug 4, 2023
…removed null

check on scope cache (AsyncLocal can be null on Threads coming from
Threadpool.UnsafeQueueUserWorkItem, having no null check was also the
original behavior)
rvdginste added a commit to rvdginste/Windsor that referenced this issue Aug 6, 2023
To fix issue castleproject#563 (support concurrently existing containers) a fix was
created in castleproject#577.  After the initial fix some refactorings were done on
the scope implementation that were not correct.

This commit changes the scope implementation back to the original
implementation of @ltines, but with the support for concurrent
containers.
rvdginste pushed a commit to rvdginste/Windsor that referenced this issue Aug 6, 2023
…removed null

check on scope cache (AsyncLocal can be null on Threads coming from
Threadpool.UnsafeQueueUserWorkItem, having no null check was also the
original behavior)
rvdginste added a commit to rvdginste/Windsor that referenced this issue Aug 6, 2023
rvdginste pushed a commit to rvdginste/Windsor that referenced this issue Aug 6, 2023
rvdginste added a commit to rvdginste/Windsor that referenced this issue Aug 6, 2023
To fix issue castleproject#563 (support concurrently existing containers) a fix was
created in castleproject#577.  After the initial fix some refactorings were done on
the scope implementation that were not correct.

This commit changes the scope implementation back to the original
implementation of @ltines, but with the support for concurrent
containers.
@rvdginste rvdginste linked a pull request Aug 6, 2023 that will close this issue
@rvdginste
Copy link
Contributor

I created a bugfix for this in PR #662. The scope implementation is now again closer to what @ltines implemented originally, but with the essentials of the earlier change preserved.

I added a test to simulate what @fsalas reported using WebApplicationFactory.
@AGiorgetti , I took your test with ThreadPool also in my PR.
Both tests fail before the fix and are green after the fix.

Not sure whether this will work in all circumstances and did not have time yet to test this in a real application, but the implementation is a lot closer to what it was originally (implementation of @ltines).

@jonorossi
Copy link
Member

Great work @rvdginste. Can I get some people on this ticket to at minimum code review the changes, would be great if someone can test them in their own application.

@AGiorgetti
Copy link

AGiorgetti commented Aug 7, 2023

I tested the fix in "production code" and it does not work, the issue moved to Root Scope Accessor.

In your latest commit @rvdginste Root scope is cached in an AsyncLocal (originally it was a plain static property), now I get "no root scope" exceptions here:

internal class ExtensionContainerRootScopeAccessor : IScopeAccessor
	{
		public ILifetimeScope GetScope(CreationContext context)
		{
			if (ExtensionContainerScope.Current == null)
			{
				throw new InvalidOperationException("No root scope");  // <-- critical point
			}

			if (ExtensionContainerScope.Current.RootScope == null)
			{
				throw new InvalidOperationException("No root scope");
			}

			return ExtensionContainerScope.Current.RootScope;
		}

		public void Dispose()
		{
		}
	}

my solution combines AspNetCor and Akka.net and does lots of resolution on background and threadpool threads.

the root the problem is still the same: AsyncLocal is uninitialized in thread created in an unsafe way.

Another way to always make things that use AsyncLocal fail is call: ExecutionContext.SuppressFlow() and try to resolve something.

I'll try to setup another test that reproduce the issue on root scope accessor too.

@AGiorgetti
Copy link

AGiorgetti commented Aug 7, 2023

Here are a couple more cases that can fail:

		[Fact]
		public async Task Can_Resolve_From_CastleWindsor() {

			var serviceProvider = new ServiceCollection();
			var container = new WindsorContainer();
			var f = new WindsorServiceProviderFactory(container);
			f.CreateBuilder(serviceProvider);

			container.Register(
				// Component.For<IUserService>().ImplementedBy<UserService>().LifestyleNetTransient(),
				Classes.FromThisAssembly().BasedOn<IUserService>().WithServiceAllInterfaces().LifestyleNetStatic()
				);

			IServiceProvider sp = f.CreateServiceProvider(container);

			IUserService actualUserService;
			actualUserService = container.Resolve<IUserService>();
			Assert.NotNull(actualUserService);

			TaskCompletionSource<IUserService> tcs = new TaskCompletionSource<IUserService>();

			ThreadPool.UnsafeQueueUserWorkItem(state => {
				IUserService actualUserService = null;
				try {
					// resolving (with the underlying Castle Windsor, not using Service Provider) with a lifecycle that has an
                                        // accessor that uses something that is AsyncLocal might be troublesome.
                                        // the custom lifecycle accessor will kicks in, but noone assigns the Current scope (which is uninitialized)
					actualUserService = container.Resolve<IUserService>();
					Assert.NotNull(actualUserService);
				}
				catch (Exception ex) {
					tcs.SetException(ex);
					return;
				}
				tcs.SetResult(actualUserService);
			}, null);

			// Wait for the work item to complete.
			var task = tcs.Task;
			IUserService result = await task;
			Assert.NotNull(result);
		}

		[Fact]
		public async Task Can_Resolve_From_ServiceProvider_cretaed_in_UnsafeQueueUserWorkItem() {

			var serviceProvider = new ServiceCollection();
			var container = new WindsorContainer();
			var f = new WindsorServiceProviderFactory(container);
			f.CreateBuilder(serviceProvider);

			container.Register(
				// Component.For<IUserService>().ImplementedBy<UserService>().LifestyleNetTransient(),
				Classes.FromThisAssembly().BasedOn<IUserService>().WithServiceAllInterfaces().LifestyleNetStatic()
				);

			TaskCompletionSource<IUserService> tcs = new TaskCompletionSource<IUserService>();

			ThreadPool.UnsafeQueueUserWorkItem(state => {
				IUserService actualUserService = null;
				try {
					// creating a service provider here will be troublesome too
					IServiceProvider sp = f.CreateServiceProvider(container);

					actualUserService = sp.GetService<IUserService>();
					Assert.NotNull(actualUserService);
				}
				catch (Exception ex) {
					tcs.SetException(ex);
					return;
				}
				tcs.SetResult(actualUserService);
			}, null);

			// Wait for the work item to complete.
			var task = tcs.Task;
			IUserService result = await task;
			Assert.NotNull(result);
		}

Trying to resolve (with the underlying Castle Windsor) something that has a lifestyle that kicks in any scope accessor will result in the usual AsyncLocal problem.

It's a pretty ugly case but in my application it happens because of different "adapters" for dependency injection that use the same Castle Windsor container (AspNetCore and Akka, to be more specific, but may happens in many other scenario).

Trying to create (or resolve) a Service Provider in an Unsafe Thread has the same problem.

EDIT: the typical problematic scenario is like this:

  • configure an AspNet application
  • use the Castle Windsor adapter to "replace" the standard Ms DI.
  • configure AspNet to use logging and any other standard feature.
  • register "something" that depends on the Ms extension logger.
  • start a background unsafe thread (like the ones of AspNet Core)
  • try to resolve "something" from the Castle Winsdor container (not from the adapted IServiceProvider) and you'll get the error
  • the problem happens because the standard aspnet core services will be registered in castle with the scoped lifesyles that use ExtensionContainerRootScopeAccessor or ExtensionContainerScopeAccessor

@AGiorgetti
Copy link

AGiorgetti commented Aug 9, 2023

Hello again, I had the time to work on the subject again.

Take a look at this branch here:
https://github.com/AGiorgetti/Windsor/tree/feature/di_asynclocal_threadpoolunsafe

I added some more test about resolving services with different lifestyle with WindsorContainer and the adapted IServiceProvider: they show supported and unsupported scenarios (at this stage I let the tests fail on purpose instead of checking for the generated exception, will add that later).

A couple of tests (Can_Resolve_LifestyleNetStatic_From_WindsorContainer, Can_Resolve_LifestyleTransient_From_WindsorContainer) deserve attention: they succeed because Transient and Scoped object ends up being tied to the root scope, which is a memory leak, maybe we can find a way to let them fail if we try to resolve those services from the rootscope when using an IServiceProvider. this is however a standard behavior of all containers.

I moved the null check from the cache to the Scope Accessors and improved the error message.

As an experiment I added a global option to map NetStatic lifestyle to the standard Castle Windsor Singleton lifestyle, I believe that singletons should always be resolved whether we have a root scope or not (this can solve some issues like the one reported in: #639) .

EDIT:

gavinschultz pushed a commit to spectraqest/Windsor that referenced this issue Sep 12, 2023
gavinschultz pushed a commit to spectraqest/Windsor that referenced this issue Sep 12, 2023
gavinschultz pushed a commit to spectraqest/Windsor that referenced this issue Sep 12, 2023
To fix issue castleproject#563 (support concurrently existing containers) a fix was
created in castleproject#577.  After the initial fix some refactorings were done on
the scope implementation that were not correct.

This commit changes the scope implementation back to the original
implementation of @ltines, but with the support for concurrent
containers.
@SandraKrogh
Copy link

SandraKrogh commented Jan 8, 2024

Hi, I am experiencing a similar issue. Is there any chance this PR could be merged anytime soon, to see if it fixes the problem? @AGiorgetti #664

Thanks in advance

@lmorvan
Copy link

lmorvan commented Jan 12, 2024

For some reason I'm not aware of, my code that used to work with previous versions does not work with the latest. I have added a comment in #595 but I think it's worth having it here.

@loicmorvan
Copy link

I have reproduced the bug with ASP.NET 6 for both gRPC and simple Web API.
https://github.com/loicmorvan/WindsorAspError

@incandera
Copy link

Hey all, thanks for all the work you do. 👍 Been following this issue for a bit; I am experiencing the exact same issue when using the current v6 version. Could the proposed fixes please be made available? Would like to stay on v6+ and not have to revert to v5 if at all possible...

@lmorvan
Copy link

lmorvan commented Jan 22, 2024

This Pull Request seems to fix the issue #666

@jonorossi
Copy link
Member

Hi everyone,

We've got 3 pull requests that I think are meant to solve the same problem. Can pull request authors and others please work together to determine which pull request I'm looking at. Thanks.

@lmorvan
Copy link

lmorvan commented Feb 1, 2024

In my opinion, #666 is simple enough to be reviewed. And it includes part of the tests that were made in #662 .

@kbl-schleupen
Copy link

In my opinion, #666 is simple enough to be reviewed. And it includes part of the tests that were made in #662 .

I agree. To be honest, i did not look at the other implementations. I tested this one because of the comment in the PR and it just worked. So i suggest @jonorossi can stick primarily with #666 and if it is okay, merge it.

@eekodeerder
Copy link

In my opinion, #666 is simple enough to be reviewed. And it includes part of the tests that were made in #662 .

I agree. To be honest, i did not look at the other implementations. I tested this one because of the comment in the PR and it just worked. So i suggest @jonorossi can stick primarily with #666 and if it is okay, merge it.

@jonorossi. It would be great if this fix could be made available through a NuGet package somehow.

alkampfergit pushed a commit to AGiorgetti/Windsor that referenced this issue Feb 9, 2024
…removed null

check on scope cache (AsyncLocal can be null on Threads coming from
Threadpool.UnsafeQueueUserWorkItem, having no null check was also the
original behavior)
@jonorossi
Copy link
Member

jonorossi commented Feb 18, 2024

As a follow up from my comment on the 24th January trying to determine which pull request I should review. I received no replies from pull request authors putting forward support for their own pull requests, so nothing happened.

There was a change 2 days ago, #668 was opened superseding #664.

We've still got 3 pull requests:

We've got 5 supporters for #666, so if no further conversation happens here by next weekend I'll merging #666 and closing the rest.

@alkampfergit
Copy link

Actually you should check #668 because the null scope is not the only problem of the extension. Due to introduction of keyed service in .NET 8 if you use libraries like Microsoft Orleans (or any other in the future) that uses this new feature for registration, the library would crash.

Also in that pull request the null scope is fixed. The PR is quite bit (I need to change lots of thing) and actually is the version I'm start using on our software. Feel free to ask for clarification etc, if you need further information.

@Servox3
Copy link

Servox3 commented Feb 20, 2024

I'll wote for #668. We should definitely support keyed service in .NET 8.

@lmorvan
Copy link

lmorvan commented Feb 20, 2024

Support for keyed service in .NET 8 is certainly a good thing to do, yet this issue is about a bug with ASP.NET 6. Is it not too early to push for keyed services?

@alkampfergit
Copy link

alkampfergit commented Feb 20, 2024

These are two unrelated problem. Problem in this issue affects .NET 6 or greater, but if you want to support .NET 8 you surely need to support Keyed service, or you are stuck using .NET up to version 7.

My comment was for @jonorossi that previously states

We've got 5 supporters for #666, so if no further conversation happens here by next weekend I'll merging #666 and closing the rest.

My object was on "closing the rest" because PR #666 only address this issue, but still you will miss support for .NET 8 that was released months ago.

So I think that PR #666 can be closed quickly to address this problem, but if you close also #668 you will miss support for .NET 8

@kbl-schleupen
Copy link

If #668 also fixes the ASP.Net Core 6+ bug and adds support for keyed registrations, I'm fine with it. It's mid-march and the migration of our solutions to .NET 8 is still blocked due to this issue. Last comment is three weeks ago.

Let me ask the other way around: is anyone against #668? If not, @jonorossi please review this one...

@ltines ltines linked a pull request May 19, 2024 that will close this issue
@ltines
Copy link
Contributor

ltines commented May 19, 2024

Hi all,

had a look through the 3 PRs @jonorossi mentioned above

few things:

  • I am able to replicate it, and it's indeed due to changes in Castle.Windsor.Extensions.DependencyInjection: support parallel containers (see #563) #577 .
  • The root cause is assumption that code that uses scope (i.e. any call to WindsorScopedServiceProvider or WindsorScopeFactory.CreateScope) is always the same async context as the one that created WindsorServiceProviderFactory.
  • That doesn't seem to be true for ASP.NET .NET 6+ (where request are new threads), Orleans and some others.

I have a fix #670 that uses Root scope when the current one is null

@nallejacobsson
Copy link

nallejacobsson commented Jul 2, 2024

Any news? It's July now. Maybe just focus on fixing this bug first (6.0.1 ?) and then adding support for .NET 8 in a separate release.

Fixing this bug would at least make 6.x usable in .NET 8 as long as you don't use keyed services, right?

I had to downgrade to 5.x to make stuff work on my end for now.

@alkampfergit
Copy link

@nallejacobsson It is not true, if you use in .NET 8 any external library can use keyed service and fails miserably. As an example, Microsoft Orleans is unusable. So, if you want to move to .NET 8, if you do not have keyed service available, you are in a dangerous position, because everything seems to work, until you upgrade one of the library and it starts using keyed service....

@nallejacobsson
Copy link

nallejacobsson commented Jul 3, 2024

@alkampfergit Ah, yes, I understand. In that case I vote for fixing both issues at the same time. I wish I was skilled enough to help.

I already moved some projects to .NET 8 using 5.x. I guess I'm living dangerously now... Do you know what would happen if an external library tried to use a keyed service? What kind of exception?

@kbl-schleupen
Copy link

Tbh: I gave up on this. @nallejacobsson Imagine they split the bug fix and the addition of NET 8 support. With the current speed it has been reviewed, we would have the NET 8 support in 2025/2026 i guess?

We forked the fix of @alkampfergit and it's working fine for weeks now. Imho, let us focus on what's working and just propose #668. So maybe it is also a solution for you?

Every distraction leads to further delays...

@nallejacobsson
Copy link

@kbl-schleupen I agree, #668 it is! @jonorossi Do you have time to look it over?

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