Skip to content

Simplify EtwEventSource singleton #226

Open
nikhilNava wants to merge 2 commits intomainfrom
users/rbeglaubter/simplify-etw-event-source
Open

Simplify EtwEventSource singleton #226
nikhilNava wants to merge 2 commits intomainfrom
users/rbeglaubter/simplify-etw-event-source

Conversation

@nikhilNava
Copy link
Copy Markdown
Contributor

@nikhilNava nikhilNava commented Mar 27, 2026

Replace Lazy/lock pattern with simple null-coalescing singleton. Rename Configure() to Initialize(), make ResetForTesting() public so NuGet consumers can use it in their tests.

…-up)

Replace Lazy<T>/lock pattern with simple null-coalescing singleton.
Rename Configure() to Initialize(), make ResetForTesting() public so
NuGet consumers can use it in their tests.
Copilot AI review requested due to automatic review settings March 27, 2026 14:15
@nikhilNava nikhilNava requested a review from a team as a code owner March 27, 2026 14:15
@github-actions
Copy link
Copy Markdown

github-actions bot commented Mar 27, 2026

⚠️ Deprecation Warning: The deny-licenses option is deprecated for possible removal in the next major release. For more information, see issue 997.

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

Snapshot Warnings

⚠️: No snapshots were found for the head SHA 6df8683.
Ensure that dependencies are being submitted on PR branches and consider enabling retry-on-snapshot-warnings. See the documentation for more information and troubleshooting advice.

Scanned Files

None

/// Resets the singleton so tests can exercise <see cref="Initialize"/> on a fresh instance.
/// </summary>
internal static void ResetForTesting()
public static void ResetForTesting()
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This should be internal

@nikhilNava nikhilNava changed the title Simplify EtwEventSource singleton for public consumption (#215 follow… Simplify EtwEventSource singleton Mar 27, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR updates the ETW event source singleton API in Observability.Runtime to be easier for external (NuGet) consumers to configure/reset, including a rename from Configure() to Initialize() and making the test reset hook public.

Changes:

  • Replace the Lazy<T> + lock singleton pattern with a null-coalescing singleton (_instance ??= ...).
  • Rename Configure() to Initialize() (with a default parameter) and update test coverage accordingly.
  • Make ResetForTesting() public and add a new test asserting double-initialization fails.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.

File Description
src/Observability/Runtime/Etw/EtwEventSource.cs Reworks singleton implementation and exposes new public initialization/reset APIs.
src/Tests/Microsoft.Agents.A365.Observability.Runtime.Tests/Etw/EtwEventSourceConfigureTests.cs Updates tests for Initialize() and adds coverage for calling it twice.

Comment on lines 56 to 60
public static void ResetForTesting()
{
lock (_lock)
{
if (_lazy.IsValueCreated)
{
_lazy.Value.Dispose();
}

_throwOnEventWriteErrors = false;
_lazy = new Lazy<EtwEventSource>(CreateInstance);
}
_instance?.Dispose();
_instance = null;
}
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

ResetForTesting() disposes and nulls _instance without synchronization. Since this is now public API, it can be called concurrently with Log/event writes or Initialize, potentially disposing an EventSource in use. Consider making it thread-safe (e.g., Interlocked.Exchange + dispose the returned instance) and/or documenting that it must only be used in single-threaded test setup/teardown.

Copilot uses AI. Check for mistakes.
}
}
}
public static EtwEventSource Log => _instance ??= new EtwEventSource();
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

Log => _instance ??= new EtwEventSource() is not thread-safe. If multiple threads access Log concurrently during startup, more than one EtwEventSource instance can be constructed; EventSource construction can throw when a duplicate name is registered, and even if it doesn't, the extra instance will be leaked/never disposed. Consider using Lazy<EtwEventSource> (thread-safe mode), LazyInitializer.EnsureInitialized, or an Interlocked.CompareExchange-based pattern to guarantee single construction.

Copilot uses AI. Check for mistakes.
Comment on lines +42 to 46
if (_instance != null)
{
if (_lazy.IsValueCreated)
{
throw new InvalidOperationException(
"EtwEventSource has already been created. Configure() must be called before the first access of Log.");
}

_throwOnEventWriteErrors = throwOnEventWriteErrors;
throw new InvalidOperationException(
"EtwEventSource has already been initialized. Initialize() must be called before the first access of Log.");
}
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

Initialize is also not synchronized with Log/other Initialize calls. Two threads can both observe _instance == null and race to create different EtwEventSource instances, or Log can create the default instance while another thread is in Initialize, causing surprising InvalidOperationExceptions. Add synchronization (lock/Interlocked) so initialization and first access are atomic and deterministic.

Copilot uses AI. Check for mistakes.
Comment on lines 40 to 41
public static void Initialize(bool throwOnEventWriteErrors = false)
{
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

Renaming/removing the public Configure(...) API in favor of Initialize(...) is a breaking change for existing NuGet consumers. If backward compatibility is required, consider keeping Configure(bool) as an [Obsolete] forwarding wrapper to Initialize(bool) for at least one release.

Copilot uses AI. Check for mistakes.
Flip Initialize() so throw-on-errors is on by default; consumers pass
suppressThrowOnEventWriteErrors: true to opt out. ResetForTesting()
reverted to internal visibility.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants