-
Notifications
You must be signed in to change notification settings - Fork 307
Generic Host #1163
Comments
Are |
Fixed! Thanks @nwoolls |
Thanks for opening the discussion, @davidfowl. @jdom pointed me here. Glad that public interface ISiloBuilder
{
ISilo Build();
ISiloBuilder ConfigureServices(Action<IServiceCollection> configureServices);
ISiloBuilder ConfigureServiceProvider(
Func<IServiceCollection, IServiceProvider> configureServiceProvider);
}
Can public interface ISilo
{
Task StartAsync(CancellationToken cancellationToken = default(CancellationToken));
// If the cancellation token is already canceled, the silo will terminate ungracefully.
// Otherwise it will perform a graceful shutdown, transfering its responsibilities to
// other silos.
Task StopAsync(CancellationToken cancellationToken = default(CancellationToken));
IServiceProvider Services { get; }
// The silo can kill itself and signal termination via this task.
Task Stopped { get; }
} In the event that an If an Is |
This looks like our IServiceProviderFactory concept. https://github.com/aspnet/DependencyInjection/blob/dev/src/DI.Abstractions/IServiceProviderFactory.cs. Thanks for the reminder, this is important for replacing the DI container.
I believe the equivalent is
Nope. The IHostedService would need to shutdown the host to terminate the other IHostedServices via IApplicationLifetime.StopApplication.
No need. StopAsync will be called if the application shuts down in any way.
IHostBuilder.Properties just allows you to round trip state during the building of the host. One reason might by idempotency. You wrote an extension methods that performed some modificationto the host and you don't want it to happen multiple times with several calls. You can use the Properties to store state and check it to see if that modification happened. IHostingEnvironment.Properties are just a grab bag of things the host wants to flow to the application. Sorta like "server variables" or "environment variables". Of course you could just use a first class service for that. |
This looks great, I think we can align with Orleans too to use this. One thing that isn't very clear here is what is the replacement for the There was no metion of |
IHostedService is the entry point. As an example, if we had this before
That's a good point. We had a discussion on what the bare minimum set of required things needed to being in the core hosting layer. Options came up but we decided that it wasn't strictly required. That of course isn't set in stone. I think it would be valuable to add it as we added logging as well (which you can make the same argument, isn't required). |
@davidfowl Just for consideration in regards to the lifetime management and how it interacts with hosted services, we (Orleans team) prototyped this some ourselves as we needed better control over the per server startup/shutdown process. This is especially relevant to us because our hosts (silos) are part of a cluster that is intended to function as a whole for distributed applications. The pattern we've come up with (and are not 100% committed to yet) consists of an observable lifetime (which we called lifecycle) and a lifecycle observer, which hosted services (or other components) could act as. These interfaces are as follows:
The stages of the lifecycle are started in order, and stopped in reverse order, providing a staged startup process that allows dependent systems to be started and coordinated (even across a cluster) before the host is considered 'active' and ready for external traffic. Our ILifecycleObservable is notably similar to the IHostLifetime, so I strongly suspect we'll be able to use our lifecycle pattern in a host built using what's been described here, but, as a general pattern, the lifecycle we're tinkering with is not limited to host lifetime, we're also using it in our grain lifecycle management (and likely any other complex start/stop sequences). The PR where we introduced this can be seen in dotnet/orleans#3270 should you want to take a deeper look. I'm not suggesting you change the lifetime pattern suggested in this thread, as it suits us fine, only bringing this similar pattern to your attention for consideration as you refine the host design. |
For my non http service fabric applications my startup looks like this
So to get rid of the self plumbing for the basic stuff, that a generic host would offer, would be nice. as for
I think unity dependency injection has a clean way of doing this. Instead of having both a service collection and a service resolver, its all the same. Meaning as soon a type has been registered, it can be resolved right away without building a new serviceProvider. This could solve the stuff about building and registering in startup. |
Nice!
Yea, that sounds like a way to solve it but we're not changing any of the dependency injection system as part of this. I'm also not a fan of having a mutable service provider. That can lead to inefficiencies and bugs. |
I work in a messaging platform that implements similar concepts for generic hosting. It currently runs both as Windows services and console applications. I will share the details of how it works and maybe can serve as inspiration in some way. This structure has been used for some years in a very large codebase, working very well for us until now. First, we have a very basic primitive caller interface IWorker
{
void Stop();
Task Execution { get; }
} The Then we have a interface IService : IWorker
{
Task StartAsync(CancellationToken cancellationToken);
} The We have another primitive called public interface IServiceContainer : IService
{
IServiceContainer Add(IService service, int tier = 0);
} When you start the container, it start all contained services in the order defined by the tier value, from the lower value to the higher. Services in the same tier are started in the same time. The Finally, we the the public interface IServiceActivator : IService
{
IDictionary<Type, IServiceProvider> ServiceProviders { get; }
ICollection<string> ActivationGroups { get; }
} The [AttributeUsage(AttributeTargets.Class)]
public sealed class ActivateAttribute : Attribute
{
public Type RegistrableType { get; set; }
public int Tier { get; set; }
public string ActivationGroup { get; set; }
} Each service is instantiated by its own container, which is created by the activator and populated by the public interface IRegistrable
{
void RegisterTo(Container container);
} The We also have a lot of helper classes for this infrastructure, like a |
This is what I came for and you did not disappoint, @davidfowl. 😂 |
@andrebires very cool! Do you have multiple DI containers in the mix? Why does the |
@davidfowl Yes, the service activator creates a DI container for each activated service and expose they throught the Also, our activator implementation receives in the constructor a root DI container and every created child DI container is linked to it, creating an DI hierarchy. If the service requires a type that is not available in its own container, it can be resolved by the root container. This is useful in some cases that we need to share some types between services. For instance, we have a connection listener service that creates "in process" transport connections to our platform, so the client connections produced by this listener should be consumed by other service. |
It is important for this spec to be clear about what ordering guarantees [or not] there are related to
This expands a general point that @jason-bragg was making above, drawing out specific experience from building the Orleans hosting mechanisms, but is also confirmed from experiences with several Java server systems I have built over the years. |
What is the purpose of the Is I think I am mostly getting confused by the choice of types used here because Also, does |
Is the Say if a hosted service gets a call to If a It might be useful to think of a state transition diagram for lifecycle stages, to help clarify some of the edge-cases. For example, is this the expectation:
Is there a concrete IRL scenario for cancelling a server |
I'll update the spec with these details.
Sequentially. Parallel startup could be a bug farm.
We'll revisit this. Today I don't think we call stop in the reverse order.
Today the DI container ensures order. Calls to register the IHostedService are ordered and will be preserved.
Those are basically events. There were made CancellationTokens because it supports both callbacks and polling. Nothing is being cancelled, it's more of a better EventHandler.
Yep, ignore that in this case. It could also be Task. See this https://github.com/dotnet/corefx/issues/16221.
Yes, they are different but they don't apply here.
Yes, they are. CancellationTokens are cooperative, so if the caller tries to cancel start up or shutdown, the implementations should get a chance to react.
No, the CancellationToken is basically a timeout. The host says, I'll give you this long time shutdown and passes a token to StopAsync. If the token fires, it's the implementation's job to stop trying to shut down gracefully and just forcefully stop instead. |
@davidfowl, @andrebires @jthelin I am of the opinion that some sort of controlled (or staged) startup/shutdown is very useful, evidenced by both the need for this within Orleans, @andrebires system, and @jthelin's experiences. As this sort of staged lifetime can be, IMO, introduced as a natural extension to the generic host, I'm not convinced it should be a first class requirement. I'd rather see a solid minimal host implementation in place earlier than waiting for one with every bell and whistle (like staged lifetime) users may want. I do, however, hope this capability is considered sufficiently in the initial version to allow its introduction by service developers or as a future improvement to the host. |
This is a minimal "silo builder". The purpose is to make it significantly easier to configure, create, and operate a silo than it is at present. We hope to collaborate on and support the generic HostBuilder abstraction once it's ready: aspnet/Hosting#1163, but this lets us continue making progress in the meantime. Sample usage: ```C# public static async Task MainAsync(string[] args) { var silo = new SiloBuilder().ConfigureLocalHostPrimarySilo().Build(); await silo.StartAsync(); Task.Run(async () => { // Wait an arbitrary amount of time and then kill the silo. // This could also be waiting for console input or some other condition. await Task.Delay(TimeSpan.FromMinutes(2)); await silo.StopAsync(); }).Ignore(); await silo.Stopped; } ``` * Moved default service implementations into `DefaultSiloServices` static class so that the implementation can be shared by SiloBuilder and Silo (for backwards compatibility) * Broke cyclic dependency between `Silo` construction and: `MembershipOracle`, `MembershipOracleData`, and `DeploymentLoadPublisher`. * Added `ISilo` interface for interacting with silo instances: * Added `ISiloBuilder` interface and `SiloBuilder` implementation for incrementally configuring and eventually constructing an `ISilo` instance. * Added an awaitable `siloTerminationTask` to `Silo` which is signaled on termination. Currently this sits alongside the similarly purposed (but not awaitable) `SiloTerminatedEvent`, which is a `WaitHandle`
The HTTP context and server are both examples where you've introduced extensibility through the notion of features. If the new That was you could introduce something like a In terms of migrating
Future release:
|
This looks a lot like NServiceBus configuration. You might want to ping @andreasohlund and see what lessons they've learned. I think they're revamping their generic hosting approach. |
Maybe, I'm not convinced yet though.
How do you imagine that being used and what would the impact of such changes be on the interface?
The interfaces don't map very well (there are improvements we want to make) and this would be a breaking change. |
@davidfowl i am assuming this is why a singleton injected into my |
One more (possibly dumb) ? if you cats don't mind ... Why wasn't it possible (or reasonable) to roll the host config and the app config together into ONE BUILDER? It seems like it would be simpler for devs if a single config builder handled both the host and the app config with one set of providers in one spot at one time. I see that if someone wanted that, they could do it themselves using the bits you've made available, but why wasn't it an option (or the default) OOB for ASP.NET Core? |
@Tratcher / @davidfowl -- was it just an issue of seperation of concerns? |
My guess is that this is due to how the configuration evolved over time. Back in the days™ we had a |
Some minimal amount of config is needed to bootstrap the host builder, IHostingEnvironment, etc.. That information is then used as input to the rest of the HostBuilder flow. E.g. |
@guardrex note we made some updates to IHostLifetime after we had trouble getting it to work with windows services. See if the new version makes any more sense: https://github.com/aspnet/Hosting/blob/38f691c09e3aae4f34be139c39ae1fb264803fd0/src/Microsoft.Extensions.Hosting/Internal/ConsoleLifetime.cs |
@Tratcher Are these updates related to this issue and if yes, I suppose they are only available in 2.1? |
@jwfx that's not related to this issue, please open a new one and give the details for your scenario. |
Re the issues where singletons aren't the same when activating the Startup class and when activating an IHostedService, are web host builders supposed to 1) switch to building their host with generic host or 2) just upgrade their use of aspnet/Hosting to version 2.1.0+ , please? |
Generic Host
As part of ASP.NET Core, we introduced a set of cross cutting concerns that are fundamental to building many applications.
These include things like configuration, dependency injection, and logging. The
WebHostBuilder
is the glue that brings those common cross cuttingconcerns together but it also couples the building of the HTTP pipeline into the same component. The goal of the generic host is to de-couple
the HTTP pipeline building from the Host API to enable other scenarios like messaging, background tasks, and supporting other non HTTP workloads that would benefit
from the same cross-cutting libraries that HTTP benefits from.
Introducing Microsoft.Extensions.Hosting
Microsoft.Extensions.Hosting is the generic hosting library that will offer a similar API to
WebHostBuilder
but won't be specific to HTTP.The eventual goal is to completely replace the
WebHostBuilder
API with this new one. The generic host will offer a few building blocksfor application models:
IHostedService
This is the entry point to code execution. Many of these may be registered in as services. They will be resolved and executed in order of registration. StartAsync will be called when the host starts, and StopAsync will be called in reverse registration order when the host shuts down gracefully. IHostedServices are registered as part of container registration and resolved by the host as an
IEnumerable<IHostedService>
.IHostBuilder
This is the main component that libraries and applications will extend to light up functionality.
IHost
The
IHost
implementation is responsible for starting and stopping all theIHostedServices
that have been registered in DI.There will also be a set of default services offered by the hosting environment:
IHostingEnvironment
The
IHostingEnvironment
provides information about the hosting environment an application is running in.IApplicationLifetime
The
IApplicationLifetime
is a service that allows the application to handle host lifetime events and also request graceful shutdown of the host.ILoggerFactory
https://github.com/aspnet/Logging
IConfiguration
https://github.com/aspnet/Configuration
IOptions<T>
https://github.com/aspnet/Options
Extensibliltiy
Host extensibility is done via extension methods on the
IHostBuilder
. The builder provides the ability to manipulate the core primitives and theexpectation is that extension methods can use this to build higher level functionality:
DependencyInjection
In order to support plugging in other containers, the host can accept an IServiceProviderFactory. This itself will not be part of the DI container registration, but will be a host intrinsic used to create the concrete DI container.
Custom container configuration can be done via the ConfigureContainer method. This gives you a strongly typed experience for configuring your container on top of the underlying API.
Host Lifetime
In order to accomodate systems that control the lifetime of the host, we introduce an new interface
Lifetime
to delegate lifetime management to another system for e.g. windows service host or console host.When
IHost.StartAsync
is called, it will register with theIHostLifetime
to notify the caller when the application started or stopped viaOnStarted
andOnStopping
. WhenIHost.StopAsync
is called, the host will notify theIHostLifetime
that it has stopped.Here's an example implementation of the
ConsoleHostLifetime
that triggers shutdown on Control + C:Startup
A note about
Startup
classes - While the concept of a Startup class is extremely useful, it's also very problematic as it requires2 dependency injection containers to be built during the startup cycle. This causes issues where singletons aren't the same when activating the
Startup class and when activating an
IHostedService
. This is because Startup.ConfigureServices lets the user add more services but also lets themget at hosting services in the Startup.ctor. So far, we've chosen to abandon startup as part of the generic host work but it would be possible to add back
at any point in the future if we figure out a cleaner way to do it.
Open Questions
WebHostBuilder
?IServiceProviderFactory<TContainer>
interface being generic might cause problems (no way to flow the generic through the system). We might need a non-generic version /cc @pakrymThe text was updated successfully, but these errors were encountered: