Skip to content

Commit f95e178

Browse files
[release/6.0-staging] Fix JsonDocument thread safety. (#92832)
* Fix JsonDocument thread safety. (#76716) Co-authored-by: stoub@microsoft.com * Update ServicingVersion.
1 parent f13337f commit f95e178

File tree

3 files changed

+41
-24
lines changed

3 files changed

+41
-24
lines changed

src/libraries/System.Text.Json/src/System.Text.Json.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@
99
<Nullable>enable</Nullable>
1010
<IncludeInternalObsoleteAttribute>true</IncludeInternalObsoleteAttribute>
1111
<IsPackable>true</IsPackable>
12-
<GeneratePackageOnBuild>false</GeneratePackageOnBuild>
13-
<ServicingVersion>8</ServicingVersion>
12+
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
13+
<ServicingVersion>9</ServicingVersion>
1414
<PackageDescription>Provides high-performance and low-allocating types that serialize objects to JavaScript Object Notation (JSON) text and deserialize JSON text to objects, with UTF-8 support built-in. Also provides types to read and write JSON text encoded as UTF-8, and to create an in-memory document object model (DOM), that is read-only, for random access of the JSON elements within a structured view of the data.
1515

1616
Commonly Used Types:

src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonDocument.cs

Lines changed: 1 addition & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,6 @@ public sealed partial class JsonDocument : IDisposable
3030
private PooledByteBufferWriter? _extraPooledByteBufferWriter;
3131
private bool _hasExtraPooledByteBufferWriter;
3232

33-
private (int, string?) _lastIndexAndString = (-1, null);
34-
3533
internal bool IsDisposable { get; }
3634

3735
/// <summary>
@@ -278,14 +276,6 @@ private ReadOnlyMemory<byte> GetPropertyRawValue(int valueIndex)
278276
{
279277
CheckNotDisposed();
280278

281-
(int lastIdx, string? lastString) = _lastIndexAndString;
282-
283-
if (lastIdx == index)
284-
{
285-
Debug.Assert(lastString != null);
286-
return lastString;
287-
}
288-
289279
DbRow row = _parsedData.Get(index);
290280

291281
JsonTokenType tokenType = row.TokenType;
@@ -300,6 +290,7 @@ private ReadOnlyMemory<byte> GetPropertyRawValue(int valueIndex)
300290
ReadOnlySpan<byte> data = _utf8Json.Span;
301291
ReadOnlySpan<byte> segment = data.Slice(row.Location, row.SizeOrLength);
302292

293+
string lastString;
303294
if (row.HasComplexChildren)
304295
{
305296
int backslash = segment.IndexOf(JsonConstants.BackSlash);
@@ -311,7 +302,6 @@ private ReadOnlyMemory<byte> GetPropertyRawValue(int valueIndex)
311302
}
312303

313304
Debug.Assert(lastString != null);
314-
_lastIndexAndString = (index, lastString);
315305
return lastString;
316306
}
317307

@@ -321,13 +311,6 @@ internal bool TextEquals(int index, ReadOnlySpan<char> otherText, bool isPropert
321311

322312
int matchIndex = isPropertyName ? index - DbRow.Size : index;
323313

324-
(int lastIdx, string? lastString) = _lastIndexAndString;
325-
326-
if (lastIdx == matchIndex)
327-
{
328-
return otherText.SequenceEqual(lastString.AsSpan());
329-
}
330-
331314
byte[]? otherUtf8TextArray = null;
332315

333316
int length = checked(otherText.Length * JsonConstants.MaxExpansionFactorWhileTranscoding);

src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonDocumentTests.cs

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
using System.Linq;
1515
using System.Runtime.InteropServices;
1616
using System.Threading.Tasks;
17+
using System.Threading;
1718

1819
namespace System.Text.Json.Tests
1920
{
@@ -2719,11 +2720,9 @@ public static void ObjectEnumeratorIndependentWalk()
27192720
Assert.Equal(test, property.Value.GetInt32());
27202721
test++;
27212722

2722-
// Subsequent read of the same JsonProperty doesn't allocate a new string
2723-
// (if another property is inspected from the same document that guarantee
2724-
// doesn't hold).
2723+
// Subsequent read of the same JsonProperty should return an equal string
27252724
string propertyName2 = property.Name;
2726-
Assert.Same(propertyName, propertyName2);
2725+
Assert.Equal(propertyName, propertyName2);
27272726
}
27282727

27292728
test = 0;
@@ -3582,6 +3581,41 @@ public static void NameEquals_Empty_Throws()
35823581
}
35833582
}
35843583

3584+
[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))]
3585+
[OuterLoop] // thread-safety / stress test
3586+
public static async Task GetString_ConcurrentUse_ThreadSafe()
3587+
{
3588+
using (JsonDocument doc = JsonDocument.Parse(SR.SimpleObjectJson))
3589+
{
3590+
JsonElement first = doc.RootElement.GetProperty("first");
3591+
JsonElement last = doc.RootElement.GetProperty("last");
3592+
3593+
const int Iters = 10_000;
3594+
using (var gate = new Barrier(2))
3595+
{
3596+
await Task.WhenAll(
3597+
Task.Factory.StartNew(() =>
3598+
{
3599+
gate.SignalAndWait();
3600+
for (int i = 0; i < Iters; i++)
3601+
{
3602+
Assert.Equal("John", first.GetString());
3603+
Assert.True(first.ValueEquals("John"));
3604+
}
3605+
}, CancellationToken.None, TaskCreationOptions.LongRunning, TaskScheduler.Default),
3606+
Task.Factory.StartNew(() =>
3607+
{
3608+
gate.SignalAndWait();
3609+
for (int i = 0; i < Iters; i++)
3610+
{
3611+
Assert.Equal("Smith", last.GetString());
3612+
Assert.True(last.ValueEquals("Smith"));
3613+
}
3614+
}, CancellationToken.None, TaskCreationOptions.LongRunning, TaskScheduler.Default));
3615+
}
3616+
}
3617+
}
3618+
35853619
private static void BuildSegmentedReader(
35863620
out Utf8JsonReader reader,
35873621
ReadOnlyMemory<byte> data,

0 commit comments

Comments
 (0)