开发粒度

在编写代码以实现粒度类之前,请创建面向 .NET Standard 或 .NET Core(首选)或 .NET Framework 4.6.1 或更高版本的新类库项目(如果因依赖项而无法使用 .NET Standard 或 .NET Core)。 可以在同一类库项目或两个不同的项目中定义粒度接口和粒度类,以便更好地将接口与实现分离。 在任何情况下,项目都需要引用 Microsoft.Orleans.Sdk NuGet 包。

有关更全面的说明,请参阅教程一 - Orleans 基础知识“项目设置”部分。

粒度接口和类

粒子相互交互,并通过调用声明为各自粒子接口的一部分的方法来进行外部调用。 粒度类实现一个或多个以前声明的粒度接口。 粒度接口的所有方法都必须返回一个 Task (对于 void 方法)、一个 Task<TResult>或一个 ValueTask<TResult> (对于返回类型的 T值的方法)。

下面是状态服务示例的摘录 Orleans :

public interface IPlayerGrain : IGrainWithGuidKey
{
    Task<IGameGrain> GetCurrentGame();

    Task JoinGame(IGameGrain game);

    Task LeaveGame(IGameGrain game);
}

public class PlayerGrain : Grain, IPlayerGrain
{
    private IGameGrain _currentGame;

    // Game the player is currently in. May be null.
    public Task<IGameGrain> GetCurrentGame()
    {
       return Task.FromResult(_currentGame);
    }

    // Game grain calls this method to notify that the player has joined the game.
    public Task JoinGame(IGameGrain game)
    {
       _currentGame = game;

       Console.WriteLine(
           $"Player {GetPrimaryKey()} joined game {game.GetPrimaryKey()}");

       return Task.CompletedTask;
    }

   // Game grain calls this method to notify that the player has left the game.
   public Task LeaveGame(IGameGrain game)
   {
       _currentGame = null;

       Console.WriteLine(
           $"Player {GetPrimaryKey()} left game {game.GetPrimaryKey()}");

       return Task.CompletedTask;
   }
}

粒度方法的响应超时

Orleans 运行时允许对每个粒度方法强制设置响应超时。 如果某个方法未能在超时内完成,运行时将抛出一个 TimeoutException。 若要施加响应超时,请在接口的 grain 方法定义中添加 ResponseTimeoutAttribute。 必须将属性添加到接口中的方法定义,而不是粒类中的方法实现,因为客户端和服务宿主都需要意识到超时。

扩展之前的 PlayerGrain 实现,以下示例演示如何对 LeaveGame 方法施加响应超时:

public interface IPlayerGrain : IGrainWithGuidKey
{
    Task<IGameGrain> GetCurrentGame();

    Task JoinGame(IGameGrain game);

    [ResponseTimeout("00:00:05")] // 5s timeout
    Task LeaveGame(IGameGrain game);
}

前面的代码设置方法的 LeaveGame 响应超时为 5 秒。 离开游戏时,如果耗时超过 5 秒,则抛出一个 TimeoutException

配置响应超时

与单个粒度方法响应超时类似,可以为所有粒度方法配置默认响应超时。 如果在指定时间段内未收到响应,对 Grain 方法的调用将会超时。 默认情况下,此时间段为 30 秒。 可以配置默认响应超时:

有关配置 Orleans的详细信息,请参阅 客户端配置服务器配置

从粒度方法返回值

定义一个粒度方法,该方法返回粒度接口中类型的 T 值作为返回值 Task<T>。 对于未使用 async 关键字标记的粒度方法,当返回值可用时,通常使用以下语句返回它:

public Task<SomeType> GrainMethod1()
{
    return Task.FromResult(GetSomeType());
}

定义一个在 Grain 接口中不返回值(实际上为 void 方法)的 Grain 方法,并使其返回 Task。 返回 Task 的指示方法的异步执行和完成。 对于未使用 async 关键字标记的粒度方法,当“void”方法完成其执行时,它需要返回特殊值 Task.CompletedTask

public Task GrainMethod2()
{
    return Task.CompletedTask;
}

被标记为 async 的粒度方法直接返回值:

public async Task<SomeType> GrainMethod3()
{
    return await GetSomeTypeAsync();
}

void标记为async不返回任何值的粒度方法只会在其执行结束时返回:

public async Task GrainMethod4()
{
    return;
}

如果一个粒度方法从另一个异步方法调用(无论是否到粒度)接收到返回值,并且不需要对此调用进行错误处理,它可以简单地返回从该异步调用中接收到的Task

public Task<SomeType> GrainMethod5()
{
    Task<SomeType> task = CallToAnotherGrain();

    return task;
}

同样,void 颗粒方法可以返回另一次Task 调用传递给它的值,而不是等待该值。

public Task GrainMethod6()
{
    Task task = CallToAsyncAPI();
    return task;
}

ValueTask<T> 可以代替 Task<T> 使用。

谷物引用

粒度引用是实现与相应粒度类相同的粒度接口的代理对象。 它封装目标粒度的逻辑标识(类型和唯一键)。 使用粒度引用来调用目标粒度。 每个粒度引用指向单个粒度(粒度类的单个实例),但你可以创建对同一粒度的多个独立引用。

由于粒度引用表示目标粒度的逻辑标识,因此它独立于粒度的物理位置,即使在完全系统重启后仍有效。 可以像使用任何其他 .NET 对象一样使用粒度引用。 你可以将其传递给方法,将其用作方法返回值等,甚至将其保存到永久性存储。

可以通过将粒度的标识传递给 IGrainFactory.GetGrain<TGrainInterface>(Type, Guid) 方法来获取粒度引用,其中 T 粒度接口是粒度接口,并且 key 是其类型内粒度的唯一键。

以下示例演示如何获取前面定义的接口的 IPlayerGrain 粒度引用。

从粒度类内部:

IPlayerGrain player = GrainFactory.GetGrain<IPlayerGrain>(playerId);

从 Orleans 客户端代码。

IPlayerGrain player = client.GetGrain<IPlayerGrain>(playerId);

有关粒度引用的详细信息,请参阅 粒度参考文章

粒度方法调用

Orleans编程模型基于异步编程。 使用上一示例中的粒度引用,下面介绍了如何执行粒度方法调用:

// Invoking a grain method asynchronously
Task joinGameTask = player.JoinGame(this);

// The await keyword effectively makes the remainder of the
// method execute asynchronously at a later point
// (upon completion of the Task being awaited) without blocking the thread.
await joinGameTask;

// The next line will execute later, after joinGameTask has completed.
players.Add(playerId);

可以加入两个或多个Tasks。 联接操作将创建一个新的 Task,当其所有组成部分 Tasks 完成时,该新操作将完成。 当一个粒度需要启动多个计算并等待所有这些计算完成,然后继续作时,此模式非常有用。 例如,前端模块生成由多个部件组成的网页时,可能会对后端进行多次调用(每个部件对应一次),并接收每个调用结果的Task。 然后,谷物将等待所有这些联接Tasks。 组合 Task 解析完成后,单个 Tasks 都已完成,并且已收到格式化网页所需的所有数据。

示例:

List<Task> tasks = new List<Task>();
Message notification = CreateNewMessage(text);

foreach (ISubscriber subscriber in subscribers)
{
    tasks.Add(subscriber.Notify(notification));
}

// WhenAll joins a collection of tasks, and returns a joined
// Task that will be resolved when all of the individual notification Tasks are resolved.
Task joinedTask = Task.WhenAll(tasks);

await joinedTask;

// Execution of the rest of the method will continue
// asynchronously after joinedTask is resolve.

错误传播

当一个粒度方法抛出异常时,Orleans 会根据需要在不同的主机间传播该异常,并沿着调用堆栈向上传递。 若要按预期工作,异常必须使用Orleans序列化,并且处理异常的主机必须可用该异常类型。 如果异常类型不可用, Orleans 则引发异常作为实例 Orleans.Serialization.UnavailableExceptionFallbackException,保留原始异常的消息、类型和堆栈跟踪。

从粒度方法引发的异常不会使粒度被停用,除非异常继承自 Orleans.Storage.InconsistentStateException。 当存储操作发现粒度的内存状态与数据库中的状态不一致时,会引发 InconsistentStateException。 除了特殊处理 InconsistentStateException之外,此行为类似于从任何 .NET 对象引发异常:异常不会导致对象被销毁。

虚拟方法

粒度类可以选择重写 OnActivateAsyncOnDeactivateAsync 虚拟方法。 运行时 Orleans 在激活和停用该类的每个实例时调用这些方法。 这样,粒度代码就有机会执行其他初始化和清理作。 因为 OnActivateAsync 抛出的异常导致激活过程失败。

虽然 OnActivateAsync 如果被重写,始终被调用作为粒度激活过程的一部分,但不能保证 OnDeactivateAsync 在所有情况下调用,例如在服务器故障或其他异常事件的情况下。 因此,您的应用程序不应依赖于 OnDeactivateAsync 执行关键操作,例如持久化状态更改。 仅将其用于尽力而为的操作。

另请参阅