[dotnet] [bidi] Wait until events are dispatched when unsubscribing - #17142

Merged
nvborisenko merged 10 commits into
SeleniumHQ:trunkfrom
nvborisenko:bidi-drain-events-per-method
Feb 26, 2026
Merged

[dotnet] [bidi] Wait until events are dispatched when unsubscribing#17142
nvborisenko merged 10 commits into
SeleniumHQ:trunkfrom
nvborisenko:bidi-drain-events-per-method

Conversation

@nvborisenko

Copy link
Copy Markdown
Member

🔗 Related Issues

Fix#16574

💥 What does this PR do?

Refactors the event dispatching mechanism in the EventDispatcher class to improve thread safety, reliability, and event handler management. The changes introduce a new EventItem structure, enhance handler registration/removal, and ensure that all pending events are processed before unsubscribing handlers. Error handling and concurrency controls have also been improved.

🔄 Types of changes

  • Bug fix (backwards compatible)

CopilotAI review requested due to automatic review settings February 26, 2026 11:23
@qodo-code-review

Copy link
Copy Markdown
Contributor

PR Type

Bug fix


Description

  • Refactors event dispatching to ensure all pending events are processed before unsubscribing handlers

  • Introduces sequence-based draining mechanism to track enqueued and processed events per registration

  • Improves thread safety with proper locking and atomic operations for handler management

  • Enhances error handling with separate try-catch blocks for deserialization and handler invocation

  • Adds handler before remote subscription to prevent race conditions on subscription failure


File Walkthrough

Relevant files
Bug fix
EventDispatcher.cs
Event draining mechanism and thread-safe handler management

dotnet/src/webdriver/BiDi/EventDispatcher.cs

  • Replaced PendingEvent struct with EventItem record containing
    registration reference instead of TypeInfo
  • Refactored EventRegistration class with sequence-based draining
    mechanism using _enqueueSeq and _processedSeq counters
  • Added DrainAsync() method to wait until all pending events for a
    registration are processed before handler removal
  • Introduced thread-safe handler management with AddHandler(),
    RemoveHandler(), and GetHandlersSnapshot() methods
  • Simplified task creation by replacing TaskFactory with direct
    Task.Run() call
  • Enhanced error handling with separate exception handling for
    deserialization and handler invocation
  • Added handler registration before remote subscription with rollback on
    failure
+111/-23

@selenium-ciselenium-ci added the C-dotnet .NET Bindings label Feb 26, 2026
@qodo-code-review

qodo-code-reviewBot commented Feb 26, 2026

Copy link
Copy Markdown
Contributor

PR Compliance Guide 🔍

Below is a summary of compliance checks for this PR:

Security Compliance
🟢
No security concerns identified No security vulnerabilities detected by AI analysis. Human verification advised for critical code.
Ticket Compliance
🟡
🎫 #16574
🟢Prevent race conditions where events are still in-flight / queued when the subscription is
disposed or the connection is closing.
Ensure BiDi log events (e.g., console logs) are reliably delivered to the registered
OnEntryAddedAsync handler without requiring a time-based delay.
Codebase Duplication Compliance
Codebase context is not defined

Follow the guide to enable codebase context checks.

Custom Compliance
🟢
Generic: Comprehensive Audit Trails

Objective: To create a detailed and reliable record of critical system actions for security analysis
and compliance.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Meaningful Naming and Self-Documenting Code

Objective: Ensure all identifiers clearly express their purpose and intent, making code
self-documenting

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Error Handling

Objective: To prevent the leakage of sensitive system information through error messages while
providing sufficient detail for internal debugging.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Security-First Input Validation and Data Handling

Objective: Ensure all data inputs are validated, sanitized, and handled securely to prevent
vulnerabilities

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

🔴
Generic: Robust Error Handling and Edge Case Management

Objective: Ensure comprehensive error handling that provides meaningful context and graceful
degradation

Status:
Silent event drop: The result of _pendingEvents.Writer.TryWrite(...) is ignored, which can silently drop
events (e.g., after Complete() during disposal) without any fallback or logging.

Referred Code
if(_events.TryGetValue(method,outvarregistration)){registration.IncrementEnqueued();_pendingEvents.Writer.TryWrite(newEventItem(jsonUtf8Bytes,bidi,registration));}

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Logging Practices

Objective: To ensure logs are useful for debugging and auditing without exposing sensitive
information like PII, PHI, or cardholder data.

Status:
Exception details logged: Error logs interpolate Exception objects (e.g., $"... {ex}"), which may include
sensitive data from exception messages/stack traces depending on upstream content and
logging sinks.

Referred Code
catch(Exceptionex){if(_logger.IsEnabled(LogEventLevel.Error)){_logger.Error($"Unhandled error processing BiDi event handler: {ex}");}}}}catch(Exceptionex){if(_logger.IsEnabled(LogEventLevel.Error)){_logger.Error($"Unhandled error deserializing BiDi event: {ex}");}

Learn more about managing compliance generic rules or creating your own custom rules

  • Update
Compliance status legend 🟢 - Fully Compliant
🟡 - Partial Compliant
🔴 - Not Compliant
⚪ - Requires Further Human Verification
🏷️ - Compliance label

@qodo-code-review

qodo-code-reviewBot commented Feb 26, 2026

Copy link
Copy Markdown
Contributor

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion Impact
Possible issue
Increment only on successful enqueue
Suggestion Impact:EnqueueEvent was changed to call IncrementEnqueued() only when _pendingEvents.Writer.TryWrite(...) returns true, aligning the enqueue counter with successful channel writes; additionally added a warning log when TryWrite fails.

code diff:

@@ -85,8 +85,17 @@
{
if (_events.TryGetValue(method, out var registration))
{
- registration.IncrementEnqueued();- _pendingEvents.Writer.TryWrite(new EventItem(jsonUtf8Bytes, bidi, registration));+ if (_pendingEvents.Writer.TryWrite(new EventItem(jsonUtf8Bytes, bidi, registration)))+ {+ registration.IncrementEnqueued();+ }+ else+ {+ if (_logger.IsEnabled(LogEventLevel.Warn))+ {+ _logger.Warn($"Failed to enqueue BiDi event with method '{method}' for processing. Event will be ignored.");+ }+ }
}

In EnqueueEvent, only call registration.IncrementEnqueued() if
_pendingEvents.Writer.TryWrite(...) returns true to prevent a potential deadlock
in DrainAsync.

dotnet/src/webdriver/BiDi/EventDispatcher.cs [88-89]

-registration.IncrementEnqueued();-_pendingEvents.Writer.TryWrite(new EventItem(jsonUtf8Bytes, bidi, registration));+if (_pendingEvents.Writer.TryWrite(new EventItem(jsonUtf8Bytes, bidi, registration)))+{+ registration.IncrementEnqueued();+}

[Suggestion processed]

Suggestion importance[1-10]: 9

__

Why: This suggestion fixes a critical bug where DrainAsync could hang indefinitely if TryWrite fails, by correctly synchronizing the enqueue counter with the channel write operation.

High
Prevent race condition during subscription

To prevent a race condition, move the registration.AddHandler(eventHandler) call
to after the _sessionProvider().SubscribeAsync call succeeds in the
SubscribeAsync method.

dotnet/src/webdriver/BiDi/EventDispatcher.cs [51-69]

 public async Task<Subscription> SubscribeAsync<TEventArgs>(string eventName, EventHandler eventHandler, SubscriptionOptions? options, JsonTypeInfo<TEventArgs> jsonTypeInfo, CancellationToken cancellationToken)
where TEventArgs : EventArgs
{
var registration = _events.GetOrAdd(eventName, _ => new EventRegistration(jsonTypeInfo));
- registration.AddHandler(eventHandler);-
try
{
var subscribeResult = await _sessionProvider().SubscribeAsync([eventName], new() { Contexts = options?.Contexts, UserContexts = options?.UserContexts }, cancellationToken).ConfigureAwait(false);
++ registration.AddHandler(eventHandler);
return new Subscription(subscribeResult.Subscription, this, eventHandler);
}
catch
{
- registration.RemoveHandler(eventHandler);+ // No need to remove the handler as it was never added in case of an exception.
throw;
}
}
  • Apply / Chat
Suggestion importance[1-10]: 8

__

Why: The suggestion correctly identifies a race condition where an event handler could process an event before its subscription is confirmed, improving the atomicity and correctness of the subscription logic.

Medium
Propagate cancellation token
Suggestion Impact:Updated the UnsubscribeAsync flow to pass cancellationToken into DrainAsync, and changed DrainAsync to accept and honor a CancellationToken by returning a cancellable task continuation.

code diff:

@@ -75,7 +75,7 @@
await _sessionProvider().UnsubscribeAsync([subscription.SubscriptionId], null, cancellationToken).ConfigureAwait(false);
// Wait until all pending events for this method are dispatched
- await registration.DrainAsync().ConfigureAwait(false);+ await registration.DrainAsync(cancellationToken).ConfigureAwait(false);
registration.RemoveHandler(subscription.EventHandler);
}
@@ -199,7 +199,7 @@
}
}
- public Task DrainAsync()+ public Task DrainAsync(CancellationToken cancellationToken)
{
lock (_drainLock)
{
@@ -218,7 +218,13 @@
return Task.CompletedTask;
}
- return tcs.Task;+ if (!cancellationToken.CanBeCanceled) return tcs.Task;++ return tcs.Task.ContinueWith(+ static _ => { },+ cancellationToken,+ TaskContinuationOptions.None,+ TaskScheduler.Default);
}

Propagate the cancellationToken from UnsubscribeAsync to registration.DrainAsync
to allow the draining operation to be cancelled.

dotnet/src/webdriver/BiDi/EventDispatcher.cs [78]

-await registration.DrainAsync().ConfigureAwait(false);+await registration.DrainAsync(cancellationToken).ConfigureAwait(false);

[Suggestion processed]

Suggestion importance[1-10]: 7

__

Why: The suggestion correctly proposes propagating the cancellationToken to DrainAsync to allow cancellation of the wait, which improves responsiveness and resource management.

Medium
High-level
Consider simplifying the custom synchronization logic

The custom synchronization logic in EventRegistration for DrainAsync is complex
and error-prone. It should be simplified using higher-level .NET concurrency
primitives for better readability and robustness.

Examples:

dotnet/src/webdriver/BiDi/EventDispatcher.cs [154-224]
privatesealedclassEventRegistration(JsonTypeInfotypeInfo){privatelong_enqueueSeq;privatelong_processedSeq;privatereadonlyobject_drainLock=new();privatereadonlyList<EventHandler>_handlers=[];privateList<(longTargetSeq,TaskCompletionSource<bool>Tcs)>?_drainWaiters;publicJsonTypeInfoTypeInfo{get;}=typeInfo;
...(clipped 61lines)

Solution Walkthrough:

Before:

privatesealedclassEventRegistration{privatelong_enqueueSeq;privatelong_processedSeq;privatereadonlyobject_drainLock=new();privateList<(longTargetSeq,TaskCompletionSource<bool>Tcs)>?_drainWaiters;publicvoidIncrementProcessed(){varprocessed=Interlocked.Increment(ref_processedSeq);lock(_drainLock){// ... logic to check waiters and complete their tasks ...}}publicTaskDrainAsync(){lock(_drainLock){vartarget=Volatile.Read(ref_enqueueSeq);if(Volatile.Read(ref_processedSeq)>=target)returnTask.CompletedTask;vartcs=newTaskCompletionSource<bool>(...);_drainWaiters??=[];_drainWaiters.Add((target,tcs));// ... double-check logic ...returntcs.Task;}}}

After:

// Using a SemaphoreSlim to signal completion could simplify the logic.privatesealedclassEventRegistration{privatelong_enqueueSeq;privatelong_processedSeq;privatereadonlySemaphoreSlim_drainSemaphore=new(0);publicvoidIncrementProcessed(){longprocessed=Interlocked.Increment(ref_processedSeq);if(processed==Volatile.Read(ref_enqueueSeq)){// Potentially release waiters_drainSemaphore.Release();}}publicasyncTaskDrainAsync(){vartarget=Volatile.Read(ref_enqueueSeq);if(Volatile.Read(ref_processedSeq)>=target)return;// Wait until the semaphore is released, indicating processing might be complete.await_drainSemaphore.WaitAsync();// Re-check if our target has been met.}}
Suggestion importance[1-10]: 8

__

Why: The suggestion correctly identifies the most complex part of the PR—the custom concurrency logic in EventRegistration—and proposes a valid alternative to improve maintainability and robustness, which is a significant quality improvement.

Medium
  • Update

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Refactors the .NET BiDi EventDispatcher to make event dispatch and handler unsubscription more reliable under concurrency, specifically ensuring pending events are processed before handlers are removed (addressing the race described in #16574).

Changes:

  • Replaces the queued event payload with an EventItem that carries the EventRegistration, and simplifies the background processing loop.
  • Introduces per-event-method draining via enqueue/processed sequence tracking so UnsubscribeAsync can wait for already-enqueued events to finish dispatching.
  • Hardens handler management with locked add/remove and snapshotting, and improves error isolation between deserialization vs handler invocation.

Comment threaddotnet/src/webdriver/BiDi/EventDispatcher.cs Outdated
Comment threaddotnet/src/webdriver/BiDi/EventDispatcher.cs Outdated
Comment threaddotnet/src/webdriver/BiDi/EventDispatcher.cs Outdated
CopilotAI review requested due to automatic review settings February 26, 2026 11:57

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 3 comments.

Comment threaddotnet/src/webdriver/BiDi/EventDispatcher.cs
Comment threaddotnet/src/webdriver/BiDi/EventDispatcher.cs Outdated
Comment threaddotnet/src/webdriver/BiDi/EventDispatcher.cs Outdated
@nvborisenko

Copy link
Copy Markdown
MemberAuthor

Seems overcomplicated, but it resolves simple behaviour:

awaitsubscription.DisposeAsync();// waiting until events in the queue are dispatched

Can be simplified if w3c/webdriver-bidi#1032 is resolved via subscriptionId in all events.

@nvborisenko
nvborisenko merged commit 2f71272 into SeleniumHQ:trunkFeb 26, 2026
18 of 19 checks passed
@nvborisenko
nvborisenko deleted the bidi-drain-events-per-method branch February 26, 2026 15:41
nvborisenko added a commit to nvborisenko/selenium-hq that referenced this pull request Mar 5, 2026
nvborisenko added a commit that referenced this pull request Mar 5, 2026
Revert "[dotnet] [bidi] Wait until events are dispatched when unsubscribing (#17142)"
This reverts commit 2f71272.
AutomatedTester pushed a commit that referenced this pull request Mar 11, 2026
Revert "[dotnet] [bidi] Wait until events are dispatched when unsubscribing (#17142)"
This reverts commit 2f71272.
krishnamohan-kothapalli pushed a commit to krishnamohan-kothapalli/selenium that referenced this pull request Mar 18, 2026
…Q#17178)
Revert "[dotnet] [bidi] Wait until events are dispatched when unsubscribing (SeleniumHQ#17142)"
This reverts commit 2f71272.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[🐛 Bug]: [dotnet] BiDi logs potentially missing with race condition

3 participants

@nvborisenko@selenium-ci
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

[dotnet] [bidi] Wait until events are dispatched when unsubscribing - #17142

Merged
nvborisenko merged 10 commits into
SeleniumHQ:trunkfrom
nvborisenko:bidi-drain-events-per-method
Feb 26, 2026
Merged

[dotnet] [bidi] Wait until events are dispatched when unsubscribing#17142
nvborisenko merged 10 commits into
SeleniumHQ:trunkfrom
nvborisenko:bidi-drain-events-per-method

Conversation

@nvborisenko

Copy link
Copy Markdown
Member

🔗 Related Issues

Fix#16574

💥 What does this PR do?

Refactors the event dispatching mechanism in the EventDispatcher class to improve thread safety, reliability, and event handler management. The changes introduce a new EventItem structure, enhance handler registration/removal, and ensure that all pending events are processed before unsubscribing handlers. Error handling and concurrency controls have also been improved.

🔄 Types of changes

  • Bug fix (backwards compatible)

CopilotAI review requested due to automatic review settings February 26, 2026 11:23
@qodo-code-review

Copy link
Copy Markdown
Contributor

PR Type

Bug fix


Description

  • Refactors event dispatching to ensure all pending events are processed before unsubscribing handlers

  • Introduces sequence-based draining mechanism to track enqueued and processed events per registration

  • Improves thread safety with proper locking and atomic operations for handler management

  • Enhances error handling with separate try-catch blocks for deserialization and handler invocation

  • Adds handler before remote subscription to prevent race conditions on subscription failure


File Walkthrough

Relevant files
Bug fix
EventDispatcher.cs
Event draining mechanism and thread-safe handler management

dotnet/src/webdriver/BiDi/EventDispatcher.cs

  • Replaced PendingEvent struct with EventItem record containing
    registration reference instead of TypeInfo
  • Refactored EventRegistration class with sequence-based draining
    mechanism using _enqueueSeq and _processedSeq counters
  • Added DrainAsync() method to wait until all pending events for a
    registration are processed before handler removal
  • Introduced thread-safe handler management with AddHandler(),
    RemoveHandler(), and GetHandlersSnapshot() methods
  • Simplified task creation by replacing TaskFactory with direct
    Task.Run() call
  • Enhanced error handling with separate exception handling for
    deserialization and handler invocation
  • Added handler registration before remote subscription with rollback on
    failure
+111/-23

@selenium-ciselenium-ci added the C-dotnet .NET Bindings label Feb 26, 2026
@qodo-code-review

qodo-code-reviewBot commented Feb 26, 2026

Copy link
Copy Markdown
Contributor

PR Compliance Guide 🔍

Below is a summary of compliance checks for this PR:

Security Compliance
🟢
No security concerns identified No security vulnerabilities detected by AI analysis. Human verification advised for critical code.
Ticket Compliance
🟡
🎫 #16574
🟢Prevent race conditions where events are still in-flight / queued when the subscription is
disposed or the connection is closing.
Ensure BiDi log events (e.g., console logs) are reliably delivered to the registered
OnEntryAddedAsync handler without requiring a time-based delay.
Codebase Duplication Compliance
Codebase context is not defined

Follow the guide to enable codebase context checks.

Custom Compliance
🟢
Generic: Comprehensive Audit Trails

Objective: To create a detailed and reliable record of critical system actions for security analysis
and compliance.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Meaningful Naming and Self-Documenting Code

Objective: Ensure all identifiers clearly express their purpose and intent, making code
self-documenting

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Error Handling

Objective: To prevent the leakage of sensitive system information through error messages while
providing sufficient detail for internal debugging.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Security-First Input Validation and Data Handling

Objective: Ensure all data inputs are validated, sanitized, and handled securely to prevent
vulnerabilities

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

🔴
Generic: Robust Error Handling and Edge Case Management

Objective: Ensure comprehensive error handling that provides meaningful context and graceful
degradation

Status:
Silent event drop: The result of _pendingEvents.Writer.TryWrite(...) is ignored, which can silently drop
events (e.g., after Complete() during disposal) without any fallback or logging.

Referred Code
if(_events.TryGetValue(method,outvarregistration)){registration.IncrementEnqueued();_pendingEvents.Writer.TryWrite(newEventItem(jsonUtf8Bytes,bidi,registration));}

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Logging Practices

Objective: To ensure logs are useful for debugging and auditing without exposing sensitive
information like PII, PHI, or cardholder data.

Status:
Exception details logged: Error logs interpolate Exception objects (e.g., $"... {ex}"), which may include
sensitive data from exception messages/stack traces depending on upstream content and
logging sinks.

Referred Code
catch(Exceptionex){if(_logger.IsEnabled(LogEventLevel.Error)){_logger.Error($"Unhandled error processing BiDi event handler: {ex}");}}}}catch(Exceptionex){if(_logger.IsEnabled(LogEventLevel.Error)){_logger.Error($"Unhandled error deserializing BiDi event: {ex}");}

Learn more about managing compliance generic rules or creating your own custom rules

  • Update
Compliance status legend 🟢 - Fully Compliant
🟡 - Partial Compliant
🔴 - Not Compliant
⚪ - Requires Further Human Verification
🏷️ - Compliance label

@qodo-code-review

qodo-code-reviewBot commented Feb 26, 2026

Copy link
Copy Markdown
Contributor

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion Impact
Possible issue
Increment only on successful enqueue
Suggestion Impact:EnqueueEvent was changed to call IncrementEnqueued() only when _pendingEvents.Writer.TryWrite(...) returns true, aligning the enqueue counter with successful channel writes; additionally added a warning log when TryWrite fails.

code diff:

@@ -85,8 +85,17 @@
{
if (_events.TryGetValue(method, out var registration))
{
- registration.IncrementEnqueued();- _pendingEvents.Writer.TryWrite(new EventItem(jsonUtf8Bytes, bidi, registration));+ if (_pendingEvents.Writer.TryWrite(new EventItem(jsonUtf8Bytes, bidi, registration)))+ {+ registration.IncrementEnqueued();+ }+ else+ {+ if (_logger.IsEnabled(LogEventLevel.Warn))+ {+ _logger.Warn($"Failed to enqueue BiDi event with method '{method}' for processing. Event will be ignored.");+ }+ }
}

In EnqueueEvent, only call registration.IncrementEnqueued() if
_pendingEvents.Writer.TryWrite(...) returns true to prevent a potential deadlock
in DrainAsync.

dotnet/src/webdriver/BiDi/EventDispatcher.cs [88-89]

-registration.IncrementEnqueued();-_pendingEvents.Writer.TryWrite(new EventItem(jsonUtf8Bytes, bidi, registration));+if (_pendingEvents.Writer.TryWrite(new EventItem(jsonUtf8Bytes, bidi, registration)))+{+ registration.IncrementEnqueued();+}

[Suggestion processed]

Suggestion importance[1-10]: 9

__

Why: This suggestion fixes a critical bug where DrainAsync could hang indefinitely if TryWrite fails, by correctly synchronizing the enqueue counter with the channel write operation.

High
Prevent race condition during subscription

To prevent a race condition, move the registration.AddHandler(eventHandler) call
to after the _sessionProvider().SubscribeAsync call succeeds in the
SubscribeAsync method.

dotnet/src/webdriver/BiDi/EventDispatcher.cs [51-69]

 public async Task<Subscription> SubscribeAsync<TEventArgs>(string eventName, EventHandler eventHandler, SubscriptionOptions? options, JsonTypeInfo<TEventArgs> jsonTypeInfo, CancellationToken cancellationToken)
where TEventArgs : EventArgs
{
var registration = _events.GetOrAdd(eventName, _ => new EventRegistration(jsonTypeInfo));
- registration.AddHandler(eventHandler);-
try
{
var subscribeResult = await _sessionProvider().SubscribeAsync([eventName], new() { Contexts = options?.Contexts, UserContexts = options?.UserContexts }, cancellationToken).ConfigureAwait(false);
++ registration.AddHandler(eventHandler);
return new Subscription(subscribeResult.Subscription, this, eventHandler);
}
catch
{
- registration.RemoveHandler(eventHandler);+ // No need to remove the handler as it was never added in case of an exception.
throw;
}
}
  • Apply / Chat
Suggestion importance[1-10]: 8

__

Why: The suggestion correctly identifies a race condition where an event handler could process an event before its subscription is confirmed, improving the atomicity and correctness of the subscription logic.

Medium
Propagate cancellation token
Suggestion Impact:Updated the UnsubscribeAsync flow to pass cancellationToken into DrainAsync, and changed DrainAsync to accept and honor a CancellationToken by returning a cancellable task continuation.

code diff:

@@ -75,7 +75,7 @@
await _sessionProvider().UnsubscribeAsync([subscription.SubscriptionId], null, cancellationToken).ConfigureAwait(false);
// Wait until all pending events for this method are dispatched
- await registration.DrainAsync().ConfigureAwait(false);+ await registration.DrainAsync(cancellationToken).ConfigureAwait(false);
registration.RemoveHandler(subscription.EventHandler);
}
@@ -199,7 +199,7 @@
}
}
- public Task DrainAsync()+ public Task DrainAsync(CancellationToken cancellationToken)
{
lock (_drainLock)
{
@@ -218,7 +218,13 @@
return Task.CompletedTask;
}
- return tcs.Task;+ if (!cancellationToken.CanBeCanceled) return tcs.Task;++ return tcs.Task.ContinueWith(+ static _ => { },+ cancellationToken,+ TaskContinuationOptions.None,+ TaskScheduler.Default);
}

Propagate the cancellationToken from UnsubscribeAsync to registration.DrainAsync
to allow the draining operation to be cancelled.

dotnet/src/webdriver/BiDi/EventDispatcher.cs [78]

-await registration.DrainAsync().ConfigureAwait(false);+await registration.DrainAsync(cancellationToken).ConfigureAwait(false);

[Suggestion processed]

Suggestion importance[1-10]: 7

__

Why: The suggestion correctly proposes propagating the cancellationToken to DrainAsync to allow cancellation of the wait, which improves responsiveness and resource management.

Medium
High-level
Consider simplifying the custom synchronization logic

The custom synchronization logic in EventRegistration for DrainAsync is complex
and error-prone. It should be simplified using higher-level .NET concurrency
primitives for better readability and robustness.

Examples:

dotnet/src/webdriver/BiDi/EventDispatcher.cs [154-224]
privatesealedclassEventRegistration(JsonTypeInfotypeInfo){privatelong_enqueueSeq;privatelong_processedSeq;privatereadonlyobject_drainLock=new();privatereadonlyList<EventHandler>_handlers=[];privateList<(longTargetSeq,TaskCompletionSource<bool>Tcs)>?_drainWaiters;publicJsonTypeInfoTypeInfo{get;}=typeInfo;
...(clipped 61lines)

Solution Walkthrough:

Before:

privatesealedclassEventRegistration{privatelong_enqueueSeq;privatelong_processedSeq;privatereadonlyobject_drainLock=new();privateList<(longTargetSeq,TaskCompletionSource<bool>Tcs)>?_drainWaiters;publicvoidIncrementProcessed(){varprocessed=Interlocked.Increment(ref_processedSeq);lock(_drainLock){// ... logic to check waiters and complete their tasks ...}}publicTaskDrainAsync(){lock(_drainLock){vartarget=Volatile.Read(ref_enqueueSeq);if(Volatile.Read(ref_processedSeq)>=target)returnTask.CompletedTask;vartcs=newTaskCompletionSource<bool>(...);_drainWaiters??=[];_drainWaiters.Add((target,tcs));// ... double-check logic ...returntcs.Task;}}}

After:

// Using a SemaphoreSlim to signal completion could simplify the logic.privatesealedclassEventRegistration{privatelong_enqueueSeq;privatelong_processedSeq;privatereadonlySemaphoreSlim_drainSemaphore=new(0);publicvoidIncrementProcessed(){longprocessed=Interlocked.Increment(ref_processedSeq);if(processed==Volatile.Read(ref_enqueueSeq)){// Potentially release waiters_drainSemaphore.Release();}}publicasyncTaskDrainAsync(){vartarget=Volatile.Read(ref_enqueueSeq);if(Volatile.Read(ref_processedSeq)>=target)return;// Wait until the semaphore is released, indicating processing might be complete.await_drainSemaphore.WaitAsync();// Re-check if our target has been met.}}
Suggestion importance[1-10]: 8

__

Why: The suggestion correctly identifies the most complex part of the PR—the custom concurrency logic in EventRegistration—and proposes a valid alternative to improve maintainability and robustness, which is a significant quality improvement.

Medium
  • Update

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Refactors the .NET BiDi EventDispatcher to make event dispatch and handler unsubscription more reliable under concurrency, specifically ensuring pending events are processed before handlers are removed (addressing the race described in #16574).

Changes:

  • Replaces the queued event payload with an EventItem that carries the EventRegistration, and simplifies the background processing loop.
  • Introduces per-event-method draining via enqueue/processed sequence tracking so UnsubscribeAsync can wait for already-enqueued events to finish dispatching.
  • Hardens handler management with locked add/remove and snapshotting, and improves error isolation between deserialization vs handler invocation.

Comment threaddotnet/src/webdriver/BiDi/EventDispatcher.cs Outdated
Comment threaddotnet/src/webdriver/BiDi/EventDispatcher.cs Outdated
Comment threaddotnet/src/webdriver/BiDi/EventDispatcher.cs Outdated
CopilotAI review requested due to automatic review settings February 26, 2026 11:57

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 3 comments.

Comment threaddotnet/src/webdriver/BiDi/EventDispatcher.cs
Comment threaddotnet/src/webdriver/BiDi/EventDispatcher.cs Outdated
Comment threaddotnet/src/webdriver/BiDi/EventDispatcher.cs Outdated
@nvborisenko

Copy link
Copy Markdown
MemberAuthor

Seems overcomplicated, but it resolves simple behaviour:

awaitsubscription.DisposeAsync();// waiting until events in the queue are dispatched

Can be simplified if w3c/webdriver-bidi#1032 is resolved via subscriptionId in all events.

@nvborisenko
nvborisenko merged commit 2f71272 into SeleniumHQ:trunkFeb 26, 2026
18 of 19 checks passed
@nvborisenko
nvborisenko deleted the bidi-drain-events-per-method branch February 26, 2026 15:41
nvborisenko added a commit to nvborisenko/selenium-hq that referenced this pull request Mar 5, 2026
nvborisenko added a commit that referenced this pull request Mar 5, 2026
Revert "[dotnet] [bidi] Wait until events are dispatched when unsubscribing (#17142)"
This reverts commit 2f71272.
AutomatedTester pushed a commit that referenced this pull request Mar 11, 2026
Revert "[dotnet] [bidi] Wait until events are dispatched when unsubscribing (#17142)"
This reverts commit 2f71272.
krishnamohan-kothapalli pushed a commit to krishnamohan-kothapalli/selenium that referenced this pull request Mar 18, 2026
…Q#17178)
Revert "[dotnet] [bidi] Wait until events are dispatched when unsubscribing (SeleniumHQ#17142)"
This reverts commit 2f71272.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[🐛 Bug]: [dotnet] BiDi logs potentially missing with race condition

3 participants

@nvborisenko@selenium-ci
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

[dotnet] [bidi] Wait until events are dispatched when unsubscribing - #17142

Merged
nvborisenko merged 10 commits into
SeleniumHQ:trunkfrom
nvborisenko:bidi-drain-events-per-method
Feb 26, 2026
Merged

[dotnet] [bidi] Wait until events are dispatched when unsubscribing#17142
nvborisenko merged 10 commits into
SeleniumHQ:trunkfrom
nvborisenko:bidi-drain-events-per-method

Conversation

@nvborisenko

Copy link
Copy Markdown
Member

🔗 Related Issues

Fix#16574

💥 What does this PR do?

Refactors the event dispatching mechanism in the EventDispatcher class to improve thread safety, reliability, and event handler management. The changes introduce a new EventItem structure, enhance handler registration/removal, and ensure that all pending events are processed before unsubscribing handlers. Error handling and concurrency controls have also been improved.

🔄 Types of changes

  • Bug fix (backwards compatible)

CopilotAI review requested due to automatic review settings February 26, 2026 11:23
@qodo-code-review

Copy link
Copy Markdown
Contributor

PR Type

Bug fix


Description

  • Refactors event dispatching to ensure all pending events are processed before unsubscribing handlers

  • Introduces sequence-based draining mechanism to track enqueued and processed events per registration

  • Improves thread safety with proper locking and atomic operations for handler management

  • Enhances error handling with separate try-catch blocks for deserialization and handler invocation

  • Adds handler before remote subscription to prevent race conditions on subscription failure


File Walkthrough

Relevant files
Bug fix
EventDispatcher.cs
Event draining mechanism and thread-safe handler management

dotnet/src/webdriver/BiDi/EventDispatcher.cs

  • Replaced PendingEvent struct with EventItem record containing
    registration reference instead of TypeInfo
  • Refactored EventRegistration class with sequence-based draining
    mechanism using _enqueueSeq and _processedSeq counters
  • Added DrainAsync() method to wait until all pending events for a
    registration are processed before handler removal
  • Introduced thread-safe handler management with AddHandler(),
    RemoveHandler(), and GetHandlersSnapshot() methods
  • Simplified task creation by replacing TaskFactory with direct
    Task.Run() call
  • Enhanced error handling with separate exception handling for
    deserialization and handler invocation
  • Added handler registration before remote subscription with rollback on
    failure
+111/-23

@selenium-ciselenium-ci added the C-dotnet .NET Bindings label Feb 26, 2026
@qodo-code-review

qodo-code-reviewBot commented Feb 26, 2026

Copy link
Copy Markdown
Contributor

PR Compliance Guide 🔍

Below is a summary of compliance checks for this PR:

Security Compliance
🟢
No security concerns identified No security vulnerabilities detected by AI analysis. Human verification advised for critical code.
Ticket Compliance
🟡
🎫 #16574
🟢Prevent race conditions where events are still in-flight / queued when the subscription is
disposed or the connection is closing.
Ensure BiDi log events (e.g., console logs) are reliably delivered to the registered
OnEntryAddedAsync handler without requiring a time-based delay.
Codebase Duplication Compliance
Codebase context is not defined

Follow the guide to enable codebase context checks.

Custom Compliance
🟢
Generic: Comprehensive Audit Trails

Objective: To create a detailed and reliable record of critical system actions for security analysis
and compliance.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Meaningful Naming and Self-Documenting Code

Objective: Ensure all identifiers clearly express their purpose and intent, making code
self-documenting

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Error Handling

Objective: To prevent the leakage of sensitive system information through error messages while
providing sufficient detail for internal debugging.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Security-First Input Validation and Data Handling

Objective: Ensure all data inputs are validated, sanitized, and handled securely to prevent
vulnerabilities

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

🔴
Generic: Robust Error Handling and Edge Case Management

Objective: Ensure comprehensive error handling that provides meaningful context and graceful
degradation

Status:
Silent event drop: The result of _pendingEvents.Writer.TryWrite(...) is ignored, which can silently drop
events (e.g., after Complete() during disposal) without any fallback or logging.

Referred Code
if(_events.TryGetValue(method,outvarregistration)){registration.IncrementEnqueued();_pendingEvents.Writer.TryWrite(newEventItem(jsonUtf8Bytes,bidi,registration));}

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Logging Practices

Objective: To ensure logs are useful for debugging and auditing without exposing sensitive
information like PII, PHI, or cardholder data.

Status:
Exception details logged: Error logs interpolate Exception objects (e.g., $"... {ex}"), which may include
sensitive data from exception messages/stack traces depending on upstream content and
logging sinks.

Referred Code
catch(Exceptionex){if(_logger.IsEnabled(LogEventLevel.Error)){_logger.Error($"Unhandled error processing BiDi event handler: {ex}");}}}}catch(Exceptionex){if(_logger.IsEnabled(LogEventLevel.Error)){_logger.Error($"Unhandled error deserializing BiDi event: {ex}");}

Learn more about managing compliance generic rules or creating your own custom rules

  • Update
Compliance status legend 🟢 - Fully Compliant
🟡 - Partial Compliant
🔴 - Not Compliant
⚪ - Requires Further Human Verification
🏷️ - Compliance label

@qodo-code-review

qodo-code-reviewBot commented Feb 26, 2026

Copy link
Copy Markdown
Contributor

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion Impact
Possible issue
Increment only on successful enqueue
Suggestion Impact:EnqueueEvent was changed to call IncrementEnqueued() only when _pendingEvents.Writer.TryWrite(...) returns true, aligning the enqueue counter with successful channel writes; additionally added a warning log when TryWrite fails.

code diff:

@@ -85,8 +85,17 @@
{
if (_events.TryGetValue(method, out var registration))
{
- registration.IncrementEnqueued();- _pendingEvents.Writer.TryWrite(new EventItem(jsonUtf8Bytes, bidi, registration));+ if (_pendingEvents.Writer.TryWrite(new EventItem(jsonUtf8Bytes, bidi, registration)))+ {+ registration.IncrementEnqueued();+ }+ else+ {+ if (_logger.IsEnabled(LogEventLevel.Warn))+ {+ _logger.Warn($"Failed to enqueue BiDi event with method '{method}' for processing. Event will be ignored.");+ }+ }
}

In EnqueueEvent, only call registration.IncrementEnqueued() if
_pendingEvents.Writer.TryWrite(...) returns true to prevent a potential deadlock
in DrainAsync.

dotnet/src/webdriver/BiDi/EventDispatcher.cs [88-89]

-registration.IncrementEnqueued();-_pendingEvents.Writer.TryWrite(new EventItem(jsonUtf8Bytes, bidi, registration));+if (_pendingEvents.Writer.TryWrite(new EventItem(jsonUtf8Bytes, bidi, registration)))+{+ registration.IncrementEnqueued();+}

[Suggestion processed]

Suggestion importance[1-10]: 9

__

Why: This suggestion fixes a critical bug where DrainAsync could hang indefinitely if TryWrite fails, by correctly synchronizing the enqueue counter with the channel write operation.

High
Prevent race condition during subscription

To prevent a race condition, move the registration.AddHandler(eventHandler) call
to after the _sessionProvider().SubscribeAsync call succeeds in the
SubscribeAsync method.

dotnet/src/webdriver/BiDi/EventDispatcher.cs [51-69]

 public async Task<Subscription> SubscribeAsync<TEventArgs>(string eventName, EventHandler eventHandler, SubscriptionOptions? options, JsonTypeInfo<TEventArgs> jsonTypeInfo, CancellationToken cancellationToken)
where TEventArgs : EventArgs
{
var registration = _events.GetOrAdd(eventName, _ => new EventRegistration(jsonTypeInfo));
- registration.AddHandler(eventHandler);-
try
{
var subscribeResult = await _sessionProvider().SubscribeAsync([eventName], new() { Contexts = options?.Contexts, UserContexts = options?.UserContexts }, cancellationToken).ConfigureAwait(false);
++ registration.AddHandler(eventHandler);
return new Subscription(subscribeResult.Subscription, this, eventHandler);
}
catch
{
- registration.RemoveHandler(eventHandler);+ // No need to remove the handler as it was never added in case of an exception.
throw;
}
}
  • Apply / Chat
Suggestion importance[1-10]: 8

__

Why: The suggestion correctly identifies a race condition where an event handler could process an event before its subscription is confirmed, improving the atomicity and correctness of the subscription logic.

Medium
Propagate cancellation token
Suggestion Impact:Updated the UnsubscribeAsync flow to pass cancellationToken into DrainAsync, and changed DrainAsync to accept and honor a CancellationToken by returning a cancellable task continuation.

code diff:

@@ -75,7 +75,7 @@
await _sessionProvider().UnsubscribeAsync([subscription.SubscriptionId], null, cancellationToken).ConfigureAwait(false);
// Wait until all pending events for this method are dispatched
- await registration.DrainAsync().ConfigureAwait(false);+ await registration.DrainAsync(cancellationToken).ConfigureAwait(false);
registration.RemoveHandler(subscription.EventHandler);
}
@@ -199,7 +199,7 @@
}
}
- public Task DrainAsync()+ public Task DrainAsync(CancellationToken cancellationToken)
{
lock (_drainLock)
{
@@ -218,7 +218,13 @@
return Task.CompletedTask;
}
- return tcs.Task;+ if (!cancellationToken.CanBeCanceled) return tcs.Task;++ return tcs.Task.ContinueWith(+ static _ => { },+ cancellationToken,+ TaskContinuationOptions.None,+ TaskScheduler.Default);
}

Propagate the cancellationToken from UnsubscribeAsync to registration.DrainAsync
to allow the draining operation to be cancelled.

dotnet/src/webdriver/BiDi/EventDispatcher.cs [78]

-await registration.DrainAsync().ConfigureAwait(false);+await registration.DrainAsync(cancellationToken).ConfigureAwait(false);

[Suggestion processed]

Suggestion importance[1-10]: 7

__

Why: The suggestion correctly proposes propagating the cancellationToken to DrainAsync to allow cancellation of the wait, which improves responsiveness and resource management.

Medium
High-level
Consider simplifying the custom synchronization logic

The custom synchronization logic in EventRegistration for DrainAsync is complex
and error-prone. It should be simplified using higher-level .NET concurrency
primitives for better readability and robustness.

Examples:

dotnet/src/webdriver/BiDi/EventDispatcher.cs [154-224]
privatesealedclassEventRegistration(JsonTypeInfotypeInfo){privatelong_enqueueSeq;privatelong_processedSeq;privatereadonlyobject_drainLock=new();privatereadonlyList<EventHandler>_handlers=[];privateList<(longTargetSeq,TaskCompletionSource<bool>Tcs)>?_drainWaiters;publicJsonTypeInfoTypeInfo{get;}=typeInfo;
...(clipped 61lines)

Solution Walkthrough:

Before:

privatesealedclassEventRegistration{privatelong_enqueueSeq;privatelong_processedSeq;privatereadonlyobject_drainLock=new();privateList<(longTargetSeq,TaskCompletionSource<bool>Tcs)>?_drainWaiters;publicvoidIncrementProcessed(){varprocessed=Interlocked.Increment(ref_processedSeq);lock(_drainLock){// ... logic to check waiters and complete their tasks ...}}publicTaskDrainAsync(){lock(_drainLock){vartarget=Volatile.Read(ref_enqueueSeq);if(Volatile.Read(ref_processedSeq)>=target)returnTask.CompletedTask;vartcs=newTaskCompletionSource<bool>(...);_drainWaiters??=[];_drainWaiters.Add((target,tcs));// ... double-check logic ...returntcs.Task;}}}

After:

// Using a SemaphoreSlim to signal completion could simplify the logic.privatesealedclassEventRegistration{privatelong_enqueueSeq;privatelong_processedSeq;privatereadonlySemaphoreSlim_drainSemaphore=new(0);publicvoidIncrementProcessed(){longprocessed=Interlocked.Increment(ref_processedSeq);if(processed==Volatile.Read(ref_enqueueSeq)){// Potentially release waiters_drainSemaphore.Release();}}publicasyncTaskDrainAsync(){vartarget=Volatile.Read(ref_enqueueSeq);if(Volatile.Read(ref_processedSeq)>=target)return;// Wait until the semaphore is released, indicating processing might be complete.await_drainSemaphore.WaitAsync();// Re-check if our target has been met.}}
Suggestion importance[1-10]: 8

__

Why: The suggestion correctly identifies the most complex part of the PR—the custom concurrency logic in EventRegistration—and proposes a valid alternative to improve maintainability and robustness, which is a significant quality improvement.

Medium
  • Update

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Refactors the .NET BiDi EventDispatcher to make event dispatch and handler unsubscription more reliable under concurrency, specifically ensuring pending events are processed before handlers are removed (addressing the race described in #16574).

Changes:

  • Replaces the queued event payload with an EventItem that carries the EventRegistration, and simplifies the background processing loop.
  • Introduces per-event-method draining via enqueue/processed sequence tracking so UnsubscribeAsync can wait for already-enqueued events to finish dispatching.
  • Hardens handler management with locked add/remove and snapshotting, and improves error isolation between deserialization vs handler invocation.

Comment threaddotnet/src/webdriver/BiDi/EventDispatcher.cs Outdated
Comment threaddotnet/src/webdriver/BiDi/EventDispatcher.cs Outdated
Comment threaddotnet/src/webdriver/BiDi/EventDispatcher.cs Outdated
CopilotAI review requested due to automatic review settings February 26, 2026 11:57

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 3 comments.

Comment threaddotnet/src/webdriver/BiDi/EventDispatcher.cs
Comment threaddotnet/src/webdriver/BiDi/EventDispatcher.cs Outdated
Comment threaddotnet/src/webdriver/BiDi/EventDispatcher.cs Outdated
@nvborisenko

Copy link
Copy Markdown
MemberAuthor

Seems overcomplicated, but it resolves simple behaviour:

awaitsubscription.DisposeAsync();// waiting until events in the queue are dispatched

Can be simplified if w3c/webdriver-bidi#1032 is resolved via subscriptionId in all events.

@nvborisenko
nvborisenko merged commit 2f71272 into SeleniumHQ:trunkFeb 26, 2026
18 of 19 checks passed
@nvborisenko
nvborisenko deleted the bidi-drain-events-per-method branch February 26, 2026 15:41
nvborisenko added a commit to nvborisenko/selenium-hq that referenced this pull request Mar 5, 2026
nvborisenko added a commit that referenced this pull request Mar 5, 2026
Revert "[dotnet] [bidi] Wait until events are dispatched when unsubscribing (#17142)"
This reverts commit 2f71272.
AutomatedTester pushed a commit that referenced this pull request Mar 11, 2026
Revert "[dotnet] [bidi] Wait until events are dispatched when unsubscribing (#17142)"
This reverts commit 2f71272.
krishnamohan-kothapalli pushed a commit to krishnamohan-kothapalli/selenium that referenced this pull request Mar 18, 2026
…Q#17178)
Revert "[dotnet] [bidi] Wait until events are dispatched when unsubscribing (SeleniumHQ#17142)"
This reverts commit 2f71272.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[🐛 Bug]: [dotnet] BiDi logs potentially missing with race condition

3 participants

@nvborisenko@selenium-ci
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

[dotnet] [bidi] Wait until events are dispatched when unsubscribing - #17142

Merged
nvborisenko merged 10 commits into
SeleniumHQ:trunkfrom
nvborisenko:bidi-drain-events-per-method
Feb 26, 2026
Merged

[dotnet] [bidi] Wait until events are dispatched when unsubscribing#17142
nvborisenko merged 10 commits into
SeleniumHQ:trunkfrom
nvborisenko:bidi-drain-events-per-method

Conversation

@nvborisenko

Copy link
Copy Markdown
Member

🔗 Related Issues

Fix#16574

💥 What does this PR do?

Refactors the event dispatching mechanism in the EventDispatcher class to improve thread safety, reliability, and event handler management. The changes introduce a new EventItem structure, enhance handler registration/removal, and ensure that all pending events are processed before unsubscribing handlers. Error handling and concurrency controls have also been improved.

🔄 Types of changes

  • Bug fix (backwards compatible)

CopilotAI review requested due to automatic review settings February 26, 2026 11:23
@qodo-code-review

Copy link
Copy Markdown
Contributor

PR Type

Bug fix


Description

  • Refactors event dispatching to ensure all pending events are processed before unsubscribing handlers

  • Introduces sequence-based draining mechanism to track enqueued and processed events per registration

  • Improves thread safety with proper locking and atomic operations for handler management

  • Enhances error handling with separate try-catch blocks for deserialization and handler invocation

  • Adds handler before remote subscription to prevent race conditions on subscription failure


File Walkthrough

Relevant files
Bug fix
EventDispatcher.cs
Event draining mechanism and thread-safe handler management

dotnet/src/webdriver/BiDi/EventDispatcher.cs

  • Replaced PendingEvent struct with EventItem record containing
    registration reference instead of TypeInfo
  • Refactored EventRegistration class with sequence-based draining
    mechanism using _enqueueSeq and _processedSeq counters
  • Added DrainAsync() method to wait until all pending events for a
    registration are processed before handler removal
  • Introduced thread-safe handler management with AddHandler(),
    RemoveHandler(), and GetHandlersSnapshot() methods
  • Simplified task creation by replacing TaskFactory with direct
    Task.Run() call
  • Enhanced error handling with separate exception handling for
    deserialization and handler invocation
  • Added handler registration before remote subscription with rollback on
    failure
+111/-23

@selenium-ciselenium-ci added the C-dotnet .NET Bindings label Feb 26, 2026
@qodo-code-review

qodo-code-reviewBot commented Feb 26, 2026

Copy link
Copy Markdown
Contributor

PR Compliance Guide 🔍

Below is a summary of compliance checks for this PR:

Security Compliance
🟢
No security concerns identified No security vulnerabilities detected by AI analysis. Human verification advised for critical code.
Ticket Compliance
🟡
🎫 #16574
🟢Prevent race conditions where events are still in-flight / queued when the subscription is
disposed or the connection is closing.
Ensure BiDi log events (e.g., console logs) are reliably delivered to the registered
OnEntryAddedAsync handler without requiring a time-based delay.
Codebase Duplication Compliance
Codebase context is not defined

Follow the guide to enable codebase context checks.

Custom Compliance
🟢
Generic: Comprehensive Audit Trails

Objective: To create a detailed and reliable record of critical system actions for security analysis
and compliance.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Meaningful Naming and Self-Documenting Code

Objective: Ensure all identifiers clearly express their purpose and intent, making code
self-documenting

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Error Handling

Objective: To prevent the leakage of sensitive system information through error messages while
providing sufficient detail for internal debugging.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Security-First Input Validation and Data Handling

Objective: Ensure all data inputs are validated, sanitized, and handled securely to prevent
vulnerabilities

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

🔴
Generic: Robust Error Handling and Edge Case Management

Objective: Ensure comprehensive error handling that provides meaningful context and graceful
degradation

Status:
Silent event drop: The result of _pendingEvents.Writer.TryWrite(...) is ignored, which can silently drop
events (e.g., after Complete() during disposal) without any fallback or logging.

Referred Code
if(_events.TryGetValue(method,outvarregistration)){registration.IncrementEnqueued();_pendingEvents.Writer.TryWrite(newEventItem(jsonUtf8Bytes,bidi,registration));}

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Logging Practices

Objective: To ensure logs are useful for debugging and auditing without exposing sensitive
information like PII, PHI, or cardholder data.

Status:
Exception details logged: Error logs interpolate Exception objects (e.g., $"... {ex}"), which may include
sensitive data from exception messages/stack traces depending on upstream content and
logging sinks.

Referred Code
catch(Exceptionex){if(_logger.IsEnabled(LogEventLevel.Error)){_logger.Error($"Unhandled error processing BiDi event handler: {ex}");}}}}catch(Exceptionex){if(_logger.IsEnabled(LogEventLevel.Error)){_logger.Error($"Unhandled error deserializing BiDi event: {ex}");}

Learn more about managing compliance generic rules or creating your own custom rules

  • Update
Compliance status legend 🟢 - Fully Compliant
🟡 - Partial Compliant
🔴 - Not Compliant
⚪ - Requires Further Human Verification
🏷️ - Compliance label

@qodo-code-review

qodo-code-reviewBot commented Feb 26, 2026

Copy link
Copy Markdown
Contributor

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion Impact
Possible issue
Increment only on successful enqueue
Suggestion Impact:EnqueueEvent was changed to call IncrementEnqueued() only when _pendingEvents.Writer.TryWrite(...) returns true, aligning the enqueue counter with successful channel writes; additionally added a warning log when TryWrite fails.

code diff:

@@ -85,8 +85,17 @@
{
if (_events.TryGetValue(method, out var registration))
{
- registration.IncrementEnqueued();- _pendingEvents.Writer.TryWrite(new EventItem(jsonUtf8Bytes, bidi, registration));+ if (_pendingEvents.Writer.TryWrite(new EventItem(jsonUtf8Bytes, bidi, registration)))+ {+ registration.IncrementEnqueued();+ }+ else+ {+ if (_logger.IsEnabled(LogEventLevel.Warn))+ {+ _logger.Warn($"Failed to enqueue BiDi event with method '{method}' for processing. Event will be ignored.");+ }+ }
}

In EnqueueEvent, only call registration.IncrementEnqueued() if
_pendingEvents.Writer.TryWrite(...) returns true to prevent a potential deadlock
in DrainAsync.

dotnet/src/webdriver/BiDi/EventDispatcher.cs [88-89]

-registration.IncrementEnqueued();-_pendingEvents.Writer.TryWrite(new EventItem(jsonUtf8Bytes, bidi, registration));+if (_pendingEvents.Writer.TryWrite(new EventItem(jsonUtf8Bytes, bidi, registration)))+{+ registration.IncrementEnqueued();+}

[Suggestion processed]

Suggestion importance[1-10]: 9

__

Why: This suggestion fixes a critical bug where DrainAsync could hang indefinitely if TryWrite fails, by correctly synchronizing the enqueue counter with the channel write operation.

High
Prevent race condition during subscription

To prevent a race condition, move the registration.AddHandler(eventHandler) call
to after the _sessionProvider().SubscribeAsync call succeeds in the
SubscribeAsync method.

dotnet/src/webdriver/BiDi/EventDispatcher.cs [51-69]

 public async Task<Subscription> SubscribeAsync<TEventArgs>(string eventName, EventHandler eventHandler, SubscriptionOptions? options, JsonTypeInfo<TEventArgs> jsonTypeInfo, CancellationToken cancellationToken)
where TEventArgs : EventArgs
{
var registration = _events.GetOrAdd(eventName, _ => new EventRegistration(jsonTypeInfo));
- registration.AddHandler(eventHandler);-
try
{
var subscribeResult = await _sessionProvider().SubscribeAsync([eventName], new() { Contexts = options?.Contexts, UserContexts = options?.UserContexts }, cancellationToken).ConfigureAwait(false);
++ registration.AddHandler(eventHandler);
return new Subscription(subscribeResult.Subscription, this, eventHandler);
}
catch
{
- registration.RemoveHandler(eventHandler);+ // No need to remove the handler as it was never added in case of an exception.
throw;
}
}
  • Apply / Chat
Suggestion importance[1-10]: 8

__

Why: The suggestion correctly identifies a race condition where an event handler could process an event before its subscription is confirmed, improving the atomicity and correctness of the subscription logic.

Medium
Propagate cancellation token
Suggestion Impact:Updated the UnsubscribeAsync flow to pass cancellationToken into DrainAsync, and changed DrainAsync to accept and honor a CancellationToken by returning a cancellable task continuation.

code diff:

@@ -75,7 +75,7 @@
await _sessionProvider().UnsubscribeAsync([subscription.SubscriptionId], null, cancellationToken).ConfigureAwait(false);
// Wait until all pending events for this method are dispatched
- await registration.DrainAsync().ConfigureAwait(false);+ await registration.DrainAsync(cancellationToken).ConfigureAwait(false);
registration.RemoveHandler(subscription.EventHandler);
}
@@ -199,7 +199,7 @@
}
}
- public Task DrainAsync()+ public Task DrainAsync(CancellationToken cancellationToken)
{
lock (_drainLock)
{
@@ -218,7 +218,13 @@
return Task.CompletedTask;
}
- return tcs.Task;+ if (!cancellationToken.CanBeCanceled) return tcs.Task;++ return tcs.Task.ContinueWith(+ static _ => { },+ cancellationToken,+ TaskContinuationOptions.None,+ TaskScheduler.Default);
}

Propagate the cancellationToken from UnsubscribeAsync to registration.DrainAsync
to allow the draining operation to be cancelled.

dotnet/src/webdriver/BiDi/EventDispatcher.cs [78]

-await registration.DrainAsync().ConfigureAwait(false);+await registration.DrainAsync(cancellationToken).ConfigureAwait(false);

[Suggestion processed]

Suggestion importance[1-10]: 7

__

Why: The suggestion correctly proposes propagating the cancellationToken to DrainAsync to allow cancellation of the wait, which improves responsiveness and resource management.

Medium
High-level
Consider simplifying the custom synchronization logic

The custom synchronization logic in EventRegistration for DrainAsync is complex
and error-prone. It should be simplified using higher-level .NET concurrency
primitives for better readability and robustness.

Examples:

dotnet/src/webdriver/BiDi/EventDispatcher.cs [154-224]
privatesealedclassEventRegistration(JsonTypeInfotypeInfo){privatelong_enqueueSeq;privatelong_processedSeq;privatereadonlyobject_drainLock=new();privatereadonlyList<EventHandler>_handlers=[];privateList<(longTargetSeq,TaskCompletionSource<bool>Tcs)>?_drainWaiters;publicJsonTypeInfoTypeInfo{get;}=typeInfo;
...(clipped 61lines)

Solution Walkthrough:

Before:

privatesealedclassEventRegistration{privatelong_enqueueSeq;privatelong_processedSeq;privatereadonlyobject_drainLock=new();privateList<(longTargetSeq,TaskCompletionSource<bool>Tcs)>?_drainWaiters;publicvoidIncrementProcessed(){varprocessed=Interlocked.Increment(ref_processedSeq);lock(_drainLock){// ... logic to check waiters and complete their tasks ...}}publicTaskDrainAsync(){lock(_drainLock){vartarget=Volatile.Read(ref_enqueueSeq);if(Volatile.Read(ref_processedSeq)>=target)returnTask.CompletedTask;vartcs=newTaskCompletionSource<bool>(...);_drainWaiters??=[];_drainWaiters.Add((target,tcs));// ... double-check logic ...returntcs.Task;}}}

After:

// Using a SemaphoreSlim to signal completion could simplify the logic.privatesealedclassEventRegistration{privatelong_enqueueSeq;privatelong_processedSeq;privatereadonlySemaphoreSlim_drainSemaphore=new(0);publicvoidIncrementProcessed(){longprocessed=Interlocked.Increment(ref_processedSeq);if(processed==Volatile.Read(ref_enqueueSeq)){// Potentially release waiters_drainSemaphore.Release();}}publicasyncTaskDrainAsync(){vartarget=Volatile.Read(ref_enqueueSeq);if(Volatile.Read(ref_processedSeq)>=target)return;// Wait until the semaphore is released, indicating processing might be complete.await_drainSemaphore.WaitAsync();// Re-check if our target has been met.}}
Suggestion importance[1-10]: 8

__

Why: The suggestion correctly identifies the most complex part of the PR—the custom concurrency logic in EventRegistration—and proposes a valid alternative to improve maintainability and robustness, which is a significant quality improvement.

Medium
  • Update

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Refactors the .NET BiDi EventDispatcher to make event dispatch and handler unsubscription more reliable under concurrency, specifically ensuring pending events are processed before handlers are removed (addressing the race described in #16574).

Changes:

  • Replaces the queued event payload with an EventItem that carries the EventRegistration, and simplifies the background processing loop.
  • Introduces per-event-method draining via enqueue/processed sequence tracking so UnsubscribeAsync can wait for already-enqueued events to finish dispatching.
  • Hardens handler management with locked add/remove and snapshotting, and improves error isolation between deserialization vs handler invocation.

Comment threaddotnet/src/webdriver/BiDi/EventDispatcher.cs Outdated
Comment threaddotnet/src/webdriver/BiDi/EventDispatcher.cs Outdated
Comment threaddotnet/src/webdriver/BiDi/EventDispatcher.cs Outdated
CopilotAI review requested due to automatic review settings February 26, 2026 11:57

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 3 comments.

Comment threaddotnet/src/webdriver/BiDi/EventDispatcher.cs
Comment threaddotnet/src/webdriver/BiDi/EventDispatcher.cs Outdated
Comment threaddotnet/src/webdriver/BiDi/EventDispatcher.cs Outdated
@nvborisenko

Copy link
Copy Markdown
MemberAuthor

Seems overcomplicated, but it resolves simple behaviour:

awaitsubscription.DisposeAsync();// waiting until events in the queue are dispatched

Can be simplified if w3c/webdriver-bidi#1032 is resolved via subscriptionId in all events.

@nvborisenko
nvborisenko merged commit 2f71272 into SeleniumHQ:trunkFeb 26, 2026
18 of 19 checks passed
@nvborisenko
nvborisenko deleted the bidi-drain-events-per-method branch February 26, 2026 15:41
nvborisenko added a commit to nvborisenko/selenium-hq that referenced this pull request Mar 5, 2026
nvborisenko added a commit that referenced this pull request Mar 5, 2026
Revert "[dotnet] [bidi] Wait until events are dispatched when unsubscribing (#17142)"
This reverts commit 2f71272.
AutomatedTester pushed a commit that referenced this pull request Mar 11, 2026
Revert "[dotnet] [bidi] Wait until events are dispatched when unsubscribing (#17142)"
This reverts commit 2f71272.
krishnamohan-kothapalli pushed a commit to krishnamohan-kothapalli/selenium that referenced this pull request Mar 18, 2026
…Q#17178)
Revert "[dotnet] [bidi] Wait until events are dispatched when unsubscribing (SeleniumHQ#17142)"
This reverts commit 2f71272.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[🐛 Bug]: [dotnet] BiDi logs potentially missing with race condition

3 participants

@nvborisenko@selenium-ci
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

[dotnet] [bidi] Wait until events are dispatched when unsubscribing - #17142

Merged
nvborisenko merged 10 commits into
SeleniumHQ:trunkfrom
nvborisenko:bidi-drain-events-per-method
Feb 26, 2026
Merged

[dotnet] [bidi] Wait until events are dispatched when unsubscribing#17142
nvborisenko merged 10 commits into
SeleniumHQ:trunkfrom
nvborisenko:bidi-drain-events-per-method

Conversation

@nvborisenko

Copy link
Copy Markdown
Member

🔗 Related Issues

Fix#16574

💥 What does this PR do?

Refactors the event dispatching mechanism in the EventDispatcher class to improve thread safety, reliability, and event handler management. The changes introduce a new EventItem structure, enhance handler registration/removal, and ensure that all pending events are processed before unsubscribing handlers. Error handling and concurrency controls have also been improved.

🔄 Types of changes

  • Bug fix (backwards compatible)

CopilotAI review requested due to automatic review settings February 26, 2026 11:23
@qodo-code-review

Copy link
Copy Markdown
Contributor

PR Type

Bug fix


Description

  • Refactors event dispatching to ensure all pending events are processed before unsubscribing handlers

  • Introduces sequence-based draining mechanism to track enqueued and processed events per registration

  • Improves thread safety with proper locking and atomic operations for handler management

  • Enhances error handling with separate try-catch blocks for deserialization and handler invocation

  • Adds handler before remote subscription to prevent race conditions on subscription failure


File Walkthrough

Relevant files
Bug fix
EventDispatcher.cs
Event draining mechanism and thread-safe handler management

dotnet/src/webdriver/BiDi/EventDispatcher.cs

  • Replaced PendingEvent struct with EventItem record containing
    registration reference instead of TypeInfo
  • Refactored EventRegistration class with sequence-based draining
    mechanism using _enqueueSeq and _processedSeq counters
  • Added DrainAsync() method to wait until all pending events for a
    registration are processed before handler removal
  • Introduced thread-safe handler management with AddHandler(),
    RemoveHandler(), and GetHandlersSnapshot() methods
  • Simplified task creation by replacing TaskFactory with direct
    Task.Run() call
  • Enhanced error handling with separate exception handling for
    deserialization and handler invocation
  • Added handler registration before remote subscription with rollback on
    failure
+111/-23

@selenium-ciselenium-ci added the C-dotnet .NET Bindings label Feb 26, 2026
@qodo-code-review

qodo-code-reviewBot commented Feb 26, 2026

Copy link
Copy Markdown
Contributor

PR Compliance Guide 🔍

Below is a summary of compliance checks for this PR:

Security Compliance
🟢
No security concerns identified No security vulnerabilities detected by AI analysis. Human verification advised for critical code.
Ticket Compliance
🟡
🎫 #16574
🟢Prevent race conditions where events are still in-flight / queued when the subscription is
disposed or the connection is closing.
Ensure BiDi log events (e.g., console logs) are reliably delivered to the registered
OnEntryAddedAsync handler without requiring a time-based delay.
Codebase Duplication Compliance
Codebase context is not defined

Follow the guide to enable codebase context checks.

Custom Compliance
🟢
Generic: Comprehensive Audit Trails

Objective: To create a detailed and reliable record of critical system actions for security analysis
and compliance.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Meaningful Naming and Self-Documenting Code

Objective: Ensure all identifiers clearly express their purpose and intent, making code
self-documenting

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Error Handling

Objective: To prevent the leakage of sensitive system information through error messages while
providing sufficient detail for internal debugging.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Security-First Input Validation and Data Handling

Objective: Ensure all data inputs are validated, sanitized, and handled securely to prevent
vulnerabilities

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

🔴
Generic: Robust Error Handling and Edge Case Management

Objective: Ensure comprehensive error handling that provides meaningful context and graceful
degradation

Status:
Silent event drop: The result of _pendingEvents.Writer.TryWrite(...) is ignored, which can silently drop
events (e.g., after Complete() during disposal) without any fallback or logging.

Referred Code
if(_events.TryGetValue(method,outvarregistration)){registration.IncrementEnqueued();_pendingEvents.Writer.TryWrite(newEventItem(jsonUtf8Bytes,bidi,registration));}

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Logging Practices

Objective: To ensure logs are useful for debugging and auditing without exposing sensitive
information like PII, PHI, or cardholder data.

Status:
Exception details logged: Error logs interpolate Exception objects (e.g., $"... {ex}"), which may include
sensitive data from exception messages/stack traces depending on upstream content and
logging sinks.

Referred Code
catch(Exceptionex){if(_logger.IsEnabled(LogEventLevel.Error)){_logger.Error($"Unhandled error processing BiDi event handler: {ex}");}}}}catch(Exceptionex){if(_logger.IsEnabled(LogEventLevel.Error)){_logger.Error($"Unhandled error deserializing BiDi event: {ex}");}

Learn more about managing compliance generic rules or creating your own custom rules

  • Update
Compliance status legend 🟢 - Fully Compliant
🟡 - Partial Compliant
🔴 - Not Compliant
⚪ - Requires Further Human Verification
🏷️ - Compliance label

@qodo-code-review

qodo-code-reviewBot commented Feb 26, 2026

Copy link
Copy Markdown
Contributor

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion Impact
Possible issue
Increment only on successful enqueue
Suggestion Impact:EnqueueEvent was changed to call IncrementEnqueued() only when _pendingEvents.Writer.TryWrite(...) returns true, aligning the enqueue counter with successful channel writes; additionally added a warning log when TryWrite fails.

code diff:

@@ -85,8 +85,17 @@
{
if (_events.TryGetValue(method, out var registration))
{
- registration.IncrementEnqueued();- _pendingEvents.Writer.TryWrite(new EventItem(jsonUtf8Bytes, bidi, registration));+ if (_pendingEvents.Writer.TryWrite(new EventItem(jsonUtf8Bytes, bidi, registration)))+ {+ registration.IncrementEnqueued();+ }+ else+ {+ if (_logger.IsEnabled(LogEventLevel.Warn))+ {+ _logger.Warn($"Failed to enqueue BiDi event with method '{method}' for processing. Event will be ignored.");+ }+ }
}

In EnqueueEvent, only call registration.IncrementEnqueued() if
_pendingEvents.Writer.TryWrite(...) returns true to prevent a potential deadlock
in DrainAsync.

dotnet/src/webdriver/BiDi/EventDispatcher.cs [88-89]

-registration.IncrementEnqueued();-_pendingEvents.Writer.TryWrite(new EventItem(jsonUtf8Bytes, bidi, registration));+if (_pendingEvents.Writer.TryWrite(new EventItem(jsonUtf8Bytes, bidi, registration)))+{+ registration.IncrementEnqueued();+}

[Suggestion processed]

Suggestion importance[1-10]: 9

__

Why: This suggestion fixes a critical bug where DrainAsync could hang indefinitely if TryWrite fails, by correctly synchronizing the enqueue counter with the channel write operation.

High
Prevent race condition during subscription

To prevent a race condition, move the registration.AddHandler(eventHandler) call
to after the _sessionProvider().SubscribeAsync call succeeds in the
SubscribeAsync method.

dotnet/src/webdriver/BiDi/EventDispatcher.cs [51-69]

 public async Task<Subscription> SubscribeAsync<TEventArgs>(string eventName, EventHandler eventHandler, SubscriptionOptions? options, JsonTypeInfo<TEventArgs> jsonTypeInfo, CancellationToken cancellationToken)
where TEventArgs : EventArgs
{
var registration = _events.GetOrAdd(eventName, _ => new EventRegistration(jsonTypeInfo));
- registration.AddHandler(eventHandler);-
try
{
var subscribeResult = await _sessionProvider().SubscribeAsync([eventName], new() { Contexts = options?.Contexts, UserContexts = options?.UserContexts }, cancellationToken).ConfigureAwait(false);
++ registration.AddHandler(eventHandler);
return new Subscription(subscribeResult.Subscription, this, eventHandler);
}
catch
{
- registration.RemoveHandler(eventHandler);+ // No need to remove the handler as it was never added in case of an exception.
throw;
}
}
  • Apply / Chat
Suggestion importance[1-10]: 8

__

Why: The suggestion correctly identifies a race condition where an event handler could process an event before its subscription is confirmed, improving the atomicity and correctness of the subscription logic.

Medium
Propagate cancellation token
Suggestion Impact:Updated the UnsubscribeAsync flow to pass cancellationToken into DrainAsync, and changed DrainAsync to accept and honor a CancellationToken by returning a cancellable task continuation.

code diff:

@@ -75,7 +75,7 @@
await _sessionProvider().UnsubscribeAsync([subscription.SubscriptionId], null, cancellationToken).ConfigureAwait(false);
// Wait until all pending events for this method are dispatched
- await registration.DrainAsync().ConfigureAwait(false);+ await registration.DrainAsync(cancellationToken).ConfigureAwait(false);
registration.RemoveHandler(subscription.EventHandler);
}
@@ -199,7 +199,7 @@
}
}
- public Task DrainAsync()+ public Task DrainAsync(CancellationToken cancellationToken)
{
lock (_drainLock)
{
@@ -218,7 +218,13 @@
return Task.CompletedTask;
}
- return tcs.Task;+ if (!cancellationToken.CanBeCanceled) return tcs.Task;++ return tcs.Task.ContinueWith(+ static _ => { },+ cancellationToken,+ TaskContinuationOptions.None,+ TaskScheduler.Default);
}

Propagate the cancellationToken from UnsubscribeAsync to registration.DrainAsync
to allow the draining operation to be cancelled.

dotnet/src/webdriver/BiDi/EventDispatcher.cs [78]

-await registration.DrainAsync().ConfigureAwait(false);+await registration.DrainAsync(cancellationToken).ConfigureAwait(false);

[Suggestion processed]

Suggestion importance[1-10]: 7

__

Why: The suggestion correctly proposes propagating the cancellationToken to DrainAsync to allow cancellation of the wait, which improves responsiveness and resource management.

Medium
High-level
Consider simplifying the custom synchronization logic

The custom synchronization logic in EventRegistration for DrainAsync is complex
and error-prone. It should be simplified using higher-level .NET concurrency
primitives for better readability and robustness.

Examples:

dotnet/src/webdriver/BiDi/EventDispatcher.cs [154-224]
privatesealedclassEventRegistration(JsonTypeInfotypeInfo){privatelong_enqueueSeq;privatelong_processedSeq;privatereadonlyobject_drainLock=new();privatereadonlyList<EventHandler>_handlers=[];privateList<(longTargetSeq,TaskCompletionSource<bool>Tcs)>?_drainWaiters;publicJsonTypeInfoTypeInfo{get;}=typeInfo;
...(clipped 61lines)

Solution Walkthrough:

Before:

privatesealedclassEventRegistration{privatelong_enqueueSeq;privatelong_processedSeq;privatereadonlyobject_drainLock=new();privateList<(longTargetSeq,TaskCompletionSource<bool>Tcs)>?_drainWaiters;publicvoidIncrementProcessed(){varprocessed=Interlocked.Increment(ref_processedSeq);lock(_drainLock){// ... logic to check waiters and complete their tasks ...}}publicTaskDrainAsync(){lock(_drainLock){vartarget=Volatile.Read(ref_enqueueSeq);if(Volatile.Read(ref_processedSeq)>=target)returnTask.CompletedTask;vartcs=newTaskCompletionSource<bool>(...);_drainWaiters??=[];_drainWaiters.Add((target,tcs));// ... double-check logic ...returntcs.Task;}}}

After:

// Using a SemaphoreSlim to signal completion could simplify the logic.privatesealedclassEventRegistration{privatelong_enqueueSeq;privatelong_processedSeq;privatereadonlySemaphoreSlim_drainSemaphore=new(0);publicvoidIncrementProcessed(){longprocessed=Interlocked.Increment(ref_processedSeq);if(processed==Volatile.Read(ref_enqueueSeq)){// Potentially release waiters_drainSemaphore.Release();}}publicasyncTaskDrainAsync(){vartarget=Volatile.Read(ref_enqueueSeq);if(Volatile.Read(ref_processedSeq)>=target)return;// Wait until the semaphore is released, indicating processing might be complete.await_drainSemaphore.WaitAsync();// Re-check if our target has been met.}}
Suggestion importance[1-10]: 8

__

Why: The suggestion correctly identifies the most complex part of the PR—the custom concurrency logic in EventRegistration—and proposes a valid alternative to improve maintainability and robustness, which is a significant quality improvement.

Medium
  • Update

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Refactors the .NET BiDi EventDispatcher to make event dispatch and handler unsubscription more reliable under concurrency, specifically ensuring pending events are processed before handlers are removed (addressing the race described in #16574).

Changes:

  • Replaces the queued event payload with an EventItem that carries the EventRegistration, and simplifies the background processing loop.
  • Introduces per-event-method draining via enqueue/processed sequence tracking so UnsubscribeAsync can wait for already-enqueued events to finish dispatching.
  • Hardens handler management with locked add/remove and snapshotting, and improves error isolation between deserialization vs handler invocation.

Comment threaddotnet/src/webdriver/BiDi/EventDispatcher.cs Outdated
Comment threaddotnet/src/webdriver/BiDi/EventDispatcher.cs Outdated
Comment threaddotnet/src/webdriver/BiDi/EventDispatcher.cs Outdated
CopilotAI review requested due to automatic review settings February 26, 2026 11:57

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 3 comments.

Comment threaddotnet/src/webdriver/BiDi/EventDispatcher.cs
Comment threaddotnet/src/webdriver/BiDi/EventDispatcher.cs Outdated
Comment threaddotnet/src/webdriver/BiDi/EventDispatcher.cs Outdated
@nvborisenko

Copy link
Copy Markdown
MemberAuthor

Seems overcomplicated, but it resolves simple behaviour:

awaitsubscription.DisposeAsync();// waiting until events in the queue are dispatched

Can be simplified if w3c/webdriver-bidi#1032 is resolved via subscriptionId in all events.

@nvborisenko
nvborisenko merged commit 2f71272 into SeleniumHQ:trunkFeb 26, 2026
18 of 19 checks passed
@nvborisenko
nvborisenko deleted the bidi-drain-events-per-method branch February 26, 2026 15:41
nvborisenko added a commit to nvborisenko/selenium-hq that referenced this pull request Mar 5, 2026
nvborisenko added a commit that referenced this pull request Mar 5, 2026
Revert "[dotnet] [bidi] Wait until events are dispatched when unsubscribing (#17142)"
This reverts commit 2f71272.
AutomatedTester pushed a commit that referenced this pull request Mar 11, 2026
Revert "[dotnet] [bidi] Wait until events are dispatched when unsubscribing (#17142)"
This reverts commit 2f71272.
krishnamohan-kothapalli pushed a commit to krishnamohan-kothapalli/selenium that referenced this pull request Mar 18, 2026
…Q#17178)
Revert "[dotnet] [bidi] Wait until events are dispatched when unsubscribing (SeleniumHQ#17142)"
This reverts commit 2f71272.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[🐛 Bug]: [dotnet] BiDi logs potentially missing with race condition

3 participants

@nvborisenko@selenium-ci
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

[dotnet] [bidi] Wait until events are dispatched when unsubscribing - #17142

Merged
nvborisenko merged 10 commits into
SeleniumHQ:trunkfrom
nvborisenko:bidi-drain-events-per-method
Feb 26, 2026
Merged

[dotnet] [bidi] Wait until events are dispatched when unsubscribing#17142
nvborisenko merged 10 commits into
SeleniumHQ:trunkfrom
nvborisenko:bidi-drain-events-per-method

Conversation

@nvborisenko

Copy link
Copy Markdown
Member

🔗 Related Issues

Fix#16574

💥 What does this PR do?

Refactors the event dispatching mechanism in the EventDispatcher class to improve thread safety, reliability, and event handler management. The changes introduce a new EventItem structure, enhance handler registration/removal, and ensure that all pending events are processed before unsubscribing handlers. Error handling and concurrency controls have also been improved.

🔄 Types of changes

  • Bug fix (backwards compatible)

CopilotAI review requested due to automatic review settings February 26, 2026 11:23
@qodo-code-review

Copy link
Copy Markdown
Contributor

PR Type

Bug fix


Description

  • Refactors event dispatching to ensure all pending events are processed before unsubscribing handlers

  • Introduces sequence-based draining mechanism to track enqueued and processed events per registration

  • Improves thread safety with proper locking and atomic operations for handler management

  • Enhances error handling with separate try-catch blocks for deserialization and handler invocation

  • Adds handler before remote subscription to prevent race conditions on subscription failure


File Walkthrough

Relevant files
Bug fix
EventDispatcher.cs
Event draining mechanism and thread-safe handler management

dotnet/src/webdriver/BiDi/EventDispatcher.cs

  • Replaced PendingEvent struct with EventItem record containing
    registration reference instead of TypeInfo
  • Refactored EventRegistration class with sequence-based draining
    mechanism using _enqueueSeq and _processedSeq counters
  • Added DrainAsync() method to wait until all pending events for a
    registration are processed before handler removal
  • Introduced thread-safe handler management with AddHandler(),
    RemoveHandler(), and GetHandlersSnapshot() methods
  • Simplified task creation by replacing TaskFactory with direct
    Task.Run() call
  • Enhanced error handling with separate exception handling for
    deserialization and handler invocation
  • Added handler registration before remote subscription with rollback on
    failure
+111/-23

@selenium-ciselenium-ci added the C-dotnet .NET Bindings label Feb 26, 2026
@qodo-code-review

qodo-code-reviewBot commented Feb 26, 2026

Copy link
Copy Markdown
Contributor

PR Compliance Guide 🔍

Below is a summary of compliance checks for this PR:

Security Compliance
🟢
No security concerns identified No security vulnerabilities detected by AI analysis. Human verification advised for critical code.
Ticket Compliance
🟡
🎫 #16574
🟢Prevent race conditions where events are still in-flight / queued when the subscription is
disposed or the connection is closing.
Ensure BiDi log events (e.g., console logs) are reliably delivered to the registered
OnEntryAddedAsync handler without requiring a time-based delay.
Codebase Duplication Compliance
Codebase context is not defined

Follow the guide to enable codebase context checks.

Custom Compliance
🟢
Generic: Comprehensive Audit Trails

Objective: To create a detailed and reliable record of critical system actions for security analysis
and compliance.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Meaningful Naming and Self-Documenting Code

Objective: Ensure all identifiers clearly express their purpose and intent, making code
self-documenting

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Error Handling

Objective: To prevent the leakage of sensitive system information through error messages while
providing sufficient detail for internal debugging.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Security-First Input Validation and Data Handling

Objective: Ensure all data inputs are validated, sanitized, and handled securely to prevent
vulnerabilities

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

🔴
Generic: Robust Error Handling and Edge Case Management

Objective: Ensure comprehensive error handling that provides meaningful context and graceful
degradation

Status:
Silent event drop: The result of _pendingEvents.Writer.TryWrite(...) is ignored, which can silently drop
events (e.g., after Complete() during disposal) without any fallback or logging.

Referred Code
if(_events.TryGetValue(method,outvarregistration)){registration.IncrementEnqueued();_pendingEvents.Writer.TryWrite(newEventItem(jsonUtf8Bytes,bidi,registration));}

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Logging Practices

Objective: To ensure logs are useful for debugging and auditing without exposing sensitive
information like PII, PHI, or cardholder data.

Status:
Exception details logged: Error logs interpolate Exception objects (e.g., $"... {ex}"), which may include
sensitive data from exception messages/stack traces depending on upstream content and
logging sinks.

Referred Code
catch(Exceptionex){if(_logger.IsEnabled(LogEventLevel.Error)){_logger.Error($"Unhandled error processing BiDi event handler: {ex}");}}}}catch(Exceptionex){if(_logger.IsEnabled(LogEventLevel.Error)){_logger.Error($"Unhandled error deserializing BiDi event: {ex}");}

Learn more about managing compliance generic rules or creating your own custom rules

  • Update
Compliance status legend 🟢 - Fully Compliant
🟡 - Partial Compliant
🔴 - Not Compliant
⚪ - Requires Further Human Verification
🏷️ - Compliance label

@qodo-code-review

qodo-code-reviewBot commented Feb 26, 2026

Copy link
Copy Markdown
Contributor

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion Impact
Possible issue
Increment only on successful enqueue
Suggestion Impact:EnqueueEvent was changed to call IncrementEnqueued() only when _pendingEvents.Writer.TryWrite(...) returns true, aligning the enqueue counter with successful channel writes; additionally added a warning log when TryWrite fails.

code diff:

@@ -85,8 +85,17 @@
{
if (_events.TryGetValue(method, out var registration))
{
- registration.IncrementEnqueued();- _pendingEvents.Writer.TryWrite(new EventItem(jsonUtf8Bytes, bidi, registration));+ if (_pendingEvents.Writer.TryWrite(new EventItem(jsonUtf8Bytes, bidi, registration)))+ {+ registration.IncrementEnqueued();+ }+ else+ {+ if (_logger.IsEnabled(LogEventLevel.Warn))+ {+ _logger.Warn($"Failed to enqueue BiDi event with method '{method}' for processing. Event will be ignored.");+ }+ }
}

In EnqueueEvent, only call registration.IncrementEnqueued() if
_pendingEvents.Writer.TryWrite(...) returns true to prevent a potential deadlock
in DrainAsync.

dotnet/src/webdriver/BiDi/EventDispatcher.cs [88-89]

-registration.IncrementEnqueued();-_pendingEvents.Writer.TryWrite(new EventItem(jsonUtf8Bytes, bidi, registration));+if (_pendingEvents.Writer.TryWrite(new EventItem(jsonUtf8Bytes, bidi, registration)))+{+ registration.IncrementEnqueued();+}

[Suggestion processed]

Suggestion importance[1-10]: 9

__

Why: This suggestion fixes a critical bug where DrainAsync could hang indefinitely if TryWrite fails, by correctly synchronizing the enqueue counter with the channel write operation.

High
Prevent race condition during subscription

To prevent a race condition, move the registration.AddHandler(eventHandler) call
to after the _sessionProvider().SubscribeAsync call succeeds in the
SubscribeAsync method.

dotnet/src/webdriver/BiDi/EventDispatcher.cs [51-69]

 public async Task<Subscription> SubscribeAsync<TEventArgs>(string eventName, EventHandler eventHandler, SubscriptionOptions? options, JsonTypeInfo<TEventArgs> jsonTypeInfo, CancellationToken cancellationToken)
where TEventArgs : EventArgs
{
var registration = _events.GetOrAdd(eventName, _ => new EventRegistration(jsonTypeInfo));
- registration.AddHandler(eventHandler);-
try
{
var subscribeResult = await _sessionProvider().SubscribeAsync([eventName], new() { Contexts = options?.Contexts, UserContexts = options?.UserContexts }, cancellationToken).ConfigureAwait(false);
++ registration.AddHandler(eventHandler);
return new Subscription(subscribeResult.Subscription, this, eventHandler);
}
catch
{
- registration.RemoveHandler(eventHandler);+ // No need to remove the handler as it was never added in case of an exception.
throw;
}
}
  • Apply / Chat
Suggestion importance[1-10]: 8

__

Why: The suggestion correctly identifies a race condition where an event handler could process an event before its subscription is confirmed, improving the atomicity and correctness of the subscription logic.

Medium
Propagate cancellation token
Suggestion Impact:Updated the UnsubscribeAsync flow to pass cancellationToken into DrainAsync, and changed DrainAsync to accept and honor a CancellationToken by returning a cancellable task continuation.

code diff:

@@ -75,7 +75,7 @@
await _sessionProvider().UnsubscribeAsync([subscription.SubscriptionId], null, cancellationToken).ConfigureAwait(false);
// Wait until all pending events for this method are dispatched
- await registration.DrainAsync().ConfigureAwait(false);+ await registration.DrainAsync(cancellationToken).ConfigureAwait(false);
registration.RemoveHandler(subscription.EventHandler);
}
@@ -199,7 +199,7 @@
}
}
- public Task DrainAsync()+ public Task DrainAsync(CancellationToken cancellationToken)
{
lock (_drainLock)
{
@@ -218,7 +218,13 @@
return Task.CompletedTask;
}
- return tcs.Task;+ if (!cancellationToken.CanBeCanceled) return tcs.Task;++ return tcs.Task.ContinueWith(+ static _ => { },+ cancellationToken,+ TaskContinuationOptions.None,+ TaskScheduler.Default);
}

Propagate the cancellationToken from UnsubscribeAsync to registration.DrainAsync
to allow the draining operation to be cancelled.

dotnet/src/webdriver/BiDi/EventDispatcher.cs [78]

-await registration.DrainAsync().ConfigureAwait(false);+await registration.DrainAsync(cancellationToken).ConfigureAwait(false);

[Suggestion processed]

Suggestion importance[1-10]: 7

__

Why: The suggestion correctly proposes propagating the cancellationToken to DrainAsync to allow cancellation of the wait, which improves responsiveness and resource management.

Medium
High-level
Consider simplifying the custom synchronization logic

The custom synchronization logic in EventRegistration for DrainAsync is complex
and error-prone. It should be simplified using higher-level .NET concurrency
primitives for better readability and robustness.

Examples:

dotnet/src/webdriver/BiDi/EventDispatcher.cs [154-224]
privatesealedclassEventRegistration(JsonTypeInfotypeInfo){privatelong_enqueueSeq;privatelong_processedSeq;privatereadonlyobject_drainLock=new();privatereadonlyList<EventHandler>_handlers=[];privateList<(longTargetSeq,TaskCompletionSource<bool>Tcs)>?_drainWaiters;publicJsonTypeInfoTypeInfo{get;}=typeInfo;
...(clipped 61lines)

Solution Walkthrough:

Before:

privatesealedclassEventRegistration{privatelong_enqueueSeq;privatelong_processedSeq;privatereadonlyobject_drainLock=new();privateList<(longTargetSeq,TaskCompletionSource<bool>Tcs)>?_drainWaiters;publicvoidIncrementProcessed(){varprocessed=Interlocked.Increment(ref_processedSeq);lock(_drainLock){// ... logic to check waiters and complete their tasks ...}}publicTaskDrainAsync(){lock(_drainLock){vartarget=Volatile.Read(ref_enqueueSeq);if(Volatile.Read(ref_processedSeq)>=target)returnTask.CompletedTask;vartcs=newTaskCompletionSource<bool>(...);_drainWaiters??=[];_drainWaiters.Add((target,tcs));// ... double-check logic ...returntcs.Task;}}}

After:

// Using a SemaphoreSlim to signal completion could simplify the logic.privatesealedclassEventRegistration{privatelong_enqueueSeq;privatelong_processedSeq;privatereadonlySemaphoreSlim_drainSemaphore=new(0);publicvoidIncrementProcessed(){longprocessed=Interlocked.Increment(ref_processedSeq);if(processed==Volatile.Read(ref_enqueueSeq)){// Potentially release waiters_drainSemaphore.Release();}}publicasyncTaskDrainAsync(){vartarget=Volatile.Read(ref_enqueueSeq);if(Volatile.Read(ref_processedSeq)>=target)return;// Wait until the semaphore is released, indicating processing might be complete.await_drainSemaphore.WaitAsync();// Re-check if our target has been met.}}
Suggestion importance[1-10]: 8

__

Why: The suggestion correctly identifies the most complex part of the PR—the custom concurrency logic in EventRegistration—and proposes a valid alternative to improve maintainability and robustness, which is a significant quality improvement.

Medium
  • Update

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Refactors the .NET BiDi EventDispatcher to make event dispatch and handler unsubscription more reliable under concurrency, specifically ensuring pending events are processed before handlers are removed (addressing the race described in #16574).

Changes:

  • Replaces the queued event payload with an EventItem that carries the EventRegistration, and simplifies the background processing loop.
  • Introduces per-event-method draining via enqueue/processed sequence tracking so UnsubscribeAsync can wait for already-enqueued events to finish dispatching.
  • Hardens handler management with locked add/remove and snapshotting, and improves error isolation between deserialization vs handler invocation.

Comment threaddotnet/src/webdriver/BiDi/EventDispatcher.cs Outdated
Comment threaddotnet/src/webdriver/BiDi/EventDispatcher.cs Outdated
Comment threaddotnet/src/webdriver/BiDi/EventDispatcher.cs Outdated
CopilotAI review requested due to automatic review settings February 26, 2026 11:57

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 3 comments.

Comment threaddotnet/src/webdriver/BiDi/EventDispatcher.cs
Comment threaddotnet/src/webdriver/BiDi/EventDispatcher.cs Outdated
Comment threaddotnet/src/webdriver/BiDi/EventDispatcher.cs Outdated
@nvborisenko

Copy link
Copy Markdown
MemberAuthor

Seems overcomplicated, but it resolves simple behaviour:

awaitsubscription.DisposeAsync();// waiting until events in the queue are dispatched

Can be simplified if w3c/webdriver-bidi#1032 is resolved via subscriptionId in all events.

@nvborisenko
nvborisenko merged commit 2f71272 into SeleniumHQ:trunkFeb 26, 2026
18 of 19 checks passed
@nvborisenko
nvborisenko deleted the bidi-drain-events-per-method branch February 26, 2026 15:41
nvborisenko added a commit to nvborisenko/selenium-hq that referenced this pull request Mar 5, 2026
nvborisenko added a commit that referenced this pull request Mar 5, 2026
Revert "[dotnet] [bidi] Wait until events are dispatched when unsubscribing (#17142)"
This reverts commit 2f71272.
AutomatedTester pushed a commit that referenced this pull request Mar 11, 2026
Revert "[dotnet] [bidi] Wait until events are dispatched when unsubscribing (#17142)"
This reverts commit 2f71272.
krishnamohan-kothapalli pushed a commit to krishnamohan-kothapalli/selenium that referenced this pull request Mar 18, 2026
…Q#17178)
Revert "[dotnet] [bidi] Wait until events are dispatched when unsubscribing (SeleniumHQ#17142)"
This reverts commit 2f71272.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[🐛 Bug]: [dotnet] BiDi logs potentially missing with race condition

3 participants

@nvborisenko@selenium-ci
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

[dotnet] [bidi] Wait until events are dispatched when unsubscribing - #17142

Merged
nvborisenko merged 10 commits into
SeleniumHQ:trunkfrom
nvborisenko:bidi-drain-events-per-method
Feb 26, 2026
Merged

[dotnet] [bidi] Wait until events are dispatched when unsubscribing#17142
nvborisenko merged 10 commits into
SeleniumHQ:trunkfrom
nvborisenko:bidi-drain-events-per-method

Conversation

@nvborisenko

Copy link
Copy Markdown
Member

🔗 Related Issues

Fix#16574

💥 What does this PR do?

Refactors the event dispatching mechanism in the EventDispatcher class to improve thread safety, reliability, and event handler management. The changes introduce a new EventItem structure, enhance handler registration/removal, and ensure that all pending events are processed before unsubscribing handlers. Error handling and concurrency controls have also been improved.

🔄 Types of changes

  • Bug fix (backwards compatible)

CopilotAI review requested due to automatic review settings February 26, 2026 11:23
@qodo-code-review

Copy link
Copy Markdown
Contributor

PR Type

Bug fix


Description

  • Refactors event dispatching to ensure all pending events are processed before unsubscribing handlers

  • Introduces sequence-based draining mechanism to track enqueued and processed events per registration

  • Improves thread safety with proper locking and atomic operations for handler management

  • Enhances error handling with separate try-catch blocks for deserialization and handler invocation

  • Adds handler before remote subscription to prevent race conditions on subscription failure


File Walkthrough

Relevant files
Bug fix
EventDispatcher.cs
Event draining mechanism and thread-safe handler management

dotnet/src/webdriver/BiDi/EventDispatcher.cs

  • Replaced PendingEvent struct with EventItem record containing
    registration reference instead of TypeInfo
  • Refactored EventRegistration class with sequence-based draining
    mechanism using _enqueueSeq and _processedSeq counters
  • Added DrainAsync() method to wait until all pending events for a
    registration are processed before handler removal
  • Introduced thread-safe handler management with AddHandler(),
    RemoveHandler(), and GetHandlersSnapshot() methods
  • Simplified task creation by replacing TaskFactory with direct
    Task.Run() call
  • Enhanced error handling with separate exception handling for
    deserialization and handler invocation
  • Added handler registration before remote subscription with rollback on
    failure
+111/-23

@selenium-ciselenium-ci added the C-dotnet .NET Bindings label Feb 26, 2026
@qodo-code-review

qodo-code-reviewBot commented Feb 26, 2026

Copy link
Copy Markdown
Contributor

PR Compliance Guide 🔍

Below is a summary of compliance checks for this PR:

Security Compliance
🟢
No security concerns identified No security vulnerabilities detected by AI analysis. Human verification advised for critical code.
Ticket Compliance
🟡
🎫 #16574
🟢Prevent race conditions where events are still in-flight / queued when the subscription is
disposed or the connection is closing.
Ensure BiDi log events (e.g., console logs) are reliably delivered to the registered
OnEntryAddedAsync handler without requiring a time-based delay.
Codebase Duplication Compliance
Codebase context is not defined

Follow the guide to enable codebase context checks.

Custom Compliance
🟢
Generic: Comprehensive Audit Trails

Objective: To create a detailed and reliable record of critical system actions for security analysis
and compliance.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Meaningful Naming and Self-Documenting Code

Objective: Ensure all identifiers clearly express their purpose and intent, making code
self-documenting

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Error Handling

Objective: To prevent the leakage of sensitive system information through error messages while
providing sufficient detail for internal debugging.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Security-First Input Validation and Data Handling

Objective: Ensure all data inputs are validated, sanitized, and handled securely to prevent
vulnerabilities

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

🔴
Generic: Robust Error Handling and Edge Case Management

Objective: Ensure comprehensive error handling that provides meaningful context and graceful
degradation

Status:
Silent event drop: The result of _pendingEvents.Writer.TryWrite(...) is ignored, which can silently drop
events (e.g., after Complete() during disposal) without any fallback or logging.

Referred Code
if(_events.TryGetValue(method,outvarregistration)){registration.IncrementEnqueued();_pendingEvents.Writer.TryWrite(newEventItem(jsonUtf8Bytes,bidi,registration));}

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Logging Practices

Objective: To ensure logs are useful for debugging and auditing without exposing sensitive
information like PII, PHI, or cardholder data.

Status:
Exception details logged: Error logs interpolate Exception objects (e.g., $"... {ex}"), which may include
sensitive data from exception messages/stack traces depending on upstream content and
logging sinks.

Referred Code
catch(Exceptionex){if(_logger.IsEnabled(LogEventLevel.Error)){_logger.Error($"Unhandled error processing BiDi event handler: {ex}");}}}}catch(Exceptionex){if(_logger.IsEnabled(LogEventLevel.Error)){_logger.Error($"Unhandled error deserializing BiDi event: {ex}");}

Learn more about managing compliance generic rules or creating your own custom rules

  • Update
Compliance status legend 🟢 - Fully Compliant
🟡 - Partial Compliant
🔴 - Not Compliant
⚪ - Requires Further Human Verification
🏷️ - Compliance label

@qodo-code-review

qodo-code-reviewBot commented Feb 26, 2026

Copy link
Copy Markdown
Contributor

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion Impact
Possible issue
Increment only on successful enqueue
Suggestion Impact:EnqueueEvent was changed to call IncrementEnqueued() only when _pendingEvents.Writer.TryWrite(...) returns true, aligning the enqueue counter with successful channel writes; additionally added a warning log when TryWrite fails.

code diff:

@@ -85,8 +85,17 @@
{
if (_events.TryGetValue(method, out var registration))
{
- registration.IncrementEnqueued();- _pendingEvents.Writer.TryWrite(new EventItem(jsonUtf8Bytes, bidi, registration));+ if (_pendingEvents.Writer.TryWrite(new EventItem(jsonUtf8Bytes, bidi, registration)))+ {+ registration.IncrementEnqueued();+ }+ else+ {+ if (_logger.IsEnabled(LogEventLevel.Warn))+ {+ _logger.Warn($"Failed to enqueue BiDi event with method '{method}' for processing. Event will be ignored.");+ }+ }
}

In EnqueueEvent, only call registration.IncrementEnqueued() if
_pendingEvents.Writer.TryWrite(...) returns true to prevent a potential deadlock
in DrainAsync.

dotnet/src/webdriver/BiDi/EventDispatcher.cs [88-89]

-registration.IncrementEnqueued();-_pendingEvents.Writer.TryWrite(new EventItem(jsonUtf8Bytes, bidi, registration));+if (_pendingEvents.Writer.TryWrite(new EventItem(jsonUtf8Bytes, bidi, registration)))+{+ registration.IncrementEnqueued();+}

[Suggestion processed]

Suggestion importance[1-10]: 9

__

Why: This suggestion fixes a critical bug where DrainAsync could hang indefinitely if TryWrite fails, by correctly synchronizing the enqueue counter with the channel write operation.

High
Prevent race condition during subscription

To prevent a race condition, move the registration.AddHandler(eventHandler) call
to after the _sessionProvider().SubscribeAsync call succeeds in the
SubscribeAsync method.

dotnet/src/webdriver/BiDi/EventDispatcher.cs [51-69]

 public async Task<Subscription> SubscribeAsync<TEventArgs>(string eventName, EventHandler eventHandler, SubscriptionOptions? options, JsonTypeInfo<TEventArgs> jsonTypeInfo, CancellationToken cancellationToken)
where TEventArgs : EventArgs
{
var registration = _events.GetOrAdd(eventName, _ => new EventRegistration(jsonTypeInfo));
- registration.AddHandler(eventHandler);-
try
{
var subscribeResult = await _sessionProvider().SubscribeAsync([eventName], new() { Contexts = options?.Contexts, UserContexts = options?.UserContexts }, cancellationToken).ConfigureAwait(false);
++ registration.AddHandler(eventHandler);
return new Subscription(subscribeResult.Subscription, this, eventHandler);
}
catch
{
- registration.RemoveHandler(eventHandler);+ // No need to remove the handler as it was never added in case of an exception.
throw;
}
}
  • Apply / Chat
Suggestion importance[1-10]: 8

__

Why: The suggestion correctly identifies a race condition where an event handler could process an event before its subscription is confirmed, improving the atomicity and correctness of the subscription logic.

Medium
Propagate cancellation token
Suggestion Impact:Updated the UnsubscribeAsync flow to pass cancellationToken into DrainAsync, and changed DrainAsync to accept and honor a CancellationToken by returning a cancellable task continuation.

code diff:

@@ -75,7 +75,7 @@
await _sessionProvider().UnsubscribeAsync([subscription.SubscriptionId], null, cancellationToken).ConfigureAwait(false);
// Wait until all pending events for this method are dispatched
- await registration.DrainAsync().ConfigureAwait(false);+ await registration.DrainAsync(cancellationToken).ConfigureAwait(false);
registration.RemoveHandler(subscription.EventHandler);
}
@@ -199,7 +199,7 @@
}
}
- public Task DrainAsync()+ public Task DrainAsync(CancellationToken cancellationToken)
{
lock (_drainLock)
{
@@ -218,7 +218,13 @@
return Task.CompletedTask;
}
- return tcs.Task;+ if (!cancellationToken.CanBeCanceled) return tcs.Task;++ return tcs.Task.ContinueWith(+ static _ => { },+ cancellationToken,+ TaskContinuationOptions.None,+ TaskScheduler.Default);
}

Propagate the cancellationToken from UnsubscribeAsync to registration.DrainAsync
to allow the draining operation to be cancelled.

dotnet/src/webdriver/BiDi/EventDispatcher.cs [78]

-await registration.DrainAsync().ConfigureAwait(false);+await registration.DrainAsync(cancellationToken).ConfigureAwait(false);

[Suggestion processed]

Suggestion importance[1-10]: 7

__

Why: The suggestion correctly proposes propagating the cancellationToken to DrainAsync to allow cancellation of the wait, which improves responsiveness and resource management.

Medium
High-level
Consider simplifying the custom synchronization logic

The custom synchronization logic in EventRegistration for DrainAsync is complex
and error-prone. It should be simplified using higher-level .NET concurrency
primitives for better readability and robustness.

Examples:

dotnet/src/webdriver/BiDi/EventDispatcher.cs [154-224]
privatesealedclassEventRegistration(JsonTypeInfotypeInfo){privatelong_enqueueSeq;privatelong_processedSeq;privatereadonlyobject_drainLock=new();privatereadonlyList<EventHandler>_handlers=[];privateList<(longTargetSeq,TaskCompletionSource<bool>Tcs)>?_drainWaiters;publicJsonTypeInfoTypeInfo{get;}=typeInfo;
...(clipped 61lines)

Solution Walkthrough:

Before:

privatesealedclassEventRegistration{privatelong_enqueueSeq;privatelong_processedSeq;privatereadonlyobject_drainLock=new();privateList<(longTargetSeq,TaskCompletionSource<bool>Tcs)>?_drainWaiters;publicvoidIncrementProcessed(){varprocessed=Interlocked.Increment(ref_processedSeq);lock(_drainLock){// ... logic to check waiters and complete their tasks ...}}publicTaskDrainAsync(){lock(_drainLock){vartarget=Volatile.Read(ref_enqueueSeq);if(Volatile.Read(ref_processedSeq)>=target)returnTask.CompletedTask;vartcs=newTaskCompletionSource<bool>(...);_drainWaiters??=[];_drainWaiters.Add((target,tcs));// ... double-check logic ...returntcs.Task;}}}

After:

// Using a SemaphoreSlim to signal completion could simplify the logic.privatesealedclassEventRegistration{privatelong_enqueueSeq;privatelong_processedSeq;privatereadonlySemaphoreSlim_drainSemaphore=new(0);publicvoidIncrementProcessed(){longprocessed=Interlocked.Increment(ref_processedSeq);if(processed==Volatile.Read(ref_enqueueSeq)){// Potentially release waiters_drainSemaphore.Release();}}publicasyncTaskDrainAsync(){vartarget=Volatile.Read(ref_enqueueSeq);if(Volatile.Read(ref_processedSeq)>=target)return;// Wait until the semaphore is released, indicating processing might be complete.await_drainSemaphore.WaitAsync();// Re-check if our target has been met.}}
Suggestion importance[1-10]: 8

__

Why: The suggestion correctly identifies the most complex part of the PR—the custom concurrency logic in EventRegistration—and proposes a valid alternative to improve maintainability and robustness, which is a significant quality improvement.

Medium
  • Update

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Refactors the .NET BiDi EventDispatcher to make event dispatch and handler unsubscription more reliable under concurrency, specifically ensuring pending events are processed before handlers are removed (addressing the race described in #16574).

Changes:

  • Replaces the queued event payload with an EventItem that carries the EventRegistration, and simplifies the background processing loop.
  • Introduces per-event-method draining via enqueue/processed sequence tracking so UnsubscribeAsync can wait for already-enqueued events to finish dispatching.
  • Hardens handler management with locked add/remove and snapshotting, and improves error isolation between deserialization vs handler invocation.

Comment threaddotnet/src/webdriver/BiDi/EventDispatcher.cs Outdated
Comment threaddotnet/src/webdriver/BiDi/EventDispatcher.cs Outdated
Comment threaddotnet/src/webdriver/BiDi/EventDispatcher.cs Outdated
CopilotAI review requested due to automatic review settings February 26, 2026 11:57

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 3 comments.

Comment threaddotnet/src/webdriver/BiDi/EventDispatcher.cs
Comment threaddotnet/src/webdriver/BiDi/EventDispatcher.cs Outdated
Comment threaddotnet/src/webdriver/BiDi/EventDispatcher.cs Outdated
@nvborisenko

Copy link
Copy Markdown
MemberAuthor

Seems overcomplicated, but it resolves simple behaviour:

awaitsubscription.DisposeAsync();// waiting until events in the queue are dispatched

Can be simplified if w3c/webdriver-bidi#1032 is resolved via subscriptionId in all events.

@nvborisenko
nvborisenko merged commit 2f71272 into SeleniumHQ:trunkFeb 26, 2026
18 of 19 checks passed
@nvborisenko
nvborisenko deleted the bidi-drain-events-per-method branch February 26, 2026 15:41
nvborisenko added a commit to nvborisenko/selenium-hq that referenced this pull request Mar 5, 2026
nvborisenko added a commit that referenced this pull request Mar 5, 2026
Revert "[dotnet] [bidi] Wait until events are dispatched when unsubscribing (#17142)"
This reverts commit 2f71272.
AutomatedTester pushed a commit that referenced this pull request Mar 11, 2026
Revert "[dotnet] [bidi] Wait until events are dispatched when unsubscribing (#17142)"
This reverts commit 2f71272.
krishnamohan-kothapalli pushed a commit to krishnamohan-kothapalli/selenium that referenced this pull request Mar 18, 2026
…Q#17178)
Revert "[dotnet] [bidi] Wait until events are dispatched when unsubscribing (SeleniumHQ#17142)"
This reverts commit 2f71272.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[🐛 Bug]: [dotnet] BiDi logs potentially missing with race condition

3 participants

@nvborisenko@selenium-ci
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

[dotnet] [bidi] Wait until events are dispatched when unsubscribing - #17142

Merged
nvborisenko merged 10 commits into
SeleniumHQ:trunkfrom
nvborisenko:bidi-drain-events-per-method
Feb 26, 2026
Merged

[dotnet] [bidi] Wait until events are dispatched when unsubscribing#17142
nvborisenko merged 10 commits into
SeleniumHQ:trunkfrom
nvborisenko:bidi-drain-events-per-method

Conversation

@nvborisenko

Copy link
Copy Markdown
Member

🔗 Related Issues

Fix#16574

💥 What does this PR do?

Refactors the event dispatching mechanism in the EventDispatcher class to improve thread safety, reliability, and event handler management. The changes introduce a new EventItem structure, enhance handler registration/removal, and ensure that all pending events are processed before unsubscribing handlers. Error handling and concurrency controls have also been improved.

🔄 Types of changes

  • Bug fix (backwards compatible)

CopilotAI review requested due to automatic review settings February 26, 2026 11:23
@qodo-code-review

Copy link
Copy Markdown
Contributor

PR Type

Bug fix


Description

  • Refactors event dispatching to ensure all pending events are processed before unsubscribing handlers

  • Introduces sequence-based draining mechanism to track enqueued and processed events per registration

  • Improves thread safety with proper locking and atomic operations for handler management

  • Enhances error handling with separate try-catch blocks for deserialization and handler invocation

  • Adds handler before remote subscription to prevent race conditions on subscription failure


File Walkthrough

Relevant files
Bug fix
EventDispatcher.cs
Event draining mechanism and thread-safe handler management

dotnet/src/webdriver/BiDi/EventDispatcher.cs

  • Replaced PendingEvent struct with EventItem record containing
    registration reference instead of TypeInfo
  • Refactored EventRegistration class with sequence-based draining
    mechanism using _enqueueSeq and _processedSeq counters
  • Added DrainAsync() method to wait until all pending events for a
    registration are processed before handler removal
  • Introduced thread-safe handler management with AddHandler(),
    RemoveHandler(), and GetHandlersSnapshot() methods
  • Simplified task creation by replacing TaskFactory with direct
    Task.Run() call
  • Enhanced error handling with separate exception handling for
    deserialization and handler invocation
  • Added handler registration before remote subscription with rollback on
    failure
+111/-23

@selenium-ciselenium-ci added the C-dotnet .NET Bindings label Feb 26, 2026
@qodo-code-review

qodo-code-reviewBot commented Feb 26, 2026

Copy link
Copy Markdown
Contributor

PR Compliance Guide 🔍

Below is a summary of compliance checks for this PR:

Security Compliance
🟢
No security concerns identified No security vulnerabilities detected by AI analysis. Human verification advised for critical code.
Ticket Compliance
🟡
🎫 #16574
🟢Prevent race conditions where events are still in-flight / queued when the subscription is
disposed or the connection is closing.
Ensure BiDi log events (e.g., console logs) are reliably delivered to the registered
OnEntryAddedAsync handler without requiring a time-based delay.
Codebase Duplication Compliance
Codebase context is not defined

Follow the guide to enable codebase context checks.

Custom Compliance
🟢
Generic: Comprehensive Audit Trails

Objective: To create a detailed and reliable record of critical system actions for security analysis
and compliance.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Meaningful Naming and Self-Documenting Code

Objective: Ensure all identifiers clearly express their purpose and intent, making code
self-documenting

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Error Handling

Objective: To prevent the leakage of sensitive system information through error messages while
providing sufficient detail for internal debugging.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Security-First Input Validation and Data Handling

Objective: Ensure all data inputs are validated, sanitized, and handled securely to prevent
vulnerabilities

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

🔴
Generic: Robust Error Handling and Edge Case Management

Objective: Ensure comprehensive error handling that provides meaningful context and graceful
degradation

Status:
Silent event drop: The result of _pendingEvents.Writer.TryWrite(...) is ignored, which can silently drop
events (e.g., after Complete() during disposal) without any fallback or logging.

Referred Code
if(_events.TryGetValue(method,outvarregistration)){registration.IncrementEnqueued();_pendingEvents.Writer.TryWrite(newEventItem(jsonUtf8Bytes,bidi,registration));}

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Logging Practices

Objective: To ensure logs are useful for debugging and auditing without exposing sensitive
information like PII, PHI, or cardholder data.

Status:
Exception details logged: Error logs interpolate Exception objects (e.g., $"... {ex}"), which may include
sensitive data from exception messages/stack traces depending on upstream content and
logging sinks.

Referred Code
catch(Exceptionex){if(_logger.IsEnabled(LogEventLevel.Error)){_logger.Error($"Unhandled error processing BiDi event handler: {ex}");}}}}catch(Exceptionex){if(_logger.IsEnabled(LogEventLevel.Error)){_logger.Error($"Unhandled error deserializing BiDi event: {ex}");}

Learn more about managing compliance generic rules or creating your own custom rules

  • Update
Compliance status legend 🟢 - Fully Compliant
🟡 - Partial Compliant
🔴 - Not Compliant
⚪ - Requires Further Human Verification
🏷️ - Compliance label

@qodo-code-review

qodo-code-reviewBot commented Feb 26, 2026

Copy link
Copy Markdown
Contributor

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion Impact
Possible issue
Increment only on successful enqueue
Suggestion Impact:EnqueueEvent was changed to call IncrementEnqueued() only when _pendingEvents.Writer.TryWrite(...) returns true, aligning the enqueue counter with successful channel writes; additionally added a warning log when TryWrite fails.

code diff:

@@ -85,8 +85,17 @@
{
if (_events.TryGetValue(method, out var registration))
{
- registration.IncrementEnqueued();- _pendingEvents.Writer.TryWrite(new EventItem(jsonUtf8Bytes, bidi, registration));+ if (_pendingEvents.Writer.TryWrite(new EventItem(jsonUtf8Bytes, bidi, registration)))+ {+ registration.IncrementEnqueued();+ }+ else+ {+ if (_logger.IsEnabled(LogEventLevel.Warn))+ {+ _logger.Warn($"Failed to enqueue BiDi event with method '{method}' for processing. Event will be ignored.");+ }+ }
}

In EnqueueEvent, only call registration.IncrementEnqueued() if
_pendingEvents.Writer.TryWrite(...) returns true to prevent a potential deadlock
in DrainAsync.

dotnet/src/webdriver/BiDi/EventDispatcher.cs [88-89]

-registration.IncrementEnqueued();-_pendingEvents.Writer.TryWrite(new EventItem(jsonUtf8Bytes, bidi, registration));+if (_pendingEvents.Writer.TryWrite(new EventItem(jsonUtf8Bytes, bidi, registration)))+{+ registration.IncrementEnqueued();+}

[Suggestion processed]

Suggestion importance[1-10]: 9

__

Why: This suggestion fixes a critical bug where DrainAsync could hang indefinitely if TryWrite fails, by correctly synchronizing the enqueue counter with the channel write operation.

High
Prevent race condition during subscription

To prevent a race condition, move the registration.AddHandler(eventHandler) call
to after the _sessionProvider().SubscribeAsync call succeeds in the
SubscribeAsync method.

dotnet/src/webdriver/BiDi/EventDispatcher.cs [51-69]

 public async Task<Subscription> SubscribeAsync<TEventArgs>(string eventName, EventHandler eventHandler, SubscriptionOptions? options, JsonTypeInfo<TEventArgs> jsonTypeInfo, CancellationToken cancellationToken)
where TEventArgs : EventArgs
{
var registration = _events.GetOrAdd(eventName, _ => new EventRegistration(jsonTypeInfo));
- registration.AddHandler(eventHandler);-
try
{
var subscribeResult = await _sessionProvider().SubscribeAsync([eventName], new() { Contexts = options?.Contexts, UserContexts = options?.UserContexts }, cancellationToken).ConfigureAwait(false);
++ registration.AddHandler(eventHandler);
return new Subscription(subscribeResult.Subscription, this, eventHandler);
}
catch
{
- registration.RemoveHandler(eventHandler);+ // No need to remove the handler as it was never added in case of an exception.
throw;
}
}
  • Apply / Chat
Suggestion importance[1-10]: 8

__

Why: The suggestion correctly identifies a race condition where an event handler could process an event before its subscription is confirmed, improving the atomicity and correctness of the subscription logic.

Medium
Propagate cancellation token
Suggestion Impact:Updated the UnsubscribeAsync flow to pass cancellationToken into DrainAsync, and changed DrainAsync to accept and honor a CancellationToken by returning a cancellable task continuation.

code diff:

@@ -75,7 +75,7 @@
await _sessionProvider().UnsubscribeAsync([subscription.SubscriptionId], null, cancellationToken).ConfigureAwait(false);
// Wait until all pending events for this method are dispatched
- await registration.DrainAsync().ConfigureAwait(false);+ await registration.DrainAsync(cancellationToken).ConfigureAwait(false);
registration.RemoveHandler(subscription.EventHandler);
}
@@ -199,7 +199,7 @@
}
}
- public Task DrainAsync()+ public Task DrainAsync(CancellationToken cancellationToken)
{
lock (_drainLock)
{
@@ -218,7 +218,13 @@
return Task.CompletedTask;
}
- return tcs.Task;+ if (!cancellationToken.CanBeCanceled) return tcs.Task;++ return tcs.Task.ContinueWith(+ static _ => { },+ cancellationToken,+ TaskContinuationOptions.None,+ TaskScheduler.Default);
}

Propagate the cancellationToken from UnsubscribeAsync to registration.DrainAsync
to allow the draining operation to be cancelled.

dotnet/src/webdriver/BiDi/EventDispatcher.cs [78]

-await registration.DrainAsync().ConfigureAwait(false);+await registration.DrainAsync(cancellationToken).ConfigureAwait(false);

[Suggestion processed]

Suggestion importance[1-10]: 7

__

Why: The suggestion correctly proposes propagating the cancellationToken to DrainAsync to allow cancellation of the wait, which improves responsiveness and resource management.

Medium
High-level
Consider simplifying the custom synchronization logic

The custom synchronization logic in EventRegistration for DrainAsync is complex
and error-prone. It should be simplified using higher-level .NET concurrency
primitives for better readability and robustness.

Examples:

dotnet/src/webdriver/BiDi/EventDispatcher.cs [154-224]
privatesealedclassEventRegistration(JsonTypeInfotypeInfo){privatelong_enqueueSeq;privatelong_processedSeq;privatereadonlyobject_drainLock=new();privatereadonlyList<EventHandler>_handlers=[];privateList<(longTargetSeq,TaskCompletionSource<bool>Tcs)>?_drainWaiters;publicJsonTypeInfoTypeInfo{get;}=typeInfo;
...(clipped 61lines)

Solution Walkthrough:

Before:

privatesealedclassEventRegistration{privatelong_enqueueSeq;privatelong_processedSeq;privatereadonlyobject_drainLock=new();privateList<(longTargetSeq,TaskCompletionSource<bool>Tcs)>?_drainWaiters;publicvoidIncrementProcessed(){varprocessed=Interlocked.Increment(ref_processedSeq);lock(_drainLock){// ... logic to check waiters and complete their tasks ...}}publicTaskDrainAsync(){lock(_drainLock){vartarget=Volatile.Read(ref_enqueueSeq);if(Volatile.Read(ref_processedSeq)>=target)returnTask.CompletedTask;vartcs=newTaskCompletionSource<bool>(...);_drainWaiters??=[];_drainWaiters.Add((target,tcs));// ... double-check logic ...returntcs.Task;}}}

After:

// Using a SemaphoreSlim to signal completion could simplify the logic.privatesealedclassEventRegistration{privatelong_enqueueSeq;privatelong_processedSeq;privatereadonlySemaphoreSlim_drainSemaphore=new(0);publicvoidIncrementProcessed(){longprocessed=Interlocked.Increment(ref_processedSeq);if(processed==Volatile.Read(ref_enqueueSeq)){// Potentially release waiters_drainSemaphore.Release();}}publicasyncTaskDrainAsync(){vartarget=Volatile.Read(ref_enqueueSeq);if(Volatile.Read(ref_processedSeq)>=target)return;// Wait until the semaphore is released, indicating processing might be complete.await_drainSemaphore.WaitAsync();// Re-check if our target has been met.}}
Suggestion importance[1-10]: 8

__

Why: The suggestion correctly identifies the most complex part of the PR—the custom concurrency logic in EventRegistration—and proposes a valid alternative to improve maintainability and robustness, which is a significant quality improvement.

Medium
  • Update

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Refactors the .NET BiDi EventDispatcher to make event dispatch and handler unsubscription more reliable under concurrency, specifically ensuring pending events are processed before handlers are removed (addressing the race described in #16574).

Changes:

  • Replaces the queued event payload with an EventItem that carries the EventRegistration, and simplifies the background processing loop.
  • Introduces per-event-method draining via enqueue/processed sequence tracking so UnsubscribeAsync can wait for already-enqueued events to finish dispatching.
  • Hardens handler management with locked add/remove and snapshotting, and improves error isolation between deserialization vs handler invocation.

Comment threaddotnet/src/webdriver/BiDi/EventDispatcher.cs Outdated
Comment threaddotnet/src/webdriver/BiDi/EventDispatcher.cs Outdated
Comment threaddotnet/src/webdriver/BiDi/EventDispatcher.cs Outdated
CopilotAI review requested due to automatic review settings February 26, 2026 11:57

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 3 comments.

Comment threaddotnet/src/webdriver/BiDi/EventDispatcher.cs
Comment threaddotnet/src/webdriver/BiDi/EventDispatcher.cs Outdated
Comment threaddotnet/src/webdriver/BiDi/EventDispatcher.cs Outdated
@nvborisenko

Copy link
Copy Markdown
MemberAuthor

Seems overcomplicated, but it resolves simple behaviour:

awaitsubscription.DisposeAsync();// waiting until events in the queue are dispatched

Can be simplified if w3c/webdriver-bidi#1032 is resolved via subscriptionId in all events.

@nvborisenko
nvborisenko merged commit 2f71272 into SeleniumHQ:trunkFeb 26, 2026
18 of 19 checks passed
@nvborisenko
nvborisenko deleted the bidi-drain-events-per-method branch February 26, 2026 15:41
nvborisenko added a commit to nvborisenko/selenium-hq that referenced this pull request Mar 5, 2026
nvborisenko added a commit that referenced this pull request Mar 5, 2026
Revert "[dotnet] [bidi] Wait until events are dispatched when unsubscribing (#17142)"
This reverts commit 2f71272.
AutomatedTester pushed a commit that referenced this pull request Mar 11, 2026
Revert "[dotnet] [bidi] Wait until events are dispatched when unsubscribing (#17142)"
This reverts commit 2f71272.
krishnamohan-kothapalli pushed a commit to krishnamohan-kothapalli/selenium that referenced this pull request Mar 18, 2026
…Q#17178)
Revert "[dotnet] [bidi] Wait until events are dispatched when unsubscribing (SeleniumHQ#17142)"
This reverts commit 2f71272.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[🐛 Bug]: [dotnet] BiDi logs potentially missing with race condition

3 participants

@nvborisenko@selenium-ci