Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -389,17 +389,22 @@ private void CompleteValues(

if (resultValue is { IsNullable: false, IsNullOrInvalidated: true })
{
if (entryContext.PropagateNullValues)
// the child that invalidated this object already propagated and reported its null.
if (!resultValue.IsInvalidated)
{
PropagateNullValues(resultValue);
}
else
{
resultValue.SetNullValue();
if (entryContext.PropagateNullValues)
{
PropagateNullValues(resultValue);
}
else
{
resultValue.SetNullValue();
}

entryContext.Result.AddNonNullViolation(context.Path);
}

_completionStatus = ExecutionTaskStatus.Faulted;
entryContext.Result.AddNonNullViolation(context.Path);
ResolverTaskFactory.ReleaseUnregisteredTasks(_taskBuffer, taskBufferStart, _resolverTaskPool);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,17 +49,22 @@ private void CompleteValue(bool success, CancellationToken cancellationToken)

if (resultValue is { IsNullable: false, IsNullOrInvalidated: true })
{
if (_operationContext.PropagateNullValues)
// the child that invalidated this object already propagated and reported its null.
if (!resultValue.IsInvalidated)
{
PropagateNullValues(resultValue);
}
else
{
resultValue.SetNullValue();
if (_operationContext.PropagateNullValues)
{
PropagateNullValues(resultValue);
}
else
{
resultValue.SetNullValue();
}

_operationContext.Result.AddNonNullViolation(_context.Path);
}

_completionStatus = ExecutionTaskStatus.Faulted;
_operationContext.Result.AddNonNullViolation(_context.Path);
ResolverTaskFactory.ReleaseUnregisteredTasks(_taskBuffer, start: 0, objectPool);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -376,7 +376,7 @@ private static void ResolveAndCompleteInline(
var operationContext = context.OperationContext;
var resolverContext = context.ResolverContext;
var groupScope = context.GroupScope;
var taskCount = groupScope is null ? 0 : context.Tasks.Count;
var taskCount = context.Tasks.Count;
var executedSuccessfully = false;
object? resolverResult = null;

Expand Down Expand Up @@ -438,12 +438,22 @@ private static void ResolveAndCompleteInline(
operationContext.ReportError(ex, resolverContext, fieldValue.Path);
}

// the children of an invalidated object can never contribute to the result.
if (fieldValue.IsInvalidated)
{
ReleaseUnregisteredTasks(
context.Tasks,
taskCount,
operationContext.Services.GetRequiredService<ObjectPool<ResolverTask>>());
}

if (groupScope is not null)
{
ReleaseWhenNoChildTasks(context.Tasks, groupScope, selection, taskCount);
}

if (fieldValue is { IsNullable: false, IsNullOrInvalidated: true })
// the child that invalidated this object already propagated and reported its null.
if (fieldValue is { IsNullable: false, IsNullOrInvalidated: true } && !fieldValue.IsInvalidated)
{
if (operationContext.PropagateNullValues)
{
Expand Down
20 changes: 7 additions & 13 deletions src/HotChocolate/Core/src/Types/Text/Json/ResultDocument.cs
Original file line number Diff line number Diff line change
Expand Up @@ -377,7 +377,13 @@ internal bool IsNullOrInvalidated(Cursor current)
{
ObjectDisposedException.ThrowIf(_disposed != 0, this);

var tokenType = _metaDb.GetElementTokenType(current);
var tokenType = _metaDb.GetElementTokenType(current, resolveReferences: false);

if (tokenType is ElementTokenType.Reference)
{
current = _metaDb.GetLocationCursor(current);
tokenType = _metaDb.GetElementTokenType(current);
}

if (tokenType is ElementTokenType.Null or ElementTokenType.None)
{
Expand All @@ -390,18 +396,6 @@ internal bool IsNullOrInvalidated(Cursor current)
return (flags & ElementFlags.IsInvalidated) == ElementFlags.IsInvalidated;
}

if (tokenType is ElementTokenType.Reference)
{
current = _metaDb.GetLocationCursor(current);
tokenType = _metaDb.GetElementTokenType(current);

if (tokenType is ElementTokenType.StartObject)
{
var flags = _metaDb.GetFlags(current);
return (flags & ElementFlags.IsInvalidated) == ElementFlags.IsInvalidated;
}
}

return false;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
using HotChocolate.Execution.Processing.Tasks;
using HotChocolate.Resolvers;
using HotChocolate.Types;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.ObjectPool;

namespace HotChocolate.Execution;

public class NonNullInvalidatedObjectTests
{
private static readonly TimeSpan s_timeout = TimeSpan.FromSeconds(10);

[Fact]
public async Task Execute_Should_SkipAndReleaseChildResolvers_When_PureChildInvalidatesResolvedObject()
{
// arrange
var counter = new SlowCounter();
var pool = new CountingResolverTaskPool();
var executor = await CreateExecutorAsync(counter, pool);

// act
var result = await executor.ExecuteAsync(
"{ holder { asyncObj { pureNull slow } } }",
TestContext.Current.CancellationToken);

// assert
result.MatchInlineSnapshot(
"""
{
"errors": [
{
"message": "Cannot return null for non-nullable field.",
"path": [
"holder",
"asyncObj",
"pureNull"
],
"extensions": {
"code": "HC0018"
}
}
],
"data": {
"holder": null
}
}
""");
Assert.Equal(0, counter.Count);
await pool.WaitUntilAllReturnedAsync();
}

[Fact]
public async Task Execute_Should_SkipAndReleaseChildResolvers_When_PureChildInvalidatesPureObject()
{
// arrange
var counter = new SlowCounter();
var pool = new CountingResolverTaskPool();
var executor = await CreateExecutorAsync(counter, pool);

// act
var result = await executor.ExecuteAsync(
"{ holder { pureObj { pureNull slow } } }",
TestContext.Current.CancellationToken);

// assert
result.MatchInlineSnapshot(
"""
{
"errors": [
{
"message": "Cannot return null for non-nullable field.",
"path": [
"holder",
"pureObj",
"pureNull"
],
"extensions": {
"code": "HC0018"
}
}
],
"data": {
"holder": null
}
}
""");
Assert.Equal(0, counter.Count);
await pool.WaitUntilAllReturnedAsync();
}

[Fact]
public async Task Execute_Should_SkipAndReleaseChildResolvers_When_PureChildInvalidatesBatchObject()
{
// arrange
var counter = new SlowCounter();
var pool = new CountingResolverTaskPool();
var executor = await CreateExecutorAsync(counter, pool);

// act
var result = await executor.ExecuteAsync(
"{ holder { batchObj { pureNull slow } } }",
TestContext.Current.CancellationToken);

// assert
result.MatchInlineSnapshot(
"""
{
"errors": [
{
"message": "Cannot return null for non-nullable field.",
"path": [
"holder",
"batchObj",
"pureNull"
],
"extensions": {
"code": "HC0018"
}
}
],
"data": {
"holder": null
}
}
""");
Assert.Equal(0, counter.Count);
await pool.WaitUntilAllReturnedAsync();
}

private static ValueTask<IRequestExecutor> CreateExecutorAsync(
SlowCounter counter,
CountingResolverTaskPool pool)
{
return new ServiceCollection()
.AddSingleton(counter)
.AddSingleton<ObjectPool<ResolverTask>>(pool)
.AddGraphQL()
.AddQueryType<Query>()
.AddObjectType<Holder>(
descriptor => descriptor
.Field("batchObj")
.Type<NonNullType<ObjectType<Obj>>>()
.ResolveBatch(ResolveBatchObj))
.BuildRequestExecutorAsync();
}

private static ValueTask<IReadOnlyList<ResolverResult>> ResolveBatchObj(
IReadOnlyList<IResolverContext> contexts)
{
var results = new ResolverResult[contexts.Count];

for (var i = 0; i < contexts.Count; i++)
{
results[i] = ResolverResult.Ok(new Obj());
}

return new ValueTask<IReadOnlyList<ResolverResult>>(results);
}

public sealed class Query
{
public async Task<Holder?> GetHolderAsync()
{
await Task.Yield();
return new Holder();
}
}

public sealed class Holder
{
[GraphQLNonNullType]
public Obj PureObj => new();

[GraphQLNonNullType]
public async Task<Obj> GetAsyncObjAsync()
{
await Task.Yield();
return new Obj();
}
}

public sealed class Obj
{
[GraphQLNonNullType]
public string? PureNull => null;

public async Task<string> GetSlowAsync([Service] SlowCounter counter)
{
counter.Increment();
await Task.Yield();
return "slow";
}
}

public sealed class SlowCounter
{
private int _count;

public int Count => Volatile.Read(ref _count);

public void Increment() => Interlocked.Increment(ref _count);
}

private sealed class CountingResolverTaskPool : ObjectPool<ResolverTask>
{
private readonly SemaphoreSlim _returned = new(0);
private int _outstanding;

public override ResolverTask Get()
{
Interlocked.Increment(ref _outstanding);
return new ResolverTask(this);
}

public override void Return(ResolverTask obj)
{
if (Interlocked.Decrement(ref _outstanding) == 0)
{
_returned.Release();
}
}

public async Task WaitUntilAllReturnedAsync()
{
while (Volatile.Read(ref _outstanding) != 0)
{
if (!await _returned.WaitAsync(s_timeout, TestContext.Current.CancellationToken))
{
throw new TimeoutException("A resolver task was not returned to its pool in time.");
}
}
}
}
}
Loading
Loading