From 90a9c6c1b0e9c408316aaa3d7567fa21e6b29204 Mon Sep 17 00:00:00 2001 From: Mikel Blanchard Date: Tue, 16 Jan 2024 10:23:10 -0800 Subject: [PATCH] [api] Fix Baggage context leak issue (#5208) --- .../.publicApi/Stable/PublicAPI.Shipped.txt | 30 ++-- src/OpenTelemetry.Api/Baggage.cs | 133 ++++++++---------- src/OpenTelemetry.Api/CHANGELOG.md | 14 ++ test/OpenTelemetry.Api.Tests/BaggageTests.cs | 92 ++++++++---- 4 files changed, 156 insertions(+), 113 deletions(-) diff --git a/src/OpenTelemetry.Api/.publicApi/Stable/PublicAPI.Shipped.txt b/src/OpenTelemetry.Api/.publicApi/Stable/PublicAPI.Shipped.txt index a87bb701214..2b0fe19c8a2 100644 --- a/src/OpenTelemetry.Api/.publicApi/Stable/PublicAPI.Shipped.txt +++ b/src/OpenTelemetry.Api/.publicApi/Stable/PublicAPI.Shipped.txt @@ -2,13 +2,13 @@ ~abstract OpenTelemetry.Context.Propagation.TextMapPropagator.Extract(OpenTelemetry.Context.Propagation.PropagationContext context, T carrier, System.Func> getter) -> OpenTelemetry.Context.Propagation.PropagationContext ~abstract OpenTelemetry.Context.Propagation.TextMapPropagator.Fields.get -> System.Collections.Generic.ISet ~abstract OpenTelemetry.Context.Propagation.TextMapPropagator.Inject(OpenTelemetry.Context.Propagation.PropagationContext context, T carrier, System.Action setter) -> void -~OpenTelemetry.Baggage.GetBaggage() -> System.Collections.Generic.IReadOnlyDictionary -~OpenTelemetry.Baggage.GetBaggage(string name) -> string -~OpenTelemetry.Baggage.GetEnumerator() -> System.Collections.Generic.Dictionary.Enumerator -~OpenTelemetry.Baggage.RemoveBaggage(string name) -> OpenTelemetry.Baggage -~OpenTelemetry.Baggage.SetBaggage(params System.Collections.Generic.KeyValuePair[] baggageItems) -> OpenTelemetry.Baggage -~OpenTelemetry.Baggage.SetBaggage(string name, string value) -> OpenTelemetry.Baggage -~OpenTelemetry.Baggage.SetBaggage(System.Collections.Generic.IEnumerable> baggageItems) -> OpenTelemetry.Baggage +OpenTelemetry.Baggage.GetBaggage() -> System.Collections.Generic.IReadOnlyDictionary! +OpenTelemetry.Baggage.GetBaggage(string! name) -> string? +OpenTelemetry.Baggage.GetEnumerator() -> System.Collections.Generic.Dictionary.Enumerator +OpenTelemetry.Baggage.RemoveBaggage(string! name) -> OpenTelemetry.Baggage +OpenTelemetry.Baggage.SetBaggage(params System.Collections.Generic.KeyValuePair[]! baggageItems) -> OpenTelemetry.Baggage +OpenTelemetry.Baggage.SetBaggage(string! name, string? value) -> OpenTelemetry.Baggage +OpenTelemetry.Baggage.SetBaggage(System.Collections.Generic.IEnumerable>! baggageItems) -> OpenTelemetry.Baggage ~OpenTelemetry.Context.AsyncLocalRuntimeContextSlot.AsyncLocalRuntimeContextSlot(string name) -> void ~OpenTelemetry.Context.AsyncLocalRuntimeContextSlot.Value.get -> object ~OpenTelemetry.Context.AsyncLocalRuntimeContextSlot.Value.set -> void @@ -20,7 +20,7 @@ ~OpenTelemetry.Context.ThreadLocalRuntimeContextSlot.ThreadLocalRuntimeContextSlot(string name) -> void ~OpenTelemetry.Context.ThreadLocalRuntimeContextSlot.Value.get -> object ~OpenTelemetry.Context.ThreadLocalRuntimeContextSlot.Value.set -> void -~override OpenTelemetry.Baggage.Equals(object obj) -> bool +override OpenTelemetry.Baggage.Equals(object? obj) -> bool ~override OpenTelemetry.Context.Propagation.B3Propagator.Extract(OpenTelemetry.Context.Propagation.PropagationContext context, T carrier, System.Func> getter) -> OpenTelemetry.Context.Propagation.PropagationContext ~override OpenTelemetry.Context.Propagation.B3Propagator.Fields.get -> System.Collections.Generic.ISet ~override OpenTelemetry.Context.Propagation.B3Propagator.Inject(OpenTelemetry.Context.Propagation.PropagationContext context, T carrier, System.Action setter) -> void @@ -34,13 +34,13 @@ ~override OpenTelemetry.Context.Propagation.TraceContextPropagator.Extract(OpenTelemetry.Context.Propagation.PropagationContext context, T carrier, System.Func> getter) -> OpenTelemetry.Context.Propagation.PropagationContext ~override OpenTelemetry.Context.Propagation.TraceContextPropagator.Fields.get -> System.Collections.Generic.ISet ~override OpenTelemetry.Context.Propagation.TraceContextPropagator.Inject(OpenTelemetry.Context.Propagation.PropagationContext context, T carrier, System.Action setter) -> void -~static OpenTelemetry.Baggage.Create(System.Collections.Generic.Dictionary baggageItems = null) -> OpenTelemetry.Baggage -~static OpenTelemetry.Baggage.GetBaggage(OpenTelemetry.Baggage baggage = default(OpenTelemetry.Baggage)) -> System.Collections.Generic.IReadOnlyDictionary -~static OpenTelemetry.Baggage.GetBaggage(string name, OpenTelemetry.Baggage baggage = default(OpenTelemetry.Baggage)) -> string -~static OpenTelemetry.Baggage.GetEnumerator(OpenTelemetry.Baggage baggage = default(OpenTelemetry.Baggage)) -> System.Collections.Generic.Dictionary.Enumerator -~static OpenTelemetry.Baggage.RemoveBaggage(string name, OpenTelemetry.Baggage baggage = default(OpenTelemetry.Baggage)) -> OpenTelemetry.Baggage -~static OpenTelemetry.Baggage.SetBaggage(string name, string value, OpenTelemetry.Baggage baggage = default(OpenTelemetry.Baggage)) -> OpenTelemetry.Baggage -~static OpenTelemetry.Baggage.SetBaggage(System.Collections.Generic.IEnumerable> baggageItems, OpenTelemetry.Baggage baggage = default(OpenTelemetry.Baggage)) -> OpenTelemetry.Baggage +static OpenTelemetry.Baggage.Create(System.Collections.Generic.Dictionary? baggageItems = null) -> OpenTelemetry.Baggage +static OpenTelemetry.Baggage.GetBaggage(OpenTelemetry.Baggage baggage = default(OpenTelemetry.Baggage)) -> System.Collections.Generic.IReadOnlyDictionary! +static OpenTelemetry.Baggage.GetBaggage(string! name, OpenTelemetry.Baggage baggage = default(OpenTelemetry.Baggage)) -> string? +static OpenTelemetry.Baggage.GetEnumerator(OpenTelemetry.Baggage baggage = default(OpenTelemetry.Baggage)) -> System.Collections.Generic.Dictionary.Enumerator +static OpenTelemetry.Baggage.RemoveBaggage(string! name, OpenTelemetry.Baggage baggage = default(OpenTelemetry.Baggage)) -> OpenTelemetry.Baggage +static OpenTelemetry.Baggage.SetBaggage(string! name, string? value, OpenTelemetry.Baggage baggage = default(OpenTelemetry.Baggage)) -> OpenTelemetry.Baggage +static OpenTelemetry.Baggage.SetBaggage(System.Collections.Generic.IEnumerable>! baggageItems, OpenTelemetry.Baggage baggage = default(OpenTelemetry.Baggage)) -> OpenTelemetry.Baggage ~static OpenTelemetry.Context.Propagation.Propagators.DefaultTextMapPropagator.get -> OpenTelemetry.Context.Propagation.TextMapPropagator ~static OpenTelemetry.Context.RuntimeContext.ContextSlotType.get -> System.Type ~static OpenTelemetry.Context.RuntimeContext.ContextSlotType.set -> void diff --git a/src/OpenTelemetry.Api/Baggage.cs b/src/OpenTelemetry.Api/Baggage.cs index 09e19a6ecf7..f3f09abf5ff 100644 --- a/src/OpenTelemetry.Api/Baggage.cs +++ b/src/OpenTelemetry.Api/Baggage.cs @@ -1,7 +1,9 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -using System.Diagnostics.CodeAnalysis; +#nullable enable + +using System.Diagnostics; using OpenTelemetry.Context; using OpenTelemetry.Internal; @@ -15,10 +17,12 @@ namespace OpenTelemetry; /// public readonly struct Baggage : IEquatable { - private static readonly RuntimeContextSlot RuntimeContextSlot = RuntimeContext.RegisterSlot("otel.baggage"); - private static readonly Dictionary EmptyBaggage = new(); + private static readonly RuntimeContextSlot> RuntimeContextSlot + = RuntimeContext.RegisterSlot>("otel.baggage"); + + private static readonly Dictionary EmptyBaggage = []; - private readonly Dictionary baggage; + private readonly Dictionary? baggage; /// /// Initializes a new instance of the struct. @@ -26,6 +30,8 @@ namespace OpenTelemetry; /// Baggage key/value pairs. internal Baggage(Dictionary baggage) { + Debug.Assert(baggage != null, "baggage was null"); + this.baggage = baggage; } @@ -59,8 +65,8 @@ internal Baggage(Dictionary baggage) /// public static Baggage Current { - get => RuntimeContextSlot.Get()?.Baggage ?? default; - set => EnsureBaggageHolder().Baggage = value; + get => new(RuntimeContextSlot.Get() ?? EmptyBaggage); + set => RuntimeContextSlot.Set(value.baggage ?? EmptyBaggage); } /// @@ -87,7 +93,7 @@ public static Baggage Current /// /// Baggage key/value pairs. /// . - public static Baggage Create(Dictionary baggageItems = null) + public static Baggage Create(Dictionary? baggageItems = null) { if (baggageItems == null) { @@ -97,13 +103,19 @@ public static Baggage Create(Dictionary baggageItems = null) Dictionary baggageCopy = new Dictionary(baggageItems.Count, StringComparer.OrdinalIgnoreCase); foreach (KeyValuePair baggageItem in baggageItems) { - if (string.IsNullOrEmpty(baggageItem.Value)) + if (string.IsNullOrEmpty(baggageItem.Key)) { - baggageCopy.Remove(baggageItem.Key); continue; } - baggageCopy[baggageItem.Key] = baggageItem.Value; + if (string.IsNullOrEmpty(baggageItem.Value)) + { + baggageCopy.Remove(baggageItem.Key); + } + else + { + baggageCopy[baggageItem.Key] = baggageItem.Value; + } } return new Baggage(baggageCopy); @@ -114,7 +126,6 @@ public static Baggage Create(Dictionary baggageItems = null) /// /// Optional . is used if not specified. /// Baggage key/value pairs. - [SuppressMessage("roslyn", "RS0026", Justification = "TODO: fix APIs that violate the backcompt requirement - multiple overloads with optional parameters: https://github.com/dotnet/roslyn/blob/main/docs/Adding%20Optional%20Parameters%20in%20Public%20API.md.")] public static IReadOnlyDictionary GetBaggage(Baggage baggage = default) => baggage == default ? Current.GetBaggage() : baggage.GetBaggage(); @@ -132,8 +143,7 @@ public static Dictionary.Enumerator GetEnumerator(Baggage baggag /// Baggage item name. /// Optional . is used if not specified. /// Baggage item or if nothing was found. - [SuppressMessage("roslyn", "RS0026", Justification = "TODO: fix APIs that violate the backcompt requirement - multiple overloads with optional parameters: https://github.com/dotnet/roslyn/blob/main/docs/Adding%20Optional%20Parameters%20in%20Public%20API.md.")] - public static string GetBaggage(string name, Baggage baggage = default) + public static string? GetBaggage(string name, Baggage baggage = default) => baggage == default ? Current.GetBaggage(name) : baggage.GetBaggage(name); /// @@ -144,16 +154,11 @@ public static string GetBaggage(string name, Baggage baggage = default) /// Optional . is used if not specified. /// New containing the key/value pair. /// Note: The returned will be set as the new instance. - [SuppressMessage("roslyn", "RS0026", Justification = "TODO: fix APIs that violate the backcompt requirement - multiple overloads with optional parameters: https://github.com/dotnet/roslyn/blob/main/docs/Adding%20Optional%20Parameters%20in%20Public%20API.md.")] - public static Baggage SetBaggage(string name, string value, Baggage baggage = default) + public static Baggage SetBaggage(string name, string? value, Baggage baggage = default) { - var baggageHolder = EnsureBaggageHolder(); - lock (baggageHolder) - { - return baggageHolder.Baggage = baggage == default - ? baggageHolder.Baggage.SetBaggage(name, value) - : baggage.SetBaggage(name, value); - } + return Current = baggage == default + ? Current.SetBaggage(name, value) + : baggage.SetBaggage(name, value); } /// @@ -163,16 +168,11 @@ public static Baggage SetBaggage(string name, string value, Baggage baggage = de /// Optional . is used if not specified. /// New containing the key/value pair. /// Note: The returned will be set as the new instance. - [SuppressMessage("roslyn", "RS0026", Justification = "TODO: fix APIs that violate the backcompt requirement - multiple overloads with optional parameters: https://github.com/dotnet/roslyn/blob/main/docs/Adding%20Optional%20Parameters%20in%20Public%20API.md.")] - public static Baggage SetBaggage(IEnumerable> baggageItems, Baggage baggage = default) + public static Baggage SetBaggage(IEnumerable> baggageItems, Baggage baggage = default) { - var baggageHolder = EnsureBaggageHolder(); - lock (baggageHolder) - { - return baggageHolder.Baggage = baggage == default - ? baggageHolder.Baggage.SetBaggage(baggageItems) - : baggage.SetBaggage(baggageItems); - } + return Current = baggage == default + ? Current.SetBaggage(baggageItems) + : baggage.SetBaggage(baggageItems); } /// @@ -184,13 +184,9 @@ public static Baggage SetBaggage(IEnumerable> bagga /// Note: The returned will be set as the new instance. public static Baggage RemoveBaggage(string name, Baggage baggage = default) { - var baggageHolder = EnsureBaggageHolder(); - lock (baggageHolder) - { - return baggageHolder.Baggage = baggage == default - ? baggageHolder.Baggage.RemoveBaggage(name) - : baggage.RemoveBaggage(name); - } + return Current = baggage == default + ? Current.RemoveBaggage(name) + : baggage.RemoveBaggage(name); } /// @@ -201,13 +197,9 @@ public static Baggage RemoveBaggage(string name, Baggage baggage = default) /// Note: The returned will be set as the new instance. public static Baggage ClearBaggage(Baggage baggage = default) { - var baggageHolder = EnsureBaggageHolder(); - lock (baggageHolder) - { - return baggageHolder.Baggage = baggage == default - ? baggageHolder.Baggage.ClearBaggage() - : baggage.ClearBaggage(); - } + return Current = baggage == default + ? Current.ClearBaggage() + : baggage.ClearBaggage(); } /// @@ -222,11 +214,11 @@ public IReadOnlyDictionary GetBaggage() /// /// Baggage item name. /// Baggage item or if nothing was found. - public string GetBaggage(string name) + public string? GetBaggage(string name) { Guard.ThrowIfNullOrEmpty(name); - return this.baggage != null && this.baggage.TryGetValue(name, out string value) + return this.baggage != null && this.baggage.TryGetValue(name, out var value) ? value : null; } @@ -237,8 +229,10 @@ public string GetBaggage(string name) /// Baggage item name. /// Baggage item value. /// New containing the key/value pair. - public Baggage SetBaggage(string name, string value) + public Baggage SetBaggage(string name, string? value) { + Guard.ThrowIfNullOrEmpty(name); + if (string.IsNullOrEmpty(value)) { return this.RemoveBaggage(name); @@ -247,7 +241,7 @@ public Baggage SetBaggage(string name, string value) return new Baggage( new Dictionary(this.baggage ?? EmptyBaggage, StringComparer.OrdinalIgnoreCase) { - [name] = value, + [name] = value!, }); } @@ -256,17 +250,19 @@ public Baggage SetBaggage(string name, string value) /// /// Baggage key/value pairs. /// New containing the key/value pair. - public Baggage SetBaggage(params KeyValuePair[] baggageItems) - => this.SetBaggage((IEnumerable>)baggageItems); + public Baggage SetBaggage(params KeyValuePair[] baggageItems) + => this.SetBaggage((IEnumerable>)baggageItems); /// /// Returns a new which contains the new key/value pair. /// /// Baggage key/value pairs. /// New containing the key/value pair. - public Baggage SetBaggage(IEnumerable> baggageItems) + public Baggage SetBaggage(IEnumerable> baggageItems) { - if (baggageItems?.Any() != true) + Guard.ThrowIfNull(baggageItems); + + if (!baggageItems.Any()) { return this; } @@ -275,13 +271,18 @@ public Baggage SetBaggage(IEnumerable> baggageItems foreach (var item in baggageItems) { + if (string.IsNullOrEmpty(item.Key)) + { + continue; + } + if (string.IsNullOrEmpty(item.Value)) { newBaggage.Remove(item.Key); } else { - newBaggage[item.Key] = item.Value; + newBaggage[item.Key] = item.Value!; } } @@ -295,7 +296,10 @@ public Baggage SetBaggage(IEnumerable> baggageItems /// New containing the key/value pair. public Baggage RemoveBaggage(string name) { + Guard.ThrowIfNullOrEmpty(name); + var baggage = new Dictionary(this.baggage ?? EmptyBaggage, StringComparer.OrdinalIgnoreCase); + baggage.Remove(name); return new Baggage(baggage); @@ -325,11 +329,11 @@ public bool Equals(Baggage other) return false; } - return baggageIsNullOrEmpty || this.baggage.SequenceEqual(other.baggage); + return baggageIsNullOrEmpty || this.baggage!.SequenceEqual(other.baggage!); } /// - public override bool Equals(object obj) + public override bool Equals(object? obj) => (obj is Baggage baggage) && this.Equals(baggage); /// @@ -349,21 +353,4 @@ public override int GetHashCode() return hash; } - - private static BaggageHolder EnsureBaggageHolder() - { - var baggageHolder = RuntimeContextSlot.Get(); - if (baggageHolder == null) - { - baggageHolder = new BaggageHolder(); - RuntimeContextSlot.Set(baggageHolder); - } - - return baggageHolder; - } - - private sealed class BaggageHolder - { - public Baggage Baggage; - } } diff --git a/src/OpenTelemetry.Api/CHANGELOG.md b/src/OpenTelemetry.Api/CHANGELOG.md index 033331f895e..883750dc406 100644 --- a/src/OpenTelemetry.Api/CHANGELOG.md +++ b/src/OpenTelemetry.Api/CHANGELOG.md @@ -2,6 +2,20 @@ ## Unreleased +* **Breaking change:** `Baggage.Current` behavior changed to align with the + [OpenTelemetry + specification](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/baggage/api.md#overview): + + > The Baggage container MUST be immutable, so that the containing Context also + > remains immutable. + + Changes now create a new + [ExecutionContext](https://learn.microsoft.com/dotnet/api/system.threading.executioncontext) + and will only be available downstream. Previously changes also flowed back to + parent contexts. + + See: [#5208](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5208) + ## 1.7.0 Released 2023-Dec-08 diff --git a/test/OpenTelemetry.Api.Tests/BaggageTests.cs b/test/OpenTelemetry.Api.Tests/BaggageTests.cs index e13aa68a502..eb1f4cb4c83 100644 --- a/test/OpenTelemetry.Api.Tests/BaggageTests.cs +++ b/test/OpenTelemetry.Api.Tests/BaggageTests.cs @@ -1,6 +1,8 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 +#nullable enable + using Xunit; namespace OpenTelemetry.Tests; @@ -27,12 +29,16 @@ public void SetAndGetTest() { var list = new List>(2) { - new KeyValuePair(K1, V1), - new KeyValuePair(K2, V2), + new(K1, V1), + new(K2, V2), }; Baggage.SetBaggage(K1, V1); var baggage = Baggage.Current.SetBaggage(K2, V2); + + Assert.Throws(() => Baggage.SetBaggage(null!, "value")); + Assert.Throws(() => Baggage.SetBaggage(null!)); + Baggage.Current = baggage; Assert.NotEmpty(Baggage.GetBaggage()); @@ -44,7 +50,7 @@ public void SetAndGetTest() Assert.Null(Baggage.GetBaggage("NO_KEY")); Assert.Equal(V2, Baggage.Current.GetBaggage(K2)); - Assert.Throws(() => Baggage.GetBaggage(null)); + Assert.Throws(() => Baggage.GetBaggage(null!)); } [Fact] @@ -52,12 +58,12 @@ public void SetExistingKeyTest() { var list = new List>(2) { - new KeyValuePair(K1, V1), + new(K1, V1), }; - Baggage.Current.SetBaggage(new KeyValuePair(K1, V1)); + Baggage.Current.SetBaggage(new KeyValuePair(K1, V1)); var baggage = Baggage.SetBaggage(K1, V1); - Baggage.SetBaggage(new Dictionary { [K1] = V1 }, baggage); + Baggage.SetBaggage(new Dictionary { [K1] = V1 }, baggage); Assert.Equal(list, Baggage.GetBaggage()); } @@ -78,10 +84,11 @@ public void SetNullValueTest() Assert.Empty(Baggage.SetBaggage(K1, null).GetBaggage()); Baggage.SetBaggage(K1, V1); - Baggage.SetBaggage(new Dictionary + Baggage.SetBaggage(new Dictionary { [K1] = null, [K2] = V2, + [string.Empty] = "ignored", }); Assert.Equal(1, Baggage.Current.Count); Assert.Contains(Baggage.GetBaggage(), kvp => kvp.Key == K2); @@ -94,7 +101,7 @@ public void RemoveTest() var empty2 = Baggage.RemoveBaggage(K1); Assert.True(empty == empty2); - var baggage = Baggage.SetBaggage(new Dictionary + var baggage = Baggage.SetBaggage(new Dictionary { [K1] = V1, [K2] = V2, @@ -112,7 +119,7 @@ public void RemoveTest() [Fact] public void ClearTest() { - var baggage = Baggage.SetBaggage(new Dictionary + var baggage = Baggage.SetBaggage(new Dictionary { [K1] = V1, [K2] = V2, @@ -151,8 +158,8 @@ public void EnumeratorTest() { var list = new List>(2) { - new KeyValuePair(K1, V1), - new KeyValuePair(K2, V2), + new(K1, V1), + new(K2, V2), }; var baggage = Baggage.SetBaggage(K1, V1); @@ -166,7 +173,7 @@ public void EnumeratorTest() var tag2 = enumerator.Current; Assert.False(enumerator.MoveNext()); - Assert.Equal(list, new List> { tag1, tag2 }); + Assert.Equal(list, [tag1, tag2]); Baggage.ClearBaggage(); @@ -207,7 +214,8 @@ public void CreateBaggageTest() ["key2"] = "value2", ["KEY2"] = "VALUE2", ["KEY3"] = "VALUE3", - ["Key3"] = null, + ["Key3"] = null!, + [string.Empty] = "ignored", }); Assert.Equal(2, baggage.Count); @@ -232,7 +240,7 @@ public void EqualityTests() baggage = Baggage.SetBaggage(K1, V1); - var baggage2 = Baggage.SetBaggage(null); + var baggage2 = Baggage.SetBaggage(Enumerable.Empty>()); Assert.Equal(baggage, baggage2); @@ -258,42 +266,76 @@ public void GetHashCodeTests() } [Fact] - public async Task AsyncLocalTests() + public async Task SynchronousFlowTest() { Baggage.SetBaggage("key1", "value1"); + InnerMethod(); + await InnerTask(); Baggage.SetBaggage("key4", "value4"); + // Note: Changes from InnerMethod & InnerTask are observed Assert.Equal(4, Baggage.Current.Count); Assert.Equal("value1", Baggage.GetBaggage("key1")); Assert.Equal("value2", Baggage.GetBaggage("key2")); Assert.Equal("value3", Baggage.GetBaggage("key3")); Assert.Equal("value4", Baggage.GetBaggage("key4")); - static async Task InnerTask() + static void InnerMethod() { Baggage.SetBaggage("key2", "value2"); - await Task.Yield(); + Assert.Equal(2, Baggage.Current.Count); + Assert.Equal("value1", Baggage.GetBaggage("key1")); + Assert.Equal("value2", Baggage.GetBaggage("key2")); + } + static Task InnerTask() + { Baggage.SetBaggage("key3", "value3"); - // key2 & key3 changes don't flow backward automatically + Assert.Equal(3, Baggage.Current.Count); + Assert.Equal("value1", Baggage.GetBaggage("key1")); + Assert.Equal("value2", Baggage.GetBaggage("key2")); + Assert.Equal("value3", Baggage.GetBaggage("key3")); + + return Task.CompletedTask; } } - [Fact] - public void ThreadSafetyTest() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task AsynchronousFlowTest(bool yield) { - Baggage.SetBaggage("rootKey", "rootValue"); // Note: Required to establish a root ExecutionContext containing the BaggageHolder we use as a lock + Baggage.SetBaggage("key1", "value1"); - Parallel.For(0, 100, (i) => + await InnerTask(yield); + + Baggage.SetBaggage("key4", "value4"); + + // Note: Changes from the InnerTask are NOT observed + Assert.Equal(2, Baggage.Current.Count); + Assert.Equal("value1", Baggage.GetBaggage("key1")); + Assert.Equal("value4", Baggage.GetBaggage("key4")); + + static async Task InnerTask(bool yield) { - Baggage.SetBaggage($"key{i}", $"value{i}"); - }); + Baggage.SetBaggage("key2", "value2"); + + if (yield) + { + await Task.Yield(); + } - Assert.Equal(101, Baggage.Current.Count); + Baggage.SetBaggage("key3", "value3"); + + Assert.Equal(3, Baggage.Current.Count); + Assert.Equal("value1", Baggage.GetBaggage("key1")); + Assert.Equal("value2", Baggage.GetBaggage("key2")); + Assert.Equal("value3", Baggage.GetBaggage("key3")); + } } }