Skip to content

Commit

Permalink
[release/7.0-staging] Fix JsonDocument thread safety. (#92831)
Browse files Browse the repository at this point in the history
* Fix JsonDocument thread safety.

Co-authored-by: [email protected]

* Update ServicingVersion

---------

Co-authored-by: Eirik Tsarpalis <[email protected]>
  • Loading branch information
github-actions[bot] and eiriktsarpalis authored Sep 30, 2023
1 parent 46a293b commit c7425f7
Show file tree
Hide file tree
Showing 3 changed files with 43 additions and 35 deletions.
4 changes: 2 additions & 2 deletions src/libraries/System.Text.Json/src/System.Text.Json.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
<NoWarn>CS8969</NoWarn>
<IncludeInternalObsoleteAttribute>true</IncludeInternalObsoleteAttribute>
<IsPackable>true</IsPackable>
<GeneratePackageOnBuild>false</GeneratePackageOnBuild>
<ServicingVersion>3</ServicingVersion>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<ServicingVersion>4</ServicingVersion>
<!-- This library has been annotated to be AOT safe -->
<EnableAOTAnalyzer>true</EnableAOTAnalyzer>
<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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,6 @@ public sealed partial class JsonDocument : IDisposable
private byte[]? _extraRentedArrayPoolBytes;
private PooledByteBufferWriter? _extraPooledByteBufferWriter;

private (int, string?) _lastIndexAndString = (-1, null);

internal bool IsDisposable { get; }

/// <summary>
Expand Down Expand Up @@ -266,14 +264,6 @@ private ReadOnlyMemory<byte> 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;
Expand All @@ -288,18 +278,9 @@ private ReadOnlyMemory<byte> GetPropertyRawValue(int valueIndex)
ReadOnlySpan<byte> data = _utf8Json.Span;
ReadOnlySpan<byte> segment = data.Slice(row.Location, row.SizeOrLength);

if (row.HasComplexChildren)
{
lastString = JsonReaderHelper.GetUnescapedString(segment);
}
else
{
lastString = JsonReaderHelper.TranscodeHelper(segment);
}

Debug.Assert(lastString != null);
_lastIndexAndString = (index, lastString);
return lastString;
return row.HasComplexChildren
? JsonReaderHelper.GetUnescapedString(segment)
: JsonReaderHelper.TranscodeHelper(segment);
}

internal bool TextEquals(int index, ReadOnlySpan<char> otherText, bool isPropertyName)
Expand All @@ -308,13 +289,6 @@ internal bool TextEquals(int index, ReadOnlySpan<char> 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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using System.Threading;

namespace System.Text.Json.Tests
{
Expand Down Expand Up @@ -2744,11 +2745,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;
Expand Down Expand Up @@ -3607,6 +3606,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<byte> data,
Expand Down

0 comments on commit c7425f7

Please sign in to comment.