Hacktoberfest 2026 : les issues que les mainteneurs ont marquées pour octobre, ouvertes et accessibles aux débutants. Parcourir les issues Hacktoberfest

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

Ouverte
#272 1 commentaire 0 réactions 0 personnes assignées Voir sur GitHub

Personne n'a encore pris cette issue.

Évaluation

Difficulté
4/5
Temps estimé
3-5 jours
Accessibilité débutants
50/100
Type d'issue
Bug
Clarté
Plutôt claire
Activité
Calme
Stack technique
python

Piste de recherche

Commencez par la reproduction autonome et exécutez-la plusieurs fois avec graphql-core 3.3.0rc0 afin d’observer le timeout. Suivez experimental_execute_incrementally à travers le sous-système incremental-publisher/graph, en particulier _enqueue et _add_deferred_fragment_node. La tâche est terminée lorsque la requête imbriquée invalide @stream/@defer se termine avec une erreur ou avec un hasNext: false final, sans modifier le comportement des cas de requêtes valides.

Rédigé par le modèle d'indexation à partir du texte de l'issue.

Description

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.

Langage dominant
Python
Étoiles
531
Forks
147
Métriques de merge des PR
Aucune PR mergée en 30 j

Guide de contribution

Aucun guide de contribution indexé pour ce dépôt

Par où commencer

  1. Lisez l'issue en entier, puis le guide de contribution du projet.
  2. Signalez en commentaire que vous la prenez — cela évite que deux personnes fassent le même travail.
  3. Forkez le dépôt et travaillez sur une branche.
  4. Ouvrez une pull request qui référence le numéro de l'issue.

Autres issues de graphql-python/graphql-core

Toutes les issues de graphql-python/graphql-core

Issues similaires

Plus d'issues Python

Recevez les nouvelles issues par e-mail

Un résumé court des issues GitHub adaptées aux débutants.