From bd0420ae73fbd67368a097f6bd2ea0dae2cb8b68 Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Tue, 11 Oct 2022 10:09:18 +0100 Subject: [PATCH 1/2] Fix JsonDocument thread safety. (#76716) Co-authored-by: stoub@microsoft.com --- .../System/Text/Json/Document/JsonDocument.cs | 19 +-------- .../JsonDocumentTests.cs | 42 +++++++++++++++++-- 2 files changed, 39 insertions(+), 22 deletions(-) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonDocument.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonDocument.cs index 6b075966bb7363..7e7bec82f2c406 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonDocument.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonDocument.cs @@ -30,8 +30,6 @@ public sealed partial class JsonDocument : IDisposable private PooledByteBufferWriter? _extraPooledByteBufferWriter; private bool _hasExtraPooledByteBufferWriter; - private (int, string?) _lastIndexAndString = (-1, null); - internal bool IsDisposable { get; } /// @@ -278,14 +276,6 @@ private ReadOnlyMemory GetPropertyRawValue(int valueIndex) { CheckNotDisposed(); - (int lastIdx, string? lastString) = _lastIndexAndString; - - if (lastIdx == index) - { - Debug.Assert(lastString != null); - return lastString; - } - DbRow row = _parsedData.Get(index); JsonTokenType tokenType = row.TokenType; @@ -300,6 +290,7 @@ private ReadOnlyMemory GetPropertyRawValue(int valueIndex) ReadOnlySpan data = _utf8Json.Span; ReadOnlySpan segment = data.Slice(row.Location, row.SizeOrLength); + string lastString; if (row.HasComplexChildren) { int backslash = segment.IndexOf(JsonConstants.BackSlash); @@ -311,7 +302,6 @@ private ReadOnlyMemory GetPropertyRawValue(int valueIndex) } Debug.Assert(lastString != null); - _lastIndexAndString = (index, lastString); return lastString; } @@ -321,13 +311,6 @@ internal bool TextEquals(int index, ReadOnlySpan otherText, bool isPropert int matchIndex = isPropertyName ? index - DbRow.Size : index; - (int lastIdx, string? lastString) = _lastIndexAndString; - - if (lastIdx == matchIndex) - { - return otherText.SequenceEqual(lastString.AsSpan()); - } - byte[]? otherUtf8TextArray = null; int length = checked(otherText.Length * JsonConstants.MaxExpansionFactorWhileTranscoding); diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonDocumentTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonDocumentTests.cs index 67a203da03b72c..36c2a56f746d19 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonDocumentTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonDocumentTests.cs @@ -14,6 +14,7 @@ using System.Linq; using System.Runtime.InteropServices; using System.Threading.Tasks; +using System.Threading; namespace System.Text.Json.Tests { @@ -2719,11 +2720,9 @@ public static void ObjectEnumeratorIndependentWalk() Assert.Equal(test, property.Value.GetInt32()); test++; - // Subsequent read of the same JsonProperty doesn't allocate a new string - // (if another property is inspected from the same document that guarantee - // doesn't hold). + // Subsequent read of the same JsonProperty should return an equal string string propertyName2 = property.Name; - Assert.Same(propertyName, propertyName2); + Assert.Equal(propertyName, propertyName2); } test = 0; @@ -3582,6 +3581,41 @@ public static void NameEquals_Empty_Throws() } } + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))] + [OuterLoop] // thread-safety / stress test + public static async Task GetString_ConcurrentUse_ThreadSafe() + { + using (JsonDocument doc = JsonDocument.Parse(SR.SimpleObjectJson)) + { + JsonElement first = doc.RootElement.GetProperty("first"); + JsonElement last = doc.RootElement.GetProperty("last"); + + const int Iters = 10_000; + using (var gate = new Barrier(2)) + { + await Task.WhenAll( + Task.Factory.StartNew(() => + { + gate.SignalAndWait(); + for (int i = 0; i < Iters; i++) + { + Assert.Equal("John", first.GetString()); + Assert.True(first.ValueEquals("John")); + } + }, CancellationToken.None, TaskCreationOptions.LongRunning, TaskScheduler.Default), + Task.Factory.StartNew(() => + { + gate.SignalAndWait(); + for (int i = 0; i < Iters; i++) + { + Assert.Equal("Smith", last.GetString()); + Assert.True(last.ValueEquals("Smith")); + } + }, CancellationToken.None, TaskCreationOptions.LongRunning, TaskScheduler.Default)); + } + } + } + private static void BuildSegmentedReader( out Utf8JsonReader reader, ReadOnlyMemory data, From fadec73d315d22e400fa288de21e82f8a01d0e52 Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Fri, 29 Sep 2023 19:56:20 +0100 Subject: [PATCH 2/2] Update ServicingVersion. --- src/libraries/System.Text.Json/src/System.Text.Json.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libraries/System.Text.Json/src/System.Text.Json.csproj b/src/libraries/System.Text.Json/src/System.Text.Json.csproj index f81e9dd4fd84c2..0b046e5c6c0cfc 100644 --- a/src/libraries/System.Text.Json/src/System.Text.Json.csproj +++ b/src/libraries/System.Text.Json/src/System.Text.Json.csproj @@ -9,8 +9,8 @@ enable true true - false - 8 + true + 9 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. Commonly Used Types: