Hacktoberfest 2026: những issue maintainer đã đánh dấu cho tháng Mười, đang mở và phù hợp người mới. Xem issue Hacktoberfest

Incremental delivery: experimental_execute_incrementally hangs (never terminates) on some invalid @stream queries

Đang mở
#272 1 bình luận 0 reaction 0 người được giao Xem trên GitHub

Chưa có ai nhận issue này.

Đánh giá

Độ khó
4/5
Thời gian dự kiến
3-5 ngày
Mức phù hợp với người mới
50/100
Loại issue
Lỗi
Độ rõ ràng
Khá rõ ràng
Mức độ hoạt động
Ít trao đổi
Công nghệ
python
Lĩnh vực
backend-api-design

Hướng nghiên cứu

Bắt đầu với reproduction độc lập và chạy lặp lại trên graphql-core 3.3.0rc0 để quan sát timeout. Truy vết experimental_execute_incrementally qua subsystem incremental-publisher/graph, đặc biệt là _enqueue và _add_deferred_fragment_node. Hoàn thành khi query @stream/@defer lồng nhau không hợp lệ kết thúc với lỗi hoặc hasNext: false cuối cùng mà không thay đổi hành vi của các trường hợp query hợp lệ.

Do mô hình lập chỉ mục viết ra từ nội dung của issue.

Mô tả

Hi there,

Whilst investigating a production bug (see #271), the fuzzing harness found a second bug. Not as critical as 271 because it only triggers on an invalid graphql shape, but it is a reproducible hang that can be reached in production. Found with the help of Claude code.

Hope this helps - unfortunately I don't have a suggested fix for this one as the root cause was less obvious.

  • Ben (Mixcloud)

Summary

experimental_execute_incrementally never terminates for certain queries combining @stream with nested @defer/child selections — the async iterator over subsequent_results blocks forever, with no final hasNext: false. Reproduced reliably on graphql-core 3.3.0rc0 (≈13/15 attempts hang at a 3 s timeout in isolation, ≈9/10 at 5 s).

Scope, stated up front: every deadlocking query I found is spec-invalid — it fails graphql.validate (a streamed list field with no subselection, and/or a field-merge conflict between a streamed and a non-streamed selection of the same field). A caller that validates before executing rejects these first. Despite extensive search I could not find a spec-valid query that hangs — making the query valid removes the hang. So this is a robustness issue rather than a correctness issue on valid input: execution should fail fast / terminate rather than hang, since experimental_execute_incrementally can be (and is, for performance) called without a separate validation pass, and a hang on any input is a DoS risk.

Environment

  • graphql-core 3.3.0rc0
  • Python 3.14

Query (spec-invalid; hangs instead of erroring)

query {
  obj {
    ... {
      child {
        child { ... @defer(label: "L4") { items @stream(initialCount: 1) } }
        ... @defer { child { fast items } }
      }
    }
  }
}

validate() reports: "Fields 'child' conflict because subfields 'items' conflict because they have differing stream directives" and "Field 'items' … must have a selection of subfields". experimental_execute_incrementally neither raises nor completes.

What is / isn't required (isolation testing)

Query Valid? Hangs?
The query above invalid yes (~13/15)
items @stream(initialCount: 1) with no subselection, alone invalid no
Streamed + non-streamed items merge conflict, alone invalid no
The same structure made valid (aliased, subselections added) valid no
~15 other hand-crafted valid @stream/@defer/nesting shapes valid no

So neither invalid feature alone hangs; the deadlock needs the invalid feature embedded in the nested @defer + @stream + child structure — and making that structure valid stops it.

Notes

  • Timing-sensitive (needs out-of-order async resolver completion), so the repro loops; not 100%/run but high (≈85–90%).
  • Likely the same incremental-publisher/graph subsystem as the _enqueue "race with a stopping consumer" guard and the separate _add_deferred_fragment_node invariant crash (filed as a separate issue): the graph doesn't converge to a terminal state for these inputs.
Self-contained reproduction (graphql-core only)
import asyncio
import random

from graphql import (
    GraphQLDeferDirective,
    GraphQLField,
    GraphQLList,
    GraphQLObjectType,
    GraphQLSchema,
    GraphQLStreamDirective,
    GraphQLString,
    parse,
    specified_directives,
    validate,
)
from graphql.execution import (
    ExperimentalIncrementalExecutionResults,
    experimental_execute_incrementally,
)
from graphql.pyutils import is_awaitable

ATTEMPTS = 15
TIMEOUT_S = 3.0


async def r_fast(_s, _i) -> str:
    await asyncio.sleep(random.uniform(0, 0.002))
    return "fast"


async def r_obj(_s, _i) -> dict:
    await asyncio.sleep(random.uniform(0, 0.001))
    return {}


async def r_list(_s, _i) -> list:
    await asyncio.sleep(random.uniform(0, 0.001))
    return [{}, {}]


OBJ = GraphQLObjectType(
    "Obj",
    lambda: {
        "fast": GraphQLField(GraphQLString, resolve=r_fast),
        "child": GraphQLField(OBJ, resolve=r_obj),
        "items": GraphQLField(GraphQLList(OBJ), resolve=r_list),
    },
)
schema = GraphQLSchema(
    GraphQLObjectType("Query", {"obj": GraphQLField(OBJ, resolve=r_obj)}),
    directives=[*specified_directives, GraphQLDeferDirective, GraphQLStreamDirective],
)

QUERY = """
query {
  obj {
    ... {
      child {
        child { ... @defer(label: "L4") { items @stream(initialCount: 1) } }
        ... @defer { child { fast items } }
      }
    }
  }
}
"""


async def run_once() -> None:
    result = experimental_execute_incrementally(schema, parse(QUERY), root_value={})
    if is_awaitable(result):
        result = await result
    if isinstance(result, ExperimentalIncrementalExecutionResults):
        async for _patch in result.subsequent_results:
            pass


async def main() -> None:
    import graphql

    print("graphql-core:", graphql.__version__)
    errs = validate(schema, parse(QUERY))
    print("query is spec-" + ("INVALID" if errs else "VALID"))
    hangs = 0
    for i in range(1, ATTEMPTS + 1):
        try:
            await asyncio.wait_for(run_once(), TIMEOUT_S)
        except TimeoutError:
            hangs += 1
            print(f"attempt {i}: HUNG (no termination within {TIMEOUT_S}s)")
    print(f"{hangs}/{ATTEMPTS} attempts hung (never terminated).")


asyncio.run(main())

This non-termination, and the finding that valid queries do not reproduce it, were established by fuzzing @defer/@stream query shapes.

Ngôn ngữ chính
Python
Star
531
Fork
147
Chỉ số merge pull request
Không có pull request nào được merge trong 30 ngày

Hướng dẫn đóng góp

Chưa lập chỉ mục được hướng dẫn đóng góp cho kho mã nguồn này

Bắt đầu từ đâu

  1. Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
  2. Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
  3. Fork repository và làm thay đổi trên một nhánh.
  4. Mở pull request có tham chiếu số hiệu của issue.

Issue khác của graphql-python/graphql-core

Tất cả issue của graphql-python/graphql-core

Issue tương tự

Thêm issue về Python

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.