Migration from 1.5 to 2.0 - OrleansContrib/Orleankka GitHub Wiki

Transitional version

It might be hard to migrate huge codebase from Orleankka 1.5 to Orleankka 2.0 in a one-shot, so we prepared an alternative migration procedure you can follow to change/fix the code incrementally.

There is a special 2.0.0-transitional-XXX version of Orleankka's packages which you can use to smooth the migration. The idea behind transitional packages is to keep the change surface area small and not feel overwhelmed with so many changes at once.

Start from 2.0.0-transitional-004 and then update iteratively. Each new transitional version will be removing a single feature or changing a single api, keeping all changes in one iteration similar and small. The guidelines and advice will be given for each change, either on how to replicate the removed feature or how to proceed with the api refactoring.

After each transition (transitional version upgrade), run all tests, check on staging environment, deploy on production. That will make troubleshooting and in case of problems rollback, easier.

Version for ASP.NET web projects

It was revealed that bug was introduced in transitional-004 which exists in all transitional versions but it only manifests itself in scenarios where Orleankka client/silo is hosted within ASP.NET web application project. If that is your case, please use an alternative string of releases suffixed as wapfix (i.e. 2.0.0-transitional-wapfix-004).

2.0.0-transitional-004

There no api changes here except minor configuration differences. This is basically Orleankka 1.5 compiled against Orleans 2.0.3.

  • New Builder(Action<ISiloHostBuilder>) and Builder(Action<IClientBuilder>) methods on ClusterActorSystem and ClientActorSystem respectively. All of the latest configuration extensions in Orleans are done against those interfaces. For example OrleansDashboard registration is done via ISiloHostBuilder extension method.
  • force argument was removed from ClusterActorSystem.Stop()
  • If you were using ClusterConfiguration.AddMemoryStorageProvider("MemoryStore") extension, in Orleans 2.0 it is an extension to ISiloHostBuilder. In this version UseInMemoryGrainStore extension was added to ClusterActorSystem to replicate this functionality
  • If you were using Azure's blob storage provider you can find AddAzureBlobGrainStorage as extension to IServiceCollection. Same for UseAzureTableReminderService
  • In Orleans 2.0 logging is done via Microsoft.Extensions.Logging so reference Microsoft.Extensions.Logging.Console package and use AddLogging extension method to IServicesCollection
  • This version was checked and is confirmed to be working with OrleansDashboard 2.0.5 (this includes patch from @jkonecki)
  • StreamProvider<T> extension method was removed from both cluster and client system.

If you use declarative stream subcriptions feature you need to:

  • For TCP streaming (SMS), use UseSimpleMessageStreamProvider("sms", o => o.Configure(x => x.FireAndForgetDelivery = false)); method
  • For azure queue streams or any other persistent streams (event hub, etc) - use native registration methods on ISiloHostBuilder and IClientBuilder respectively. Then register names of the providers in cluster actor system by using RegisterPersistentStreamProviders("name1", "name2") method. See example for azure queue stream provider here

2.0.0-transitional-010

Migrating to this version will make most of your efforts since it brings you closer to one of the major breaking changes that were done in Orleankka 2.x - removal of code generation for grain interfaces and classes. If you try to migrate to Orleankka 2.x final by skipping this version it may have severe effects on your production environment so you should be aware why it's important.

ATTENTION: Simultaneously update Orleans to 2.4.4 (and keep it until the end of the transition to Orleankka 2.x final) as starting from that version it contains critical patch required for this and any subsequent transitional releases to work properly.

BUG: This version of Orleankka contains a specific bug (which retains until the last transitional version): you may run into a codegen failure upon startup if a first visible type in any of the assemblies you do register with Orleankka is a generic type. Just make it not first (ie rename or make internal) :)

Introduction

As you may know the standard way in Orleans to implement actors is to create a grain interface with methods and grain class that implements it. Then clients can invoke actor code by calling methods on this interface. This makes sense for Orleans object-oriented model of communication but not so much for Orleankka's functional actor api. Orleankka is message-based and all actor interfaces have the same shape - just a few standard methods taking single object argument (message) as a parameter. I thought why creating all these interfaces if they equal, we can just codegen those at runtime!

Another problem was that base grain class provided by Orleans had a somewhat dirty api at that time (the project was immature) and since I'm an adept of a POCO-based approach, where grain infrastructure had to be clearly separated from user business code, I have created a layer of indirection - actor shell grain which was exposing clean, unit-testable api, hiding some of the ugly parts of Orleans. To implement this the codegen was extended to also auto-generate the grain shell class (implementing auto-generated grain interface) that was delegating all the work to actual POCO-like class (Actor)

Both ideas turned out to be stupid as I was constantly battling with the framework, the feature set was lagging behind upstream and there were millions of other problems with this approach. Lessons learned - don't hide (fight) the framework!

So I made a decision to fix this situation and in 2.x version of Orleankka get rid of code generation in favor of native grain interfaces and base classes. Unfortunately, this led to a major breaking change. In 1.x auto-generated interfaces/classes were having path like Fun.X where X was either the actor type name (if ActorType attribute was used) or the full interface (if actor has custom IActor interface)/class path.

Now the only safe way to migrate live cluster from 1.x to 2.0 is to have the exact same interface/class names/paths as were auto-generated. Otherwise, the message routing will be broken until full cluster update. But what's more critical is that external services such as reminder service may store grain identity and they will be permanently broken delivering messages into a void. At the moment Orleans services don't provide an api to fix the aftermath, but luckily for all 1.x users there exists a workaround - TypeCodeOverride.

The (grand) FIX

In Orleans, the actual identity of a grain interface or class is just an integer code, a hash computed from the full path (name). Since being a calculated hash it's possible to run into hash collisions and so Orleans team wisely added support for overriding the hash by placing special TypeCodeOverride on the grain interface/class.

The question is how do you know what type code was computed? This is actually easy - look into Orleans log. During silo startup, Orleans prints all registered grain interfaces and classes along with type codes. Search for the following:

info: Orleans.Runtime.GrainTypeManager[101711]
      Loaded grain type summary for 12 types:
      Grain class Fun.Fun.Api [-1093717963 (0xBECF3035)] from Orleankka.Auto.Implementations.dll implementing interfaces: Orleans.IRemindable [-831689659 (0xCE6D6C45)], Fun.IApi [428485482 (0x198A2B6A)], Orleankka.Core.IActorEndpoint [-1771180915 (0x966DEC8D)], Orleans.IGrainWithStringKey [-1277021679 (0xB3E23211)]
      Grain class Fun.Demo.Fun.Demo.ITopic [348193966 (0x14C104AE)] from Orleankka.Auto.Implementations.dll implementing interfaces: Orleans.IRemindable [-831689659 (0xCE6D6C45)], Fun.Demo.IITopic [433931250 (0x19DD43F2)], Orleankka.Core.IActorEndpoint [-1771180915 (0x966DEC8D)], Orleans.IGrainWithStringKey [-1277021679 (0xB3E23211)]

Make a copy of this list. Here you can see type codes for grain interface and class. You will need both. For example, the type codes for IApi actor would be:

  • Class -> -1093717963
  • Interface -> 428485482

And resulting code changes respectively would be:

[TypeCodeOverride(428485482)]
public interface IApi : IActor {}

[TypeCodeOverride(-1093717963)]
public class Api : Actor, IApi {}

WARNING: Get type code listing before updating to transitional-010! Run your app while you still on a previous transitional version and grab the logs.

That is it. You will need to repeat it for every actor interface/pair in your project. Be careful to not misuse the numbers. Make sure that both the interface and the class are declared as public. Don't make any other changes before completing this procedure.

NOTE: If you'll be introducing new actor types while on this version do not forget to repeat this process for them or you may run into troubles later. The better approach will be to use TypeCodeOverride from the beginning. Just fabricate any unique integer, the number doesn't really matter, it just needs to be unique. May save you from troubles in the future.

WARNING! If you didn't have a custom interface previously defined for your actor class (yes, Orleankka 1.x was allowing this) you will need extra care. Read on this below.

Interfaceless actors

In Orleankka 1.x it was not required to define custom actor interface (IActor) since actor interfaces were auto-generated and it was possible to obtain an actor reference by actor class type or by string-based actor type alias. This version introduces a requirement that each actor class should have a custom actor interface defined as in Orleankka 2.x. You will need to explicitly declare custom IActor derived interface for each actor class if it's not present already.

Beware, that while you didn't declare the custom actor interface the one was still auto-generated by Orleankka! So all of the previous advice on type codes still apply. Look into logs, figure out what type code was associated with an auto-generated interface and put it in [TypeCodeOverride] attribute for that newly created explicit custom actor interface.

TIP: You can quickly extract an interface by using R#. Just place the cursor on actor class name and press Ctrl-Shift-R (Refactor This), then press X or select Extract Interface in the context menu. Make sure you didn't forget to specify IActor to be implemented by that custom interface.

WARNING! Even if you put the same type code that was associated with auto-generated interface, there still could be places where code was relying on serialized GrainReference string. This may lead to subtle bugs. As an example, while updating Orleankka test project, the introduction of explicit actor interfaces broke storage provider tests due to the changed grain reference key string. Previously, grain reference was having full type name in Fun. namespace as interface name, while the newly introduced interface was in a different namespace. We advise to migrate away from stock storage providers to your own before migrating to this version.

ActorType("foo")

The actor type codes act as aliases for actor interface types. This version introduces one more breaking change, requiring this attribute to be placed only on a custom actor interface. If you've been using interfaceless actors just move it from the actor class to a custom actor interface declaration.

Actor references

The last step is to check whether all invocations to ActorSystem.ActorOf<T>/ActorSystem.WorkerOf<T>/ActorSystem.TypedActorOf<T>/etc actually have custom actor interface specified for generic constraint. Since C# doesn't support interface generic constraint it's possible by error to specify an actor class that implements IActor as generic type target instead of custom actor interface and that will satisfy the compiler. The outcome of such a mistake is application failing at the run time, so apply extra care and check all invocations.

FINAL CAUTION

Once you applied type codes everywhere, created missing custom actor interfaces and fixed all actor reference invocations - run all existing tests, deploy on staging environment (if you have it) and check! If everything is ok - deploy on production environment. If nothing is broken - you're done and may upgrade to a next transitional release!

2.0.0-transitional-011

This version removes the [Interleave] attribute in favor of native [MayInterleave] attribute. To specify which message types could be interleaved you will need to create a public static predicate function. Example refactoring:

/// before

[Interleave(typeof(Foo))]
[Interleave(typeof(Bar))]
public class MyActor : Actor, IMyActor {
}

/// 2.0.0-transitional-011

using Orleans.Concurrency;

[MayInterleave(nameof(IsReentrant))]
public class MyActor : Actor, IMyActor {
   public static bool IsReentrant(object msg) => msg is Foo || msg is Bar;
}

2.0.0-transitional-012

[ActorType] attribute has gone. For the greater good. The reason is simple: since Orleankka 2.x relies on native grain interfaces, it doesn’t give anything more than just a simple alias. There is no way to make Orleans respect it, the framework will be routing messages using grain interface path. So to remove confusion I removed the attribute.

If you still need that naming indirection (perhaps you're using dynamic actor type selection or was using it for storage purposes), you can create simple helper to translate between string actor code and interface type:

static class ActorTypeMap
{
  
    static Dictionary<string, Type> byCode = new Dictionary<string, Type>();
    static Dictionary<Type, string> byType = new Dictionary<Type, string>();

    static ActorTypeMap()
    {
       Add<IFoo>("foo");

       void Add<T>(string code) where T : IActor
       {
           byCode.Add(code, typeof(T));
           byType.Add(typeof(T), code);

       }
    }
}

Alternatively, you may introduce your own ActorType attribute and populate this map by scanning assemblies at startup. Choose your weapon!

WARNING! This change may break stock storage providers, since the serialized grain reference string could be used as the storage key (AzureBlobStore)! We advise to migrate away from stock storage providers to your own before migrating to this version.

Other notable api changes

ActorPath.Type has been renamed to ActorPath.Interface and now it stores the full path of the actor interface (namespace + name).

ActorPath.From(string type, string id) has been removed. Instead of ActorPath.From("Api", "facebook") use ActorPath.For<IApi>("facebook") or ActorPath.For(typeof(TActor), id) methods.

ActorPathExtensions.ToActorPath has been removed in favor of ActorPath.For<T>.

As a consequence of actor type removal, all of the ActorSystem extension methods for acquiring actor references (ActorOf/WorkerOf/TypedActorOf/etc) now require actor interface type to be specified instead of a string type name.

WARNING: Make sure you've tested all places where code was using actor type. Storage providers, serialization, etc. Grain references and actor paths will now be pointing to the full path of the custom actor interface!

2.0.0-transitional-013

This release removes support for RequestOrigin info and respectively removes support for sending messages to self via ActorBehavior.Fire. The added value of Fire was automatic recognition of a situation when a message needs to be sent via Self or could just be handled directly (for example in testing harness). Basically, it was detecting the case when code was running inside a timer callback. This was handy but the complexity of intrinsic mechanics wasn't warranting the presence of this feature. So no more magic. To replace the semantics of Fire use Self.Ask<>/Tell/Notify when inside timer callback, otherwise call ActorBehavior.HandleReceive directly. The helper method you can create to mimic the deprecated functionality:

public abstract class ProcessManager : Actor
{
   public bool FireMessagesToSelfViaReceiveForTesting;

   public async Task FireSelf(object message)
   {
        if (FireMessagesToSelfViaReceiveForTesting)
        {
            await OnReceive(message);
            return;
        }

        var sender = Behavior.Current;
        
        try
        {
            await Self.Tell(message);
        }
        catch (UnhandledMessageException) when (sender != Behavior.Current)
        {
            Log.Warning(
                "Message `{message}` sent to the self from the behavior `{sender}` was unhandled. " +
                "Perhaps due to the current behavior `{current}` being different (ie switched). Ignoring ...",
                message.GetType(), sender, Behavior.Current);
        }
    }
}

Other notable changes

ActorBehaviorExtensions used for mocking/testing behaviors were removed from TestKit. Now you can call HandleActivate/HandleDeactivate/HandleReceive/HandleReminder directly on actor behavior. As an alternative, you may simply copy code for those extension methods into your project. Also, to test messages sent to the self, simply cast Self to ActorRefMock or acquire via mock runtime (runtime.System.MockActorOf<IMigrationManager>("XXX")) and use it to set/check expectations.

2.0.0-transitional-014

In this version, the Autorun feature has been removed. Hopefully, nobody was using it and nobody will be crying for it:) You can easily replace the functionality of this feature by using the startup task and custom activation message. See example here

2.0.0-transitional-015

Bootstrapper is gone in favor of native startup tasks (silo lifecycle participants). For more information check Orleans docs. In short, to replace the functionality of Bootstrapper use AddStartupTask extension method available for ISiloHostBuilder. To get an instance of IActorSystem use IServiceProvider which is passed as a parameter to a startup task (ie services.GetService<IActorSystem>().

2.0.0-transitional-016

[KeepAlive] was removed in favor of native configuration setting. Check how to configure grain collection age on per-type basis in Orleans docs or see example here.

WARNING: Unfortunately, there is a minor bug in this release that you should be aware of. For this to work properly you need to specify an actor type name in a special way. For, example if you have MyActor actor class which implements IMyActor actor interface you need to configure GC like this:

builder.Configure<GrainCollectionOptions>(o => o
    .ClassSpecificCollectionAge[$"Fun.{typeof(IMyActor).FullName}"] = 
        TimeSpan.FromMinutes(2));

Here, the full name of the actor interface is used rather than the full name of the actor class (major deviation from standard configuration), plus the Fun. suffix which together makes the correct full path of auto-generated grain shell class.

Alternatively, you may recreate this attribute in your project using the sample code below:

[AttributeUsage(AttributeTargets.Class)]
public class KeepAliveAttribute : Attribute
{
    public int Hours { get; set; }
    public int Minutes { get; set; }
}

// on ISiloHostBuilder
builder.Configure<GrainCollectionOptions>(o =>
{
    var actors = Assembly.GetExecutingAssembly().GetTypes()
        .Where(x => typeof(Actor).IsAssignableFrom(x) && !x.IsAbstract);
    foreach (var each in actors)
    {
        var att = each.GetCustomAttribute<KeepAliveAttribute>();
        if (att == null)
            continue;

        var hh = att.Hours;
        var mm = att.Minutes;

        var @interface = ActorInterface.Of(each);
        o.ClassSpecificCollectionAge[$"Fun.{@interface.FullName}"] = 
            TimeSpan.FromMinutes(mm + hh * 60);
    }
});

NOTE: Migration to Orleankka 2.x final would require revisiting this configuration, changing keys to be a full path of a grain class (as per Orleans docs).

2.0.0-transitional-017

Sticky actors feature has been removed. What was that for? Previously, if you put [Sticky] attribute on the actor the runtime was making sure that once this actor is activated it will stick into memory forever, ie will be alive all the time. This was done by setting infinite grain collection age for this actor type and by registering (and handling) upon actor activation a special reminder (with a minimum allowed frequency), which was creating the illusion of stickiness. It was a handy feature for all sorts of pollers, per-silo caches. etc but it's nothing special and could be easily replicated by few lines of code. Check example here.

As an alternative, this functionality could be replicated by using an interceptor (actor invoker). This will reduce the amount of repetitive code (DRY):

/// interceptor
public class StickyHandler : ActorInvoker
{
    const string StickyReminderName = "##sticky##";

    public StickyHandler(IActorInvoker next = null)
        : base(next)
    {}

    public override async Task OnActivate(Actor actor)
    {
        var period = TimeSpan.FromMinutes(1);
        await actor.Reminders.Register(StickyReminderName, period, period);
        await base.OnActivate(actor);
    }

    public override async Task OnReminder(Actor actor, string id)
    {
        if (id == StickyReminderName)
            return;

        await base.OnReminder(actor, id);
    }
}

/// apply to an actor
[Invoker("sticky")]
[TypeCodeOverride(-1483810351)]
public class AccountingFeedPoller : Actor, IAccountingFeedPoller
{}

/// register invoker
var config = ActorSystem.Configure()
     .Cluster
     .ActorInvoker("sticky", new StickyHandler());

2.0.0-transitional-018

ActorActivator is gone! Use native IGrainActivator as an actor factory. Yes, this version provides support for creating Actor instances using native actor activator api (and/or built-in DI container). However, for this to work properly you need to implement an activator in a special way.

  1. You should register it as a singleton in the service collection, same as with ActorActivator
  2. You should fallback to DefaultGrainActivator for resolving/releasing non-actor instances

Example (full version):

/// BEFORE

class DI : IActorActivator
{
    ...

    public Actor Activate(Type type, string id, IActorRuntime runtime, Dispatcher dispatcher)
    {
        if (type == typeof(Api))
            return new Api(new ObserverCollection(), ApiWorkerFactory.Create(id));

        if (type == typeof(Topic))
            return new Topic(storage);

        throw new InvalidOperationException($"Unknown actor type: {type}");
    }
}

/// AFTER
class DI : IGrainActivator
{
    readonly IServiceProvider services;
    readonly DefaultGrainActivator activator;

    public DI(IServiceProvider services)
    {
        this.services = services;
        activator = new DefaultGrainActivator(services);
    }

    public object Create(IGrainActivationContext context)
    {
        var type = context.GrainType;
        var id = context.GrainIdentity.PrimaryKeyString;

        if (!typeof(Actor).IsAssignableFrom(type))
            return activator.Create(context);

        if (type == typeof(Api))
            return new Api(new ObserverCollection(), ApiWorkerFactory.Create(id));

        if (type == typeof(Topic))
            return new Topic(services.GetService<ITopicStorage>());

        throw new InvalidOperationException($"Unknown actor type: {type}");
    }

    public void Release(IGrainActivationContext context, object grain) 
    {
        if (!typeof(Actor).IsAssignableFrom(context.GrainType))
            activator.Release(context, grain);
    }
}

Built-in dependency injection works without any additional setup. For integration with 3rd party DI containers see this example.

2.0.0-transitional-019

StatefulActor is no longer supported. We advise migrating away from stock storage providers to your own before migrating to this version.

2.0.0-transitional-020

The tiny but important change in this version is the alignment of signatures between transitional and 2.x (Orleans native) for callback method specified in [MayInterleave] attribute.

/// BEFORE
[MayInterleave(nameof(IsReentrant))]
class TestActor : Actor, ITestActor
{
    public static bool IsReentrant(object msg) => msg is ReentrantMessage;
}

/// AFTER
[MayInterleave(nameof(IsReentrant))]
class TestActor : Actor, ITestActor
{
    public static bool IsReentrant(InvokeMethodRequest req) => req.Message(x => x is ReentrantMessage);
}

Note, there is a special helper method used to extract the message from InvokeMethodRequest and fulfill the predicate. It works with all kinds of requests, including messages received via streams. It also features automatic unwrapping of Immutable.

2.0.0-transitional-021

This is the last transitional release. It aligns interceptors api between 1.x and 2.x. ActorRefInvoker and ActorInvoker were replaced with ActorRefMiddleware and ActorMiddleware respectively. The main difference is that middlewares have uniform api and actor lifecycle events and reminder messages are delivered as messages (Activate/Deactivate/Reminder) instead of being method callbacks as in old api.

/// BEFORE

public interface IActorInvoker
{
    Task<object> OnReceive(Actor actor, object message);
    Task OnReminder(Actor actor, string id);

    Task OnActivate(Actor actor);
    Task OnDeactivate(Actor actor);
}

public class TestActorInterceptionInvoker : ActorInvoker
{
    public override Task<object> OnReceive(Actor actor, object message)
    {
        if (message is Foo)
        {
            Log.Error($"Intercepted poison message destined to {actor.GetType()}:{actor.Id}");
            throw new InvalidOperationException("foo is not allowed");
        }

        return base.OnReceive(actor, message);
    }

    public override async Task OnActivate(Actor actor)
    {        
        await base.OnActivate(actor);
        Log.Info($"{actor.GetType()}:{actor.Id} activated");
    }
}
/// AFTER

public interface IActorMiddleware
{
    Task<object> Receive(Actor actor, object message, Receive receiver);
}

public class TestActorMiddleware : ActorMiddleware
{
    public override Task<object> Receive(Actor actor, object message, Receive receiver)
    {
        switch (message)
        {
            case Foo _:
                Log.Error($"Intercepted poison message destined to {actor.GetType()}:{actor.Id}");              
                throw new InvalidOperationException("foo is not allowed");

            case Bar _:
                Log.Warning($"Bailing out silently for Bar message destined to {actor.GetType()}:{actor.Id}");              
                return null;

            case Activate _:
                Log.Info($"{actor.GetType()}:{actor.Id} activated");
                break;
        }

        return Next.Receive(actor, message, receiver);
    }
}

Also, you will need to fix the middleware registration by using ActorRefMiddleware(m)/ActorMiddleware(m) methods on the ClusterConfigurator. For type-specific middlewares use respective method overloads.

MIGRATION NOTES ON THE JUMP TO 2.4.6

This is the first 2.x release of Orleankka you can jump to after you've completed migration through all of the transitional releases. It will demand Orleans 2.4.4 since it will contain the important patch for TypeCodeOverride bug. This version of Orleankka will feature an additional package containing some of 1.x api and features to further smooth your migration.

1st thing after upgrade

  • Install Orleankka.Legacy.Runtime NuGet package to projects that have references to Actor class or do configure silo (ie silo host).

Fixing broken actor code

  • Interface: IActor -> IActorGrain
  • Implementation: import using Orleankka.Legacy.Runtime; namespace where Actor class is defined
  • TimerService.Register(..)/Unregister(..) now requires timer id. In 1.x there was an overload which used the callback function name as id, if not specified

Fixing broken test code

There were multiple, but minor api changes to TestKit in 2.x compared to 1.x. Some into which I bumped after upgrade (there could be others):

  • in 2.x message-based timers were added, so in order to unit test callback-based timers, you need to cast returned RecordedTimer to RecordedCallbackTimer by using .CallbackTimer() method. Then you can get function pointer via Callback() property as it was previously

Fixing broken configuration code

Orleankka 2.x does not wrap Orleans configuration builder anymore, but exists merely as an extension. So all setup is done via native Orleans configuration api, using either SiloHostBuilder or ClientBuilder respectively.

Server (silo host)

In Orleankka 1.x configuration was done via extension methods on the ActorSystemConfigurator object. It then returned an instance of ClusterActorSystem which could be used to start, stop or dispose the silo host.

/// BEFORE

var system = ActorSystem.Configure()
    .Cluster()
    .Builder(b => b
        .Configure<ClusterOptions>(options =>
        {
            options.ClusterId = "test";
            options.ServiceId = "test";
        })
        .UseLocalhostClustering()
        .AddSimpleMessageStreamProvider("sms")
        .AddMemoryGrainStorage("PubSubStore"))
    .Assemblies(
        typeof(Join).Assembly, 
        Assembly.GetExecutingAssembly())
    .Done();

system.Start().Wait();

In Orleankka 2.x configuration is done solely by using extension methods on the SiloHostBuilder class. Assemblies and services are registered using this builder, and all of the configuration options as well. The only twist is to enable Orleankka you need to call UseOrleankka(Action<ClusterOptions> configure) extension method at the very end of the setup (this is required so we can grab previously registered assemblies with actors). All further configuration which is particular to Orleankka is available via ClusterOptions object passed as the parameter to configuration callback.

/// AFTER

var host = new SiloHostBuilder()
    .Configure<ClusterOptions>(options =>
    {
        options.ClusterId = "test";
        options.ServiceId = "test";
    })
    .UseLocalhostClustering()
    .AddSimpleMessageStreamProvider("sms")
    .AddMemoryGrainStorage("PubSubStore")    
    .ConfigureApplicationParts(x => x
        .AddApplicationPart(Assembly.GetExecutingAssembly())
        .AddApplicationPart(typeof(IMyActor).Assembly)
        .AddApplicationPart(typeof(MemoryGrainStorage).Assembly)
        .WithCodeGeneration()) // <--- important
    .UseOrleankka(x => x       // <--- enable Orleankka
        .ActorMiddleware(new LoggingMiddleware()))
    .Build();

await host.StartAsync();

Pay attention to the line right just after all assemblies are added. The .WithCodeGeneration()) will turn on a run-time proxy and serializer code generation, required for Orleankka to work properly.

To enable support for legacy features such as declarative stream subscriptions and old actor behavior apis, add another extension method right after the one which originally enables Orleankka. Then configure the client in the same way.

    .UseOrleankka(x => x 
        .ActorMiddleware(new LoggingMiddleware()))
    .UseOrleankkaLegacyFeatures(x => x               // <--- enable Orleankka legacy features
        .AddSimpleMessageStreamProvider("sms")       // <--- hook declarative stream subscriptions
        .RegisterPersistentStreamProviders("aqp"));  // <--- hook declarative stream subscriptions
    .Build();
  • If you've been configuring Orleans with xml-based ClientConfiguration/ClusterConfiguration (cmon, it's impossible :) you can still do it with AddLegacyClusterConfigurationSupport() extension method. But it's better to invest a fraction of a time in replacing it with programmatic configuration
  • UseSimpleMessageStreamProvider renamed to AddSimpleMessageStreamProvider method you can find on LegacyOrleankkaClusterOptions class

WARNING: Don't forget to change keys used to specify type-specific grain collection options to be a full path of a grain class (as per Orleans docs). Warned in this release.

Client

The client configuration is similarly done with the help of native ClientBuilder and available extension methods.

/// BEFORE

var system = ActorSystem.Configure()
    .Client()
    .Builder(b => b.Configure<ClusterOptions>(options =>
        {
            options.ClusterId = "test";
            options.ServiceId = "test";
        })
        .UseLocalhostClustering()
        .AddSimpleMessageStreamProvider("sms"))
    .Assemblies(typeof(IMyActor).Assembly)
    .Done();

await system.Connect(retries: 5);  // <-- this piece of cheese is moved

As with silo configuration, add UseOrleankka() and UseOrleankkaLegacyFeatures() at the end of setup. However, since Orleankka doesn't wrap Orleans anymore you will need to re-implement Connect() routine yourself. The following implementation is what suggested by Orleans team (you can find it in samples):

/// AFTER

public static async Task Main()
{
  var system = await Connect(retries: 2);
  ...
}

static async Task<IClientActorSystem> Connect(int retries = 0, TimeSpan? retryTimeout = null)
{
    if (retryTimeout == null)
        retryTimeout = TimeSpan.FromSeconds(5);

    while (true)
    {
        try
        {
            var client = new ClientBuilder()
                .Configure<ClusterOptions>(options =>
                {
                    options.ClusterId = DemoClusterId;
                    options.ServiceId = DemoServiceId;
                })
                .UseLocalhostClustering()
                .ConfigureApplicationParts(x => x
                    .AddApplicationPart(typeof(IChatUser).Assembly)
                    .WithCodeGeneration())
                .UseOrleankka()                             
                .UseOrleankkaLegacyFeatures(o => o
                    .AddSimpleMessageStreamProvider("sms"))
                .Build();

            await client.Connect();
            return client.ActorSystem();  // <- extension method to get actor system 
        }
        catch (Exception ex)
        {
            if (retries-- == 0)
            {
                Console.WriteLine("Can't connect to cluster. Max retries reached.");
                throw;
            }

            Console.WriteLine(
                $"Can't connect to cluster: '{ex.Message}'." +
                $"Trying again in {(int)retryTimeout.Value.TotalSeconds} seconds ...");

            await Task.Delay(retryTimeout.Value);
        }
    }
}

Check out how the reference to IClientActorSystem is returned by using an extension method on ClientBuilder which is just a shortcut for services.GetService<IClientActorSystem>.

Embedded

For embedded setup, when both client and silo live in the same process - use direct client support from Orleans. Basically, you simply configure the silo using SiloHostBuilder, start it and then you can get a reference to IClientActorSystem by using the same extension method as is available for the client - silo.ActorSystem().

Good luck and see you in 2020 😎

⚠️ **GitHub.com Fallback** ⚠️