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)
| Grain | Key | Description |
|---|---|---|
AccountGrain | Guid (userId) | Global account state, character list |
CharacterGrain | Guid + string (charId + seasonId) | Per-season character state |
RefreshTokenGrain | Guid (userId) | JWT refresh token management |
PlayerPresenceGrain | Guid (userId) | Online status tracking |
SessionLogGrain | Guid (userId) | Session persistence |
UserIdentityGrain | Guid (userId) | Provider identity mapping |
UserProfileGrain | Guid (userId) | User profile data |
SocialGrain | Guid (userId) | Friends, blocks |
Inventory Host (Titan.InventoryHost)
| Grain | Key | Description |
|---|---|---|
CharacterInventoryGrain | Guid + string | Character bag and equipment |
AccountStashGrain | Guid + string | Shared stash tabs |
ItemGeneratorGrain | long (stateless) | Item generation worker |
BaseTypeRegistryGrain | string ("default") | Base type definitions |
ModifierRegistryGrain | string ("default") | Modifier definitions |
ItemHistoryGrain | Guid (itemId) | Item audit trail |
Trading Host (Titan.TradingHost)
| Grain | Key | Description |
|---|---|---|
TradeGrain | Guid (tradeId) | Trade session state |
SeasonRegistryGrain | string ("default") | Season definitions |
SeasonMigrationGrain | string (seasonId) | Character migration |
RateLimitConfigGrain | string ("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
- Identity Grains - Account, Character, Session
- Item Grains - Inventory, Generation, Registries
- Trading Grains - Trade sessions, Rules
- Season Grains - Seasons, Migration