Skip to main content

Orleans Grains Overview

Titan uses Microsoft Orleans virtual actors (grains) to manage distributed state.

Virtual Actor Model​

Orleans grains are:

  • Single-threaded: One request at a time per grain instance
  • Location-transparent: Orleans routes calls to the correct silo
  • Persistent: State is automatically saved to storage
  • Lazy-activated: Created on first access, deactivated when idle

Grain Types by Host​

Identity Host (Titan.IdentityHost)​

GrainKeyDescription
AccountGrainGuid (userId)Global account state, character list
CharacterGrainGuid + string (charId + seasonId)Per-season character state
RefreshTokenGrainGuid (userId)JWT refresh token management
PlayerPresenceGrainGuid (userId)Online status tracking
SessionLogGrainGuid (userId)Session persistence
UserIdentityGrainGuid (userId)Provider identity mapping
UserProfileGrainGuid (userId)User profile data
SocialGrainGuid (userId)Friends, blocks

Inventory Host (Titan.InventoryHost)​

GrainKeyDescription
CharacterInventoryGrainGuid + stringCharacter bag and equipment
AccountStashGrainGuid + stringShared stash tabs
ItemGeneratorGrainlong (stateless)Item generation worker
BaseTypeRegistryGrainstring ("default")Base type definitions
ModifierRegistryGrainstring ("default")Modifier definitions
ItemHistoryGrainGuid (itemId)Item audit trail

Trading Host (Titan.TradingHost)​

GrainKeyDescription
TradeGrainGuid (tradeId)Trade session state
SeasonRegistryGrainstring ("default")Season definitions
SeasonMigrationGrainstring (seasonId)Character migration
RateLimitConfigGrainstring ("default")Rate limit configuration

Grain Keys​

Simple Keys​

Most grains use a single GUID:

var accountGrain = clusterClient.GetGrain<IAccountGrain>(userId);
var tradeGrain = clusterClient.GetGrain<ITradeGrain>(tradeId);

Compound Keys​

Character-related grains use compound keys (GUID + string):

// characterId + seasonId
var charGrain = clusterClient.GetGrain<ICharacterGrain>(characterId, seasonId);
var invGrain = clusterClient.GetGrain<ICharacterInventoryGrain>(characterId, seasonId);

This allows the same character ID to have different states in different seasons (e.g., after migration).

String Keys​

Singleton-style grains use string keys:

var registry = clusterClient.GetGrain<IBaseTypeRegistryGrain>("default");
var seasons = clusterClient.GetGrain<ISeasonRegistryGrain>("default");

Persistence​

Grains persist state to PostgreSQL (or CockroachDB) using MemoryPack serialization:

public class AccountGrain : Grain, IAccountGrain
{
private readonly IPersistentState<AccountGrainState> _state;

public AccountGrain(
[PersistentState("account", "OrleansStorage")]
IPersistentState<AccountGrainState> _state)
{
_state = state;
}

public async Task UpdateAsync()
{
_state.State.SomeValue = newValue;
await _state.WriteStateAsync(); // Persist to DB
}
}

State Serialization​

Grain state uses MemoryPack for efficient binary serialization:

[GenerateSerializer]
[MemoryPackable]
public partial class AccountGrainState
{
[Id(0), MemoryPackOrder(0)]
public Account? Account { get; set; }

[Id(1), MemoryPackOrder(1)]
public List<CharacterSummary> Characters { get; set; } = new();
}

Benefits:

  • ~40% smaller than JSON
  • Fast serialization via source generators
  • Compatible with Orleans' streaming

Stateless Workers​

Some grains are stateless workers that can run on any silo:

[StatelessWorker]
public class ItemGeneratorGrain : Grain, IItemGeneratorGrain
{
// No state - can have multiple instances
public async Task<Item> GenerateAsync(string baseTypeId, ItemRarity rarity)
{
// Generate item logic
}
}

Next Steps​