0

I have the following code:

Task task = null;
var channel = System.Threading.Channels.Channel.CreateUnbounded<string>();

using (var activity = MyActivitySource.StartActivity("Parent"))
{
    task = Task.Factory.StartNew(async () =>
    {
        //Activity.Current = null;
        var item = await channel.Reader.ReadAsync();
        Console.WriteLine("Task: {0}", Activity.Current?.DisplayName);
    });
}

Console.WriteLine("Current end: {0}", Activity.Current?.DisplayName ?? "(null)");

await channel.Writer.WriteAsync("something");
await task;

I would like to start the task without injecting Activity. I cannot create the task outside the using(var acrivity...).

One option (I suppose) is setting Activity.Current = null at the beginning of the task. Is there an alternative option?

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
Sierrodc
  • 845
  • 6
  • 18
  • 1
    What are you trying to do? Channels are used for pub/sub, and you subscriber task only reads a single item. It's created *inside* the outer activity. A correct subscriber method would be very different. As for OpenTelemetry, a subscriber should have its own activity and the context should probably flow from publisher to subscriber. That way you'll be able to track operations on the message from publisher to subscriber – Panagiotis Kanavos Mar 31 '22 at 10:39
  • The main problem was that the subscriber is Lazy (=> created when needed). In this way the parent Activity is the Activity of the first component that access the Lazy object. – Sierrodc Apr 12 '22 at 08:34

1 Answers1

0

One option (I suppose) is setting Activity.Current = null at the beginning of the task. Is there an alternative option?

My preferred solution would be to move the "processing" code into a separate top-level loop. I.e., move the code currently within StartNew into a completely separate method, wrap it in an await foreach (var item in channel.Reader.ReadAllAsync()), and start that loop at the time you start your channel. Thus essentially making it an ActionBlock of sorts.

You may need to augment your current string item with other values to make that work. I.e., if you need per-item completion, then you can replace string with (string Item, TaskCompletionSource Completion).

There is another solution that may work (but I really recommend using the top-level loop instead): you can suppress the logical context flow (which suppresses all AsyncLocal-style values). But then I would recommend ensuring the task has started on the thread pool thread before letting it escape that block, just in case some code waits synchronously. So that would look like this:

using (var activity = MyActivitySource.StartActivity("Parent"))
{
  using var suppressFlow = ExecutionContext.SuppressFlow();
  var taskStarted = new TaskCompletionSource(TaskCreationOptions.ExecuteContinuationsAsynchronously);
  task = Task.Run(async () =>
  {
    taskStarted.TrySetResult();
    var item = await channel.Reader.ReadAsync();
    Console.WriteLine("Task: {0}", Activity.Current?.DisplayName);
  });
}

But really, I think putting in a "main loop" would be better.

Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • Ok. Putting everything in top-level loop is a solution, but I have many tasks created on demand. The scenario is the following: I have a trading platform where I have to handle CRUD operations for day-ahead hours. Each hour is handled by one task. If no orders for a specific hour => no task is created. so every task is a "lazy task". I have to refactor the code to put everything top-level. I accept your answer (ps: I don't know what taskcompletionsource is used for, maybe it is useless). – Sierrodc Apr 05 '22 at 13:45