Skip to content

Commit 0a6a3c7

Browse files
committed
Add Orleans grain development rules to AI context
1 parent 0035e94 commit 0a6a3c7

3 files changed

Lines changed: 68 additions & 1 deletion

File tree

.github/copilot-instructions.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,17 @@ Include in every request:
4242
- keep active-room indexing and keepalive behavior in `RoomDirectoryGrain`
4343
- treat `[KeepAlive]` as explicit infrastructure-only usage
4444

45+
## Orleans grain constraints
46+
- Never use bare `catch { }` — always `catch (Exception ex)` with `ILogger<T>`.
47+
- Parallelize independent grain calls with `Task.WhenAll`, not sequential `await` in loops.
48+
- Hoist repeated grain calls out of loops.
49+
- Batch DB deletes with `WHERE ... IN (...)`, not per-entity `ExecuteDeleteAsync`.
50+
- Use timer-flush for housekeeping writes (see `RoomPersistenceGrain`).
51+
- No hardcoded limits in grains — pass from handlers via `IConfiguration`.
52+
- Use tracked EF deletes when atomicity with inserts is required.
53+
- Replace `.Ignore()` with a `LogAndForget` helper that logs faulted tasks.
54+
- Cap in-memory per-event collections (message history, queues).
55+
4556
## Task routing hints
4657
- Handler task: use neighboring handler + `Turbo.Primitives/Orleans/GrainFactoryExtensions.cs`.
4758
- Grain task: use grain interface + snapshot/state types as primary references.

AGENTS.md

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ Activate the relevant skill checklist before editing code in that domain:
2020
- `grain-development`
2121
- Trigger: editing files under `Turbo.*\\Grains\\**` or `Turbo.Primitives/**/Grains/*.cs`.
2222
- Enforce: keep ownership boundaries, lifecycle rules, and snapshot/state coherence.
23+
- Enforce: all rules in the **Orleans grain development rules** section below.
2324
- `session-presence-routing`
2425
- Trigger: touching session gateway, presence flow, room routing, outbound composer fan-out.
2526
- Enforce: player outbound via `PlayerPresenceGrain.SendComposerAsync`; no direct handler socket sends.
@@ -64,13 +65,57 @@ Default output format:
6465
- Prefer deterministic handlers/services with clear guard clauses.
6566
- Preserve cancellation and async flow where it already exists.
6667
- 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).
6869
- For revision compatibility work, prefer restoring/adding missing incoming message contracts in `Turbo.Primitives/Messages/Incoming/**` before mutating serializer/composer payload behavior.
6970
- Do not alter serializer/composer behavior by replacing real payload writes with placeholder constants (for example, unconditional `WriteInteger(0)`) unless explicitly requested.
7071
- If work references `Revision<id>` parsers/serializers, edit the plugin repo path:
7172
- `../turbo-sample-plugin/TurboSamplePlugin/Revision/**`
7273
- Do not hallucinate those trees into `turbo-cloud`.
7374

75+
## Orleans grain development rules
76+
These rules exist because every one of these mistakes has shipped and caused real issues.
77+
78+
### Never swallow exceptions silently
79+
Every bare `catch { }` hides a real bug path. Always use `catch (Exception ex)` and log it.
80+
If a cross-grain notification fails silently, state goes asymmetric and nobody knows why.
81+
- **Required**: inject `ILogger<T>` into every grain that does cross-grain calls or DB work.
82+
- **Forbidden**: bare `catch { }`, `catch (Exception) { }` without logging.
83+
84+
### Parallelize independent grain calls
85+
When checking status on N grains (e.g. online status for a friend list), do not `await` each one in a `foreach`.
86+
Grain calls to different grains can run concurrently with `Task.WhenAll`.
87+
- Sequential = O(n) round-trips. Parallel = O(1) wall time.
88+
- Apply everywhere: activation hydration, search results, batch accept/deny.
89+
90+
### Do not repeat identical grain calls in loops
91+
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+
74119
## Profile and grain flow constraints
75120
- Keep packet handlers orchestration-only:
76121
- validate input

CONTEXT.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,17 @@
5151
- Lifecycle:
5252
- inactive grains are Orleans-managed and can deactivate automatically unless explicitly marked `[KeepAlive]`.
5353

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+
5465
## Placement rules
5566
- New host startup/wiring behavior:
5667
- `Turbo.Main/` (usually `Program.cs`, `Extensions/`, or `Console/`)

0 commit comments

Comments
 (0)