Migration from 1.5 to 2.0 - OrleansContrib/Orleankka GitHub Wiki
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.
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
).
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>)
andBuilder(Action<IClientBuilder>)
methods onClusterActorSystem
andClientActorSystem
respectively. All of the latest configuration extensions in Orleans are done against those interfaces. For example OrleansDashboard registration is done viaISiloHostBuilder
extension method. -
force
argument was removed fromClusterActorSystem.Stop()
- If you were using
ClusterConfiguration.AddMemoryStorageProvider("MemoryStore")
extension, in Orleans 2.0 it is an extension toISiloHostBuilder
. In this versionUseInMemoryGrainStore
extension was added toClusterActorSystem
to replicate this functionality - If you were using Azure's blob storage provider you can find
AddAzureBlobGrainStorage
as extension toIServiceCollection
. Same forUseAzureTableReminderService
- In Orleans 2.0 logging is done via
Microsoft.Extensions.Logging
so referenceMicrosoft.Extensions.Logging.Console
package and useAddLogging
extension method toIServicesCollection
- 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
andIClientBuilder
respectively. Then register names of the providers in cluster actor system by usingRegisterPersistentStreamProviders("name1", "name2")
method. See example for azure queue stream provider here
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) :)
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
.
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.
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.
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.
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.
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!
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;
}
[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.
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!
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);
}
}
}
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.
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
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>()
.
[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).
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());
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.
- You should register it as a singleton in the service collection, same as with
ActorActivator
- 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.
StatefulActor
is no longer supported. We advise migrating away from stock storage providers to your own before migrating to this version.
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.
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.
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.
- Install
Orleankka.Legacy.Runtime
NuGet package to projects that have references toActor
class or do configure silo (ie silo host).
-
Interface:
IActor
->IActorGrain
-
Implementation: import
using Orleankka.Legacy.Runtime;
namespace whereActor
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
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
toRecordedCallbackTimer
by using.CallbackTimer()
method. Then you can get function pointer viaCallback()
property as it was previously
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.
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 withAddLegacyClusterConfigurationSupport()
extension method. But it's better to invest a fraction of a time in replacing it with programmatic configuration -
UseSimpleMessageStreamProvider
renamed toAddSimpleMessageStreamProvider
method you can find onLegacyOrleankkaClusterOptions
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.
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>
.
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 😎