Skip to content

Commit bb02de1

Browse files
CopilotstephentoubCopilot
authored
[C#] Use event delegate for thread-safe, insertion-ordered event handler dispatch (#624)
* Initial plan * Use event delegate composition for thread-safe, insertion-ordered handler dispatch Replace HashSet<SessionEventHandler> with a private event (multicast delegate). The compiler-generated add/remove accessors use a lock-free CAS loop, dispatch reads the field once for an inherent snapshot, and invocation order matches registration order. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Stephen Toub <stoub@microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 7cc50b6 commit bb02de1

File tree

1 file changed

+12
-9
lines changed

1 file changed

+12
-9
lines changed

dotnet/src/Session.cs

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,13 @@ namespace GitHub.Copilot.SDK;
4444
/// </example>
4545
public partial class CopilotSession : IAsyncDisposable
4646
{
47-
private readonly HashSet<SessionEventHandler> _eventHandlers = new();
47+
/// <summary>
48+
/// Multicast delegate used as a thread-safe, insertion-ordered handler list.
49+
/// The compiler-generated add/remove accessors use a lock-free CAS loop over the backing field.
50+
/// Dispatch reads the field once (inherent snapshot, no allocation).
51+
/// Expected handler count is small (typically 1–3), so Delegate.Combine/Remove cost is negligible.
52+
/// </summary>
53+
private event SessionEventHandler? _eventHandlers;
4854
private readonly Dictionary<string, AIFunction> _toolHandlers = new();
4955
private readonly JsonRpc _rpc;
5056
private volatile PermissionRequestHandler? _permissionHandler;
@@ -243,8 +249,8 @@ void Handler(SessionEvent evt)
243249
/// </example>
244250
public IDisposable On(SessionEventHandler handler)
245251
{
246-
_eventHandlers.Add(handler);
247-
return new ActionDisposable(() => _eventHandlers.Remove(handler));
252+
_eventHandlers += handler;
253+
return new ActionDisposable(() => _eventHandlers -= handler);
248254
}
249255

250256
/// <summary>
@@ -256,11 +262,8 @@ public IDisposable On(SessionEventHandler handler)
256262
/// </remarks>
257263
internal void DispatchEvent(SessionEvent sessionEvent)
258264
{
259-
foreach (var handler in _eventHandlers.ToArray())
260-
{
261-
// We allow handler exceptions to propagate so they are not lost
262-
handler(sessionEvent);
263-
}
265+
// Reading the field once gives us a snapshot; delegates are immutable.
266+
_eventHandlers?.Invoke(sessionEvent);
264267
}
265268

266269
/// <summary>
@@ -550,7 +553,7 @@ await InvokeRpcAsync<object>(
550553
// Connection is broken or closed
551554
}
552555

553-
_eventHandlers.Clear();
556+
_eventHandlers = null;
554557
_toolHandlers.Clear();
555558

556559
_permissionHandler = null;

0 commit comments

Comments
 (0)