-
Notifications
You must be signed in to change notification settings - Fork 4.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Async thread's Activiy.Current is set incorrectly and cannot be rolled back #110072
Comments
You can see |
This is unfortunately by design because of how async locals work. Also this example is inherently racy. What are you trying to achieve? |
@CuteLeon I am closing the issue as @davidfowl indicated this is by design how the async local work. Feel free to respond with any questions or ask we can help with. |
Hi @davidfowl @tarekgh My actual scenario is like below:
I understand that AsyncLocal can manage independent data in each asynchronous task. But in my scenario, activity "MainInitialization" was propagated to multiple threads/tasks, but only stopped in a single thread/task, which resulted in the inability to correctly roll back the current activity to null in the extra threads/tasks, and it causes new activities starting on extra threads to inherit from the incorrect parent activity. |
And here is a WinForm application to reproduce: using System.Diagnostics;
using Microsoft.Extensions.DependencyInjection;
namespace WinFormsApp1
{
class FormA : Form
{
private FormB formB;
private readonly IServiceProvider provider;
private readonly ActivitySource activitySource;
public FormA(IServiceProvider provider, ActivitySource activitySource)
{
this.Text = "Form A";
this.provider = provider;
this.activitySource = activitySource;
}
protected override void OnShown(EventArgs e)
{
Debug.Print($"{DateTime.Now:HH:mm:ss.fff} [TID={Environment.CurrentManagedThreadId,3}] {this.GetType().Name} Handle created.");
base.OnShown(e);
this.Initialize().ConfigureAwait(false);
}
public async Task Initialize()
{
var mainActivity = this.activitySource.StartActivity("MainActivity");
var form = default(FormB)!;
using var waitHandle = new AutoResetEvent(false);
var threadStart = new ThreadStart(() =>
{
form = provider.GetRequiredService<FormB>();
_ = form.Handle;
waitHandle.Set();
form.Show();
Application.Run(form);
});
var thread = new Thread(threadStart);
thread.SetApartmentState(ApartmentState.STA);
thread.Start();
waitHandle.WaitOne();
this.formB = form;
await Task.WhenAll(new FormB[] { formB }.Select(x => x.Initialize()));
await Task.Delay(100);
mainActivity?.Stop();
}
}
class FormB : Form
{
private readonly ActivitySource activitySource;
public FormB(ActivitySource activitySource)
{
this.Text = "Form B";
this.activitySource = activitySource;
}
protected override void OnHandleCreated(EventArgs e)
{
Debug.Print($"{DateTime.Now:HH:mm:ss.fff} [TID={Environment.CurrentManagedThreadId,3}] {this.GetType().Name} Handle created.");
base.OnHandleCreated(e);
}
public async Task Initialize()
{
var childActivity = this.activitySource.StartActivity("ChildActivity");
await Task.Delay(100);
Debug.Assert(childActivity?.Parent is not null && childActivity.Parent.DisplayName == "MainActivity");
childActivity?.Stop();
}
protected override void OnClick(EventArgs e)
{
Debug.Print($"{DateTime.Now:HH:mm:ss.fff} [TID={Environment.CurrentManagedThreadId,3}] Clicked on FormB >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>");
base.OnClick(e);
var clickActivity = this.activitySource.StartActivity("ClickActivity");
// Program is break here if you click on FormB
Debug.Assert(clickActivity?.Parent is null);
clickActivity?.Stop();
}
}
internal static class Program
{
[STAThread]
static void Main()
{
ApplicationConfiguration.Initialize();
var services = new ServiceCollection();
services.AddSingleton<FormA>();
services.AddSingleton<FormB>();
services.AddSingleton<ActivitySource>(new ActivitySource("MyActivitySource"));
services.AddSingleton<ActivityListener>(new ActivityListener
{
ShouldListenTo = (x) => true,
Sample = (ref ActivityCreationOptions<ActivityContext> options) => ActivitySamplingResult.AllDataAndRecorded,
ActivityStarted = (x) => Debug.Print($"{DateTime.Now:HH:mm:ss.fff} [TID={Environment.CurrentManagedThreadId,3}] ActivityStarted: {x.DisplayName} (Parent={x.Parent?.DisplayName ?? "[null]"}) (Current={Activity.Current?.DisplayName ?? "[null]"})"),
ActivityStopped = (x) => Debug.Print($"{DateTime.Now:HH:mm:ss.fff} [TID={Environment.CurrentManagedThreadId,3}] ActivityStopped: {x.DisplayName} (Parent={x.Parent?.DisplayName ?? "[null]"}) (Current={Activity.Current?.DisplayName ?? "[null]"})"),
});
var provider = services.BuildServiceProvider();
var activitySource = provider.GetRequiredService<ActivitySource>();
var activityListener = provider.GetRequiredService<ActivityListener>();
ActivitySource.AddActivityListener(activityListener);
Activity.CurrentChanged += (s, e) => Debug.Print($"{DateTime.Now:HH:mm:ss.fff} [TID={Environment.CurrentManagedThreadId,3}] \t\tCurrentChanged: {e.Previous?.DisplayName ?? "[null]"} => {e.Current?.DisplayName ?? "[null]"}");
var formA = provider.GetRequiredService<FormA>();
Application.Run(formA);
}
}
} |
And I found that if I move |
If you start the activity before calling |
What are you trying to achieve here? Do you want thread B to use the activity created in form A as its parent? Or do you prefer to always have a |
I hope "the parent to be set only during form B initialization, with any subsequent activity creations having a null parent", because form B initialization is inside of form A initialization. |
If you want to keep your original code, in B initialization after stopping the activity which is created there, you can do |
Description
Hi Team:
I found that Activity.Current is returning a outdated Activity when start new activity in an async scenario, because Activity.Current's type is AsyncLocal and Activity's Parent is passed to mutilple thread but Activity only stopped on single thread.;
I started the ActivityParent in ThreadParent, then call and await an async task on ThreadChild in ActivityA's scope, create the ActivityChild on ThreadChild, ActivityChild is having ActivityParent as its parent, which is good.
Then I stopped ActivityChild on ThreadChild, ThreadChild's Activity.Current is rolled back to ActivityChild's parent (ActicityParent).
Then program go back to ThreadParent and ActivityParent stops on ThreadParent, ThreadParent's Activity.Current rolls back to ActivityParent's parent (null), which is good.
ThreadChild is designed to be alive and waiting for other jobs, but ActivityParent will never stop again on ThreadChild, so that ThreadChild's Activity.Current keeps returnning outdated ActivityParent as new Activity's parent on ThreadChild, and can not be rolled back to null any more.
Reproduction Steps
Expected behavior
Return an Activity object with correct parent from ActivitySource.StartActivity() in async scenario.
Actual behavior
Activity.Current can not roll back correctly once activitywas passed over multiple threads.
Regression?
No response
Known Workarounds
No response
Configuration
.Net 8.0
Windows Server 2022
x64
Other information
No response
The text was updated successfully, but these errors were encountered: