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

Co-hosted and Service Fabric #6961

Closed
StephaneMartignier opened this issue Feb 17, 2021 · 11 comments
Closed

Co-hosted and Service Fabric #6961

StephaneMartignier opened this issue Feb 17, 2021 · 11 comments
Assignees
Labels
stale Issues with no activity for the past 6 months
Milestone

Comments

@StephaneMartignier
Copy link

Hi everyone,

So I'm interested to use the co-hosted client but with Service Fabric.

So fare we have a web api as Stateless Service on Service Fabric. We have configured it based on this documentation.

protected override IEnumerable<ServiceInstanceListener> CreateServiceInstanceListeners()
        {
            return new ServiceInstanceListener[]
            {
                CreateOrleansServiceListener(),
                CreateApiServiceListener()
            };
        }

        private ServiceInstanceListener CreateApiServiceListener()
        {
            return new ServiceInstanceListener(serviceContext =>
                    new KestrelCommunicationListener(serviceContext, "ServiceEndpoint", (url, listener) =>
                    {
                        ServiceEventSource.Current.ServiceMessage(serviceContext, $"Starting Kestrel on {url}");

                        return new WebHostBuilder()
                                    .UseKestrel()
                                    .ConfigureServices(
                                        services => services
                                            .AddSingleton<StatelessServiceContext>(serviceContext)
                                            .AddSingleton<FabricClient>(new FabricClient()))
                                    .UseContentRoot(Directory.GetCurrentDirectory())
                                    .UseStartup<SFStartup>()
                                    .UseServiceFabricIntegration(listener, ServiceFabricIntegrationOptions.None)
                                    .UseUrls(url)
                                    .Build();
                    }));
        }

        private ServiceInstanceListener CreateOrleansServiceListener()
        {
            // Getting whether this applicaton runs on a local cluster or a cluster deployed on a cloud
            var configurationPackage = Context.CodePackageActivationContext.GetConfigurationPackageObject("Config");

            var clusterListener = OrleansServiceListener.CreateStateless(
            (fabricServiceContext, siloHostBuilder) =>
            {
                // Get connection strings from the service parameters
                var storageConnectionStringParameter = configurationPackage.Settings.Sections["ConnectionStrings"].Parameters["StorageConnectionString"];
                var storageConnectionString = storageConnectionStringParameter.Value;

                var fileBlobStorageConnectionStringParameter = configurationPackage.Settings.Sections["ConnectionStrings"].Parameters["FileBlobStorageConnectionString"];
                var fileBlobStorageConnectionString = fileBlobStorageConnectionStringParameter.Value;

                // Add Configuration to DI
                var config = new PortalConfiguration
                {
                    BlobStorageConnectionString = fileBlobStorageConnectionString
                };

                siloHostBuilder.UseStandardSiloConfig(
                    fabricServiceContext,
                    "development",
                    storageConnectionString);

                siloHostBuilder.ConfigureServices(services =>
                {
                    services.AddTransient(_ => config);
                    services.AddHttpClient();
                    services.AddSingleton(_ => new FabricClient());
                    services.AddSingleton(_ => fabricServiceContext);
                });

                // Add grains
                siloHostBuilder.ConfigureApplicationParts(parts =>
                {
                    parts.AddApplicationPart(typeof(IFileManagementGrain).Assembly).WithReferences(); // Interfaces
                    parts.AddApplicationPart(typeof(FileManagementGrain).Assembly).WithReferences(); // Implementation
                });
            });

            return clusterListener;
        }

Then we configure the client in the ConfigureServices of the Startup

public virtual void ConfigureServices(IServiceCollection services)
        {
            var clusterClient = this.GetClient();

            services.AddSingleton(clusterClient);

            services.AddCorsConfigurationWithoutSignalR(CorsPolicy);
            services.AddControllers().AddNewtonsoftJson();

            services.AddSwaggerGen(x =>
            {
                x.SwaggerDoc("v1", new OpenApiInfo
                {
                    Version = "v1",
                    Title = $"{nameof(Echino.Portal.Services.Api)} API",
                    Description = $"API of {nameof(Echino.Portal.Services.Api)}"
                });

                //var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
                //var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
                //x.IncludeXmlComments(xmlPath);
            });
        }

Basically all that our API is doing is offer entry point as controller and calling grain like

        /// <summary>
        /// Get a list of existings projects
        /// </summary>
        /// <returns>List of <see cref="Project"/></returns>
        [HttpGet]
        [Route("/api/[controller]")]
        [ProducesResponseType(typeof(List<Project>), 200)]
        public async Task<ActionResult<List<Project>>> GetProjects()
        {
            var projectsManagementGrain = _clusterClient.GetGrain<IProjectsManagementGrain>(_appName);

            var result = await projectsManagementGrain.GetProjectsAsync();

            return Ok(result);
        }

From what we understood of the documentation we are in the case where the co-hosted client can be used, but we are unable to configure it and we can't find any documentation on how to do it. Is it impossible to use the co-hosted client with ServiceFabric?

Thanks a lot

@ReubenBond
Copy link
Member

Hi @StephaneMartignier, in your case, you have two DI containers: one for ASP.NET and one for Orleans. If you want, you can use the .NET Generic Host and host Orleans and ASP.NET in the same container/host. You configure Orleans by calling the IHostBuilder.UseOrleans method on the host builder.

You would create a Service Fabric statefull/stateless service listener which creates/starts/stops/disposes that host builder. To be clear, you would not need the Orleans Service Fabric integration package with that approach.

One of the internal Orleans + Service Fabric services adopts that approach, and I find it preferable.

@ReubenBond ReubenBond self-assigned this Feb 18, 2021
@ReubenBond ReubenBond added this to the Triage milestone Feb 18, 2021
@StephaneMartignier
Copy link
Author

Hi @ReubenBond,

Thanks for the answer. Have you any code sample that I can take a look at for better understanding?
Because I don't undertand all of your answer, I've tried to use the new Generic Host with Service Fabric but I can't make it work,
I've seen in this GitHub issue (microsoft/service-fabric-aspnetcore#48) that for now the genreric host is not compatible with Service Fabric right?

@ReubenBond
Copy link
Member

You can certainly host a service built with the .NET Generic Host inside a Service Fabric service. Here are some code snippets that you could fix up and use yourself (and maybe other people on that Service Fabric thread would find them useful, too):

/// <summary>
/// Service Fabric <see cref="ICommunicationListener"/> which integrates with an <see cref="IHost"/> instance. 
/// </summary>
internal class HostedServiceCommunicationListener : ICommunicationListener
{
    private readonly Func<Task<IHost>> _createHost;
    private IHost _host;

    public HostedServiceCommunicationListener(Func<Task<IHost>> createHost)
    {
        _createHost = createHost ?? throw new ArgumentNullException(nameof(createHost));
    }

    /// <inheritdoc />
    public async Task<string> OpenAsync(CancellationToken cancellationToken)
    {
        try
        {
            _host = await _createHost();
            await _host.StartAsync(cancellationToken);
        }
        catch
        {
            Abort();
            throw;
        }

        // This service does not expose any endpoints to Service Fabric for discovery by others.
        return null;
    }

    /// <inheritdoc />
    public async Task CloseAsync(CancellationToken cancellationToken)
    {
        IHost host = _host;
        if (host is object)
        {
            await host.StopAsync(cancellationToken);
        }

        _host = null;
    }

    /// <inheritdoc />
    public void Abort()
    {
        IHost host = _host;
        if (host is null)
        {
            return;
        }

        var cancellation = new CancellationTokenSource();
        cancellation.Cancel(false);

        try
        {
            host.StopAsync(cancellation.Token).GetAwaiter().GetResult();
        }
        catch
        {
            // Ignore.
        }
        finally
        {
            _host = null;
        }
    }
}

In your StatelessService implementation, you would then build the host like this (note you could use Host.CreateDefaultBuilder() instead of new HostBuilder(), if you want the defaults it provides):

public class MyStatelessService : StatelessService
{
    protected MyStatelessService(StatelessServiceContext serviceContext) : base(serviceContext)
    {
    }

    private async Task<IHost> CreateHostAsync(StatelessServiceContext serviceContext)
    {
        try
        {
            IHostBuilder hostBuilder = new HostBuilder();

            hostBuilder.UseEnvironment(this.EnvironmentName);

            hostBuilder.UseOrleans(siloBuilder => /* */);

            // Since the Web service depends on the silo being started, add it after the silo.
            // This ensures that the silo is started completely before starting the Web service.
            hostBuilder.ConfigureServices(
                services =>
                {
                    // Configure a grace period for host shutdown.
                    services.Configure<HostOptions>(options => options.ShutdownTimeout = TimeSpan.FromMinutes(2));

                    // Report service health to Service Fabric in the background 
                    services.AddSingleton<IHostedService>(serviceProvider => /* ... */);
                });

            // Configure ASP.NET
            hostBuilder.ConfigureWebDefaults(/* */);

            return hostBuilder.Build();
        }
        catch (Exception e)
        {
            // Log and throw
            throw;
        }
    }

    protected sealed override IEnumerable<ServiceInstanceListener> CreateServiceInstanceListeners()
    {
        // Create a listener which creates and runs an IHost
        yield return new ServiceInstanceListener(
            context => new HostedServiceCommunicationListener(
                () => this.CreateHostAsync(context),
                "HostedServiceListener"));
    }
}

@StephaneMartignier
Copy link
Author

Hi,

thanks for the code :)

Unfortunately I won't have the time to dig into before next week due to others priorities popping up

I will reach back to you on this thread when I've done some work on it

Thanks again

@wusi2cool
Copy link

@StephaneMartignier Where you able to get this to work. I tried using @ReubenBond code snippet also but I could not get it work

@StephaneMartignier
Copy link
Author

Hi @wusi2cool, I wasn't able to get this to work, I was on more urgent work and also on some vacations

@ReubenBond
Copy link
Member

@wusi2cool what issues are you hitting?

@wusi2cool
Copy link

@ReubenBond This is the configuration I am using. I cannot reach the application when using this configuration. My knowledge of service fabric is limited, so I am pretty sure I must have done something wrong.
<--ServiceManifest-->




    protected override IEnumerable<ServiceInstanceListener> CreateServiceInstanceListeners()
    {
        yield return new ServiceInstanceListener(context => 
        {
            return new HostedServiceCommunicationListener(() =>
            {
                return this.CreateHostAsync(context);
            }, "ServiceEndpoint");
        });
    }
  
   //tested this on a plane asp.net core project and it worked
    private async Task<IHost> CreateHostAsync(StatelessServiceContext serviceContext)
    {
            IHostBuilder hostBuilder = new HostBuilder();

            hostBuilder.ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder.UseStartup<Startup>();
            });
            return hostBuilder.Build();
    }
    
     //same as yours 
     internal class HostedServiceCommunicationListener : ICommunicationListener
    {
        private readonly Func<Task<IHost>> _createHost;

        private IHost _host;

        public HostedServiceCommunicationListener(Func<Task<IHost>> createHost, string name)
        {
            _createHost = createHost ?? throw new ArgumentNullException(nameof(createHost));
        }
        public void Abort()
        {
            IHost host = _host;
            if (host is null)
            {
                return;
            }

            var cancellation = new CancellationTokenSource();
            cancellation.Cancel(false);

            try
            {
                host.StopAsync(cancellation.Token).GetAwaiter().GetResult();
            }
            catch
            {
                // Ignore.
            }
            finally
            {
                _host = null;
            }
        }

        public async Task CloseAsync(CancellationToken cancellationToken)
        {
            IHost host = _host;
            if (host is object)
            {
                await host.StopAsync(cancellationToken);
            }

            _host = null;
        }

        public async Task<string> OpenAsync(CancellationToken cancellationToken)
        {
            try
            {
                _host = await _createHost();
                await _host.StartAsync(cancellationToken);
            }
            catch
            {
                Abort();
                throw;
            }

            return null;
        }
    }
}

@ReubenBond
Copy link
Member

ReubenBond commented Apr 7, 2021

Are you configuring endpoints in Service Fabric? See an example here:

You would then need to read the port numbers from those endpoints and configure your silo with them. I don't see any call to .UseOrleans(...), so I assume you took it out for your sample.

@ghost ghost added the stale Issues with no activity for the past 6 months label Dec 7, 2021
@ghost
Copy link

ghost commented Dec 7, 2021

We are marking this issue as stale due to the lack of activity in the past six months. If there is no further activity within two weeks, this issue will be closed. You can always create a new issue based on the guidelines provided in our pinned announcement.

@ghost
Copy link

ghost commented Mar 4, 2022

This issue has been marked stale for the past 30 and is being closed due to lack of activity.

@ghost ghost closed this as completed Mar 4, 2022
@ghost ghost locked as resolved and limited conversation to collaborators Apr 4, 2022
This issue was closed.
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
stale Issues with no activity for the past 6 months
Projects
None yet
Development

No branches or pull requests

3 participants