You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
- Enforce: player outbound via `PlayerPresenceGrain.SendComposerAsync`; no direct handler socket sends.
@@ -64,13 +65,78 @@ Default output format:
64
65
- Prefer deterministic handlers/services with clear guard clauses.
65
66
- Preserve cancellation and async flow where it already exists.
66
67
- Handle failure paths explicitly; do not ship happy-path-only changes.
67
-
- Avoid dead code, unused allocations, and broad catch blocks that hide errors.
68
+
- Avoid dead code, unused allocations, and broad catch blocks that hide errors (see **Orleans grain development rules** for specifics).
68
69
- For revision compatibility work, prefer restoring/adding missing incoming message contracts in `Turbo.Primitives/Messages/Incoming/**` before mutating serializer/composer payload behavior.
69
70
- Do not alter serializer/composer behavior by replacing real payload writes with placeholder constants (for example, unconditional `WriteInteger(0)`) unless explicitly requested.
70
71
- If work references `Revision<id>` parsers/serializers, edit the plugin repo path:
If a grain method calls its own player's `GetSummaryAsync` inside a loop, hoist the call before the loop.
92
+
Same result every iteration = wasted round-trips.
93
+
94
+
### Batch DB operations
95
+
Do not loop `ExecuteDeleteAsync` per entity. Use a single `WHERE ... IN (...)` query.
96
+
Same for composer fan-out: collect all updates, send once.
97
+
98
+
### Use timer-based flush for housekeeping writes
99
+
Follow the `RoomPersistenceGrain` pattern: queue dirty state, flush with `RegisterGrainTimer` on interval, and flush on `OnDeactivateAsync`.
100
+
Do not issue per-event DB writes that block the grain turn.
101
+
102
+
### Do not hardcode limits in grains
103
+
Handlers already read configuration values (e.g. `Turbo:FriendList:UserFriendLimit`) from `IConfiguration` and pass them to grains.
104
+
Magic numbers like `Take(50)`, `Take(20)`, or `maxIgnoreCapacity = 100` must come from configuration parameters on the grain interface method.
105
+
A 10,000 user hotel needs different tuning than a 10 player dev server.
106
+
107
+
### Use tracked deletes for atomicity
108
+
`ExecuteDeleteAsync` commits immediately and bypasses the EF change tracker.
109
+
If a delete + insert must succeed or fail together, use `FirstOrDefaultAsync` + `Remove` so both go through one `SaveChangesAsync`.
110
+
111
+
### Replace .Ignore() with a LogAndForget helper
112
+
Orleans `.Ignore()` makes cross-grain failures invisible. Use a `LogAndForget` extension that calls `ContinueWith(OnlyOnFaulted)` to log the exception.
113
+
Still fire-and-forget, but failures are visible in production logs.
114
+
115
+
### Bound session/history collections
116
+
Any in-memory collection that grows per-message (e.g. conversation history) must have a configurable cap.
117
+
Without a cap, long-running sessions leak memory.
118
+
119
+
### One grain per responsibility — isolate heavy I/O
120
+
Each major domain component should operate in its own grain. When a grain needs heavy I/O (DB writes, persistence flushes), delegate that work to a dedicated secondary grain so it does not block the primary grain's turn.
121
+
- Example: `RoomGrain` delegates furniture saves to `RoomPersistenceGrain`. The room grain stays responsive while persistence queues and flushes.
122
+
- Do not combine domain logic and persistence flushing in the same grain.
123
+
124
+
### Use grain boundaries for thread safety
125
+
Orleans grains are single-threaded by design. Use this for concurrency-sensitive operations by giving each user their own grain for the operation.
126
+
- Example: each player gets a `PurchaseGrain` so catalog purchases are serialized per-player with no locks needed.
127
+
- Example: limited-edition items should use a dedicated grain (e.g. `LimitedItemGrain`) so concurrent buyers are safely serialized.
128
+
- Do not add manual locking (`lock`, `SemaphoreSlim`) inside grains — that fights the actor model.
129
+
130
+
### Grains orchestrate their own outbound communication
131
+
When grain state changes (e.g. wallet balance updates), the grain itself sends the snapshot to `PlayerPresenceGrain.SendComposerAsync`. The caller that triggered the change does not pass or send the composer — the grain owns that responsibility.
-**Wrong**: handler calls `grain.UpdateWalletAsync(...)` → handler builds composer → handler sends composer to player.
134
+
135
+
### Do not mutate the database directly for grain-owned state
136
+
Grains may hold cached or in-memory state that will not reflect direct DB changes. All mutations to grain-owned data must go through the grain's methods, even when the player is offline.
137
+
- If a grain uses `[PersistentState]`, state is hydrated from the configured store (not DB) on activation. Direct DB edits will be overwritten by stale store data.
138
+
- Admin tools and external systems must call grain methods, not issue raw SQL/DB updates, for data that grains own.
Copy file name to clipboardExpand all lines: CONTEXT.md
+15Lines changed: 15 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -51,6 +51,21 @@
51
51
- Lifecycle:
52
52
- inactive grains are Orleans-managed and can deactivate automatically unless explicitly marked `[KeepAlive]`.
53
53
54
+
## Grain runtime patterns
55
+
- Every grain that does cross-grain calls or DB work must inject `ILogger<T>` and log caught exceptions. No bare `catch { }`.
56
+
- Independent grain calls (e.g. checking online status for N friends) must use `Task.WhenAll`, not sequential `await` in a loop.
57
+
- Identical grain calls must not repeat inside loops — hoist before the loop.
58
+
- DB batch operations use single `WHERE ... IN (...)` queries, not per-entity `ExecuteDeleteAsync` loops.
59
+
- Housekeeping writes (e.g. delivered flags) follow the timer-flush pattern: queue dirty state, flush with `RegisterGrainTimer`, flush on `OnDeactivateAsync`. See `RoomPersistenceGrain` for reference.
60
+
- Do not hardcode limits (`Take(N)`, capacity constants) in grains. Pass them from handlers via `IConfiguration`.
61
+
- When a delete + insert must be atomic, use EF tracked operations (`Remove` + `SaveChangesAsync`), not `ExecuteDeleteAsync`.
62
+
- Replace `.Ignore()` on grain tasks with a `LogAndForget` helper that logs faulted continuations.
63
+
- In-memory collections that grow per-event (message history, queues) must have a configurable cap.
64
+
- One grain per responsibility: isolate heavy I/O (DB writes, persistence) into secondary grains so the primary domain grain stays responsive (e.g. `RoomGrain` → `RoomPersistenceGrain`).
65
+
- Use grain single-threading for concurrency safety: per-player `PurchaseGrain` for catalog buys, dedicated grains for limited-edition items. Do not add manual locks inside grains.
66
+
- Grains orchestrate their own outbound: when state changes, the grain sends the composer via `PlayerPresenceGrain.SendComposerAsync`. Callers do not build or send composers after calling a grain method.
67
+
- All mutations to grain-owned data must go through grain methods. Do not update the database directly — grains may hold cached state that will not reflect raw DB changes.
68
+
54
69
## Placement rules
55
70
- New host startup/wiring behavior:
56
71
-`Turbo.Main/` (usually `Program.cs`, `Extensions/`, or `Console/`)
0 commit comments