Skip to content

Commit ee1891d

Browse files
committed
Add grain architecture principles to AI context
1 parent 0a6a3c7 commit ee1891d

3 files changed

Lines changed: 29 additions & 0 deletions

File tree

.github/copilot-instructions.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,10 @@ Include in every request:
5252
- Use tracked EF deletes when atomicity with inserts is required.
5353
- Replace `.Ignore()` with a `LogAndForget` helper that logs faulted tasks.
5454
- Cap in-memory per-event collections (message history, queues).
55+
- One grain per responsibility — isolate heavy I/O into secondary grains (e.g. `RoomPersistenceGrain`).
56+
- Use grain single-threading for concurrency safety (per-player `PurchaseGrain`, per-item `LimitedItemGrain`). No manual locks.
57+
- Grains orchestrate their own outbound communication via `PlayerPresenceGrain.SendComposerAsync`. Callers do not send composers.
58+
- All mutations to grain-owned data go through grain methods. No direct DB updates for grain-owned state.
5559

5660
## Task routing hints
5761
- Handler task: use neighboring handler + `Turbo.Primitives/Orleans/GrainFactoryExtensions.cs`.

AGENTS.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,27 @@ Still fire-and-forget, but failures are visible in production logs.
116116
Any in-memory collection that grows per-message (e.g. conversation history) must have a configurable cap.
117117
Without a cap, long-running sessions leak memory.
118118

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.
132+
- **Correct**: handler calls `grain.UpdateWalletAsync(...)` → grain updates state → grain calls `PlayerPresenceGrain.SendComposerAsync(...)`.
133+
- **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.
139+
119140
## Profile and grain flow constraints
120141
- Keep packet handlers orchestration-only:
121142
- validate input

CONTEXT.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,10 @@
6161
- When a delete + insert must be atomic, use EF tracked operations (`Remove` + `SaveChangesAsync`), not `ExecuteDeleteAsync`.
6262
- Replace `.Ignore()` on grain tasks with a `LogAndForget` helper that logs faulted continuations.
6363
- 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.
6468

6569
## Placement rules
6670
- New host startup/wiring behavior:

0 commit comments

Comments
 (0)