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

Non-static GrainClient #2822

Merged
merged 32 commits into from
Mar 10, 2017
Merged

Non-static GrainClient #2822

merged 32 commits into from
Mar 10, 2017

Conversation

ReubenBond
Copy link
Member

This PR implements the majority of the remaining items in #467. With this PR merged, an end-user can

  1. Call into multiple separate clusters from a client
  2. Call into multiple other clusters from within one cluster

The core abstraction which this introduces is IClusterClient, which implements IGrainFactory. It is implemented by the public class ClusterClient. ClusterClient has static Create methods which match the existing GrainClient.Initialize methods with the major difference that they create and do not start the client. A client is started via a call to Start, which is asynchronous.

This does not implement support for automatically binding GrainReferences to their originating cluster, but there is an IGrainFactory.BindGrainReference(ref) method for that purpose - so users aren't completely stuck. The plan is to implement those kinds of features later on, probably leveraging the clusterId concept from GeoClusters.

Calling from one grain into multiple other clusters:

public class ConnectorGrain : Grain, IConnectorGrain
{
    private readonly IClusterClient clientOne = ClusterClient.Create(
        new ClientConfiguration
        {
            Gateways = new List<IPEndPoint>(new[] {new IPEndPoint(IPAddress.Loopback, 11112)}),
            PreferedGatewayIndex = 0
        });

    private readonly IClusterClient clientTwo = ClusterClient.Create(
        new ClientConfiguration
        {
            Gateways = new List<IPEndPoint>(new[] {new IPEndPoint(IPAddress.Loopback, 22222)}),
            PreferedGatewayIndex = 0
        });

    public override async Task OnActivateAsync()
    {
        await base.OnActivateAsync();
        await this.clientOne.Start();
        await this.clientTwo.Start();
    }

    public async Task<string> Three()
    {
        var one = this.clientOne.GetGrain<IOneGrain>(Guid.NewGuid());
        var two = this.clientTwo.GetGrain<ITwoGrain>(Guid.NewGuid());
        return await one.One() + " + " + await two.Two();
    }
}

Similarly, for standalone clients:

var clientOne = ClusterClient.Create(CreateConfig(11112));
var clientTwo = ClusterClient.Create(CreateConfig(22222));
var clientThree = ClusterClient.Create(CreateConfig(33332));

await Task.WhenAll(
    clientOne.Start(),
    clientTwo.Start(),
    clientThree.Start());

var one = clientOne.GetGrain<IOneGrain>(Guid.NewGuid());
var two = clientTwo.GetGrain<ITwoGrain>(Guid.NewGuid());
var three = clientThree.GetGrain<IConnectorGrain>(Guid.NewGuid());

while (!Console.KeyAvailable)
{
    Console.WriteLine($"{one}: {await one.One()}");
    Console.WriteLine($"{two}: {await two.Two()}");
    Console.WriteLine($"{two}: {await three.Three()}");

    await Task.Delay(TimeSpan.FromSeconds(1));
}

@ReubenBond
Copy link
Member Author

I've tagged this as a work-in-progress for now. The biggest change intended for the final version is to make the client startup asynchronous

@ReubenBond ReubenBond force-pushed the non-static-grainclient branch 2 times, most recently from 53fc1fc to b4ec494 Compare March 8, 2017 03:00
@ReubenBond
Copy link
Member Author

Alright, the IClusterClient.Start() method in the latest revision is async :)

@jdom
Copy link
Member

jdom commented Mar 8, 2017

What is the reasoning for having to call Start? Should we separate the creation of the ClusterClient into something that returns an already initialized client? Close(Async) still makes sense, kind of like an async Dispose method, but it sounds like initializing shouldn't be done on the ClusterClient. What do you think?
On a separate note, we were talking with Jason that we could avoid having a static Create method on it, and separate it into ClusterClientBuilder or something of the sort, with a configuration a little bit closer to asp.net (or EF, since it's a client library). Of course this can be after the PR is merged.

@ReubenBond
Copy link
Member Author

For example, consider this flow:

  • Create client
  • Wire up event handlers (cluster disconnect, client request, maybe some custom ones from the service provider)
  • Start the client

That flow has no chance of race conditions, whereas the following does

  • Create & start the client
  • Wire up handlers

Additionally, I believe creation should be synchronous, whereas connecting to a cluster is async.

I'd be happy if we had a ClientBuilder, but I figured that change should coincide with a similar change on the silo side and at the same time, we should allow the user to provide their own DI container (and inject their own services).

Copy link
Member

@jdom jdom left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some small comments I started to write. Sending them since we also synced in person. I'll continue reviewing anyway.

remove
{
this.ThrowIfDisposed();
this.runtimeClient.ClusterConnectionLost -= value;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this shouldn't throw, right? Should be safe to remove the event handler in any order

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure what the norm is, but as of the latest commit, this event is no longer public (I kept it on IInternalClusterClient). I've removed the throw.

/// Initializes a new instance of the <see cref="ClusterClient"/> class.
/// </summary>
/// <param name="configuration">The client configuration.</param>
public ClusterClient(ClientConfiguration configuration)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

private to favor the factory method (or builder in the future)?

Copy link
Member Author

@ReubenBond ReubenBond Mar 9, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done - I made the class internal

this.ThrowIfDisposed();
using (await this.initLock.LockAsync())
{
await this.runtimeClient.Start();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no checks to see if it's already started? or is runtime client idempotent?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BTW, should at least check that it wasn't disposed after acquiring the lock

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added the disposal check after the lock (thanks). I'll add a double-start check.

{
get { return this.runtimeClient.ClientInvokeCallback; }
set { this.runtimeClient.ClientInvokeCallback = value; }
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These seem like initialization concerns, so if moving to a separate builder, they should go there

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, kept as internal for easier testing.

public async Task Start()
{
this.ThrowIfDisposed();
using (await this.initLock.LockAsync())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

all awaits in this class should use .ConfigureAwait(false)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, thanks.

/// <inheritdoc />
public void Stop()
{
this.Stop(gracefully: true).Wait();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one for example would deadlock in some environments if .ConfigureAwait(false) is not used.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

var client = this.GetClient();
client?.Stop();
client?.Dispose();
this.SetClient(null);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should SetClient always be called even with exceptions? (ie: in a finally block, or moving it before stopping?)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed, thanks.

@galvesribeiro
Copy link
Member

Great work @ReubenBond! Looking forward to have my tests back on .Net Core :P

@ReubenBond
Copy link
Member Author

I'll run functionals again before merge

@ReubenBond
Copy link
Member Author

Ok, functionals look good

@galvesribeiro
Copy link
Member

:shipit:

@jdom jdom merged commit 430a1de into dotnet:master Mar 10, 2017
@jdom
Copy link
Member

jdom commented Mar 10, 2017

Thanks! Awesome! Reuben, can you merge to vso/master?

@ReubenBond
Copy link
Member Author

@jdom huge thanks for reviewing this! I'll do the merge (there's a commit needed to avoid breakages)

@ReubenBond ReubenBond deleted the non-static-grainclient branch March 10, 2017 08:27
@lmagyar
Copy link
Contributor

lmagyar commented May 20, 2017

What is the best practice to use these myClientBuilder.Build(); and await myClusterClient.Connect(); methods?

As I see, these are not "cheap" calls, is the suggested OnActivateAsync() is a good place? Or should we use some global/singleton instance and inject it in the grain ctor???

Should we handle somewhere the ClusterConnectionLost events, or is this relevant only for the explicit stream subscriptions? If we should handle these events, as I see we have to re-Build and re-Connect everything again, is it correct?

@sergeybykov
Copy link
Contributor

As I see, these are not "cheap" calls, is the suggested OnActivateAsync() is a good place? Or should we use some global/singleton instance and inject it in the grain ctor???

Are you considering this for the case of grains talking to grains in another cluster?

The primary use case here is clients (frontends) connecting to multiple clusters. There the cost is ~the same as it was with GrainClient.Initialize().

@lmagyar
Copy link
Contributor

lmagyar commented May 22, 2017

Yes, I figured out the client story and would like to clarify the grain-to-grain version, maybe the inter-cluster streaming if possible. As I understand this would be useful for connecting Orleans uServices directly. I'm just preparing for a presentation about Orleans's features (covering even the planned features), that's why I'm asking.

@ReubenBond
Copy link
Member Author

@lmagyar this is not a tested/supported scenario. The ClientBuilder is designed for external use (i.e, outside of the context of a grain). Anecdotally, I found that it works for me when I only use the IClusterClient to make grain calls, but I am not sure that it will work correctly when streams or observers are involved. For example, the IClusterClient does not have access to the grain's scheduler to schedule those out-of-band calls on. This is possible to implement, but it's not implemented today, and certainly it is not a tested or supported scenario.

@shlomiw
Copy link
Contributor

shlomiw commented Jun 25, 2017

@ReubenBond - I'm on 1.5.0-rc. is it possible to create IClusterClient inside silo and use it to call the same silo grains and/or subscribe streams? I was hoping to use it in my shared common libs for both the client and the silo (i.e. listening to general events in the system).

I've tried but without success.

EDIT - it seems that it is working, will further check and update (hard day today :))

@galvesribeiro
Copy link
Member

@shlomiw yes, it is possible. You can have multiple IClusterClient instances inside and outside a Silo.

@shlomiw
Copy link
Contributor

shlomiw commented Jun 25, 2017

@galvesribeiro / @ReubenBond - the IClusterClient instantiation inside a silo does work, but I have now a race condition.
As I've seen, you can Connect the client to the silo only when the silo is ready to get requests.
That makes sense. But my issue that I need this Client for other contexts, i.e. a grain which uses my library which in turn uses the Client. But a call to this grain can precede the Client initialization.
I also use this shared IClusterClient for Streams subscriptions and events publishing.

To sum-up an example:

  • I have a common library of Cache which is shared among both Clients and Silos.
  • To initialize one of the Cache items - it needs to call a specific grain.
  • My common library uses IClusterClient to invoke grains, this Client must be initialized when the app starts.
  • The Silo needs to initialize this library, and therefore creates a IClusterClient which connect to the silo itself. This can be done, as I seen, only after the silo is up and ready to handle requests. I couldn't initialize the Client inside a Bootstrap Provider.
  • I get a race condition of a Grain handling a request and then trying to fetch the mentioned Cache item, but it fails since the IClusterClient wasn't initialized yet.

I can try to solve it with coordinations mechanisms, but I was wondering if there's a better approach. And it would be best to use the shared IClusterClient.

BTW - my previous solution, before 1.5.0, was having different DI to the shared library exposing IGrainFactory, Streams subscriptions etc. For the IGrainFactory inside a silo - I just captured a context of one of the grains. But it is very hacky.

What do you think? thanks in advance!

@galvesribeiro
Copy link
Member

galvesribeiro commented Jun 25, 2017

TL;DR; If you are inside the target silo, there is no point on use IClusterClient to connect to itself... You may have an abstraction on your common library (i.e. an interface) and then initialize it with the proper implementation (inside a Silo use GrainFactory and on a client IClusterClient).

When the silo starts up, the GrainFactory is already initialized and available to grain code.

In other hand, your client (assuming you are on 1.5.x and using the new non-static client from this PR) will use IClusterClient implementation which is available for external clients (it also works if you are inside a Silo but connecting to an external cluster).

There is no DI today (yet) on our client side unfortunately...

@shlomiw
Copy link
Contributor

shlomiw commented Jun 26, 2017

@galvesribeiro - I can't figure out how to use GrainClient inside a silo. See also #1780. That why I thought IClusterClient to the rescue.
What am I missing?

@galvesribeiro
Copy link
Member

Sorry @shlomiw. Please look at my updated reply. It is GrainFactory the property in the grain which is the actual client.

@shlomiw
Copy link
Contributor

shlomiw commented Jun 26, 2017

Ok, I understand. I hoped I could use IClusterClient to share the code also in the silo.
I'm going to revert it back to what it was:
Via bootstrap provider call a grain, which in turns inject the IGrainFactory and also IStreamProvider to my shared DI.
Note that this grain must be kept alive, otherwise it'll loose the stream subscriptions when it is being deactivated.

@galvesribeiro
Copy link
Member

Note that this grain must be kept alive, otherwise it'll loose the stream subscriptions when it is being deactivated

Why? The subscriptions are persistent as far as I know. If a grain is deactivated and have an active subscription, it just need to call Resume() inside OnActivateAsync() in order to receive the message. Why you think it is not persistent?

I would have a generic interface in your case. Something like this:

public interface IOrleansClient
{
      IGrainFactory GetFactory();
}

Both IClusterClient and GrainFactory implement IGrainFactory. Your shared library should return the GrainFactory instance inside a silo or IClusterClient in case it is accessing an external Cluster.

@shlomiw
Copy link
Contributor

shlomiw commented Jun 26, 2017

@galvesribeiro - first of all - thanks a lot for your quick replies! it's not trivial :)

  1. Keeping the grain alive - I use this Grain for general streaming purposes, I need this stream all the time. The grain is only a way to access the stream inside the silo. Nobody calls it, only the bootstrap provider. So after a while it'll be deactivated and the stream is lost (until re-activation of the grain), I never said it's not persistent :) so I just keep it alive with timer that calls itself (using AsReference().KeepAlive(), and and inside KeepAlive() calling DelayDeactivation(), I think it's robust enough..).

  2. Yep - I did something similar :) and put inside the interface IGrainFactory and IStreamProvider.

Again, it would be best if you'd allow IClusterClient inside the silo, maybe with specific initialization for the silo context. It would be the most elegant solution.

Thanks again!

@jdom
Copy link
Member

jdom commented Jun 26, 2017

I would indeed recommend you do not create an IClusterClient to itself. We do have intentions to make this scenario a lot simpler (basically being able to call the silo outside of the context of a grain), but in the meantime you should avoid creating a real client against itself just for this.

Also, be aware that what you are doing with the timers is not robust enough to keep the grain activated forever. Since this is just a normal grain registered in the directory, if the silo that owns that partition of the directory dies, then the activation will realize it's no longer in the directory and be deactivated. @ReubenBond is currently looking into improving this for normal grains.
Maybe the alternative is to use GrainServices? I'm not familiar enough with them to know for sure.
Alternatively you can consider using StatelessWorkers, which are not registered in the directory, and are always local to the caller (and besides the name, they are not required to be stateless).

@shlomiw
Copy link
Contributor

shlomiw commented Jun 27, 2017

@jdom - you're completly right! I have to use a local grain since it's needed per Silo. StatelessWorker is a good idea.
Thanks for pointing that out!

@galvesribeiro
Copy link
Member

@shlomiw the GrainService as @jdom suggested is also a good option if you need one per silo.

@shlomiw
Copy link
Contributor

shlomiw commented Aug 30, 2017

@ReubenBond - maybe #3362 will solve my above issue and be the elegant solution I was hoping for. To share a common infra code with IClusterClient also for the silo process.

@github-actions github-actions bot locked and limited conversation to collaborators Dec 11, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants