graphql-go-tools

repository·master·Indexed 21 days ago

https://github.com/wundergraph/graphql-go-tools

A high-performance GraphQL Router and API Gateway framework written in Go. It provides core engine components for GraphQL Federation, query planning, and execution, designed for high throughput and low garbage collection overhead. The library includes tools for lexing, parsing, AST validation, normalization, and execution planning. It serves as the core engine for the Cosmo Router.

Tokens
44K
Snippets
100
Records
149
Agent score
74%

What's inside graphql-go-tools

  1. Overview of graphql-go-tools

    master

    graphql-go-tools is a high-performance GraphQL Router and API Gateway framework written in Golang. It is designed for high throughput and low garbage collection overhead. It is the core engine used by the Cosmo Router, which provides a complete, production-ready solution for Federated GraphQL.

    Key capabilities include:

    • GraphQL Federation support
    • High-performance query planning and resolution
    • Built-in support for batching federation entity calls
    • Optimized for performance (benchmarks show up to 8x more requests per second and 8x lower p99 latency compared to Apollo Router in certain end-to-end scenarios).
  2. Use the grpctest package for gRPC-to-GraphQL testing

    master

    The grpctest package is a testing utility designed to validate grpc_datasource functionality in graphql-go-tools. It provides a complete ecosystem for testing gRPC-to-GraphQL integration, including:

    • Mock gRPC Service: A functional implementation of a product service supporting entity lookups (Product, Storage, Warehouse), queries (filtering, pagination), mutations, and complex types (Unions, Interfaces, Nullable fields).
    • GraphQL Schema: A comprehensive schema located in testdata/products.graphqls covering advanced GraphQL features.
    • Field Mappings: Logic that maps GraphQL operations, fields, arguments, and enums to their corresponding gRPC/Protobuf counterparts.
    • Schema Management: Utilities for loading, parsing, and validating schemas.

    Use this package to ensure your grpc_datasource implementation correctly handles request/response mapping, field resolution, error handling, and performance characteristics.

  3. What is NetPoll

    master

    NetPoll is a Go abstraction layer for epoll (Linux) and kqueue (macOS). It is designed to provide a simplified interface for managing network connections using OS-specific event notification mechanisms.

    Key characteristics:

    • Platform Support: Uses epoll on Linux and kqueue on macOS.
    • Windows Behavior: Does not use event notification on Windows; instead, it handles connections in separate goroutines.
    • CGO-free: It is implemented to avoid the need for CGO.
    • Scope: It is a specialized library focused on net.Conn methods for high-performance network I/O.
  4. Refactor Defer ID from String to Int in Resolve Package

    master

    The resolve package has been refactored to use int instead of string for DeferID fields to improve performance and simplify logic. When working with the resolve engine, ensure you are using integer comparisons and zero-value checks (0) instead of empty string checks ("").

    Key type changes:

    • DeferFetchGroup.DeferID is now int.
    • FetchDependencies.DeferID is now int.
    • DeferField.DeferID is now int.
    • resolvable.deferID is now int.

    When checking for the presence of a Defer ID, use if r.deferID != 0 instead of if r.deferID != "".

    // Old pattern
    type DeferField struct {
    	DeferID string
    }
    
    // New pattern
    type DeferField struct {
    	DeferID int
    }
  5. How the GraphQL @defer planning phase works

    master

    During the Planning phase, the engine decides which datasource fetches each field and in which @defer scope. A planner is uniquely identified by a combination of its datasource and its deferID.

    Key behaviors include:

    • Scoping: To prevent deferred fields from being accidentally claimed by a non-deferred planner, the engine enforces that a field only joins a planner if their deferID matches. A field with deferID == 0 is considered non-deferred.
    • Multiple Planners: A single deferID can result in multiple planners if the deferred fields are reachable from different root anchors (e.g., one from the root query and another from an entity node).
    • Defer Parents: The ProcessDefer function identifies 'defer parents'—ancestor nodes on the same datasource that are not themselves deferred but must be part of the path to a deferred field. These ancestors are planned as non-deferred paths to anchor the deferred fetch at a real root (like a root query or an entity node).
    • Required Fields: Fields needed for entity resolution (like @key or @requires) are injected with @__defer_internal directives. @requires fields use the requesting field's deferID, while @key fields use the parent's deferID to ensure the key is available before the deferred fetch runs.
    type DeferInfo struct {
        ID       int
        Label    string
        ParentID int
    }
  6. Understand the Defer Parallel Execution architecture

    master

    The Defer Parallel Execution implementation replaces the sequential defer execution loop with a tree-based executor. This allows sibling defers to run concurrently while ensuring child defers only execute after their parent has completed, adhering to the GraphQL specification's parent-before-child ordering guarantee.

    Key Components:

    • DeferTreeNode: A tree structure (mirroring FetchTreeNode) that organizes defers into Single, Sequence, or Parallel nodes.
    • Post-processor: A component that converts DeferDescriptors into a DeferTree during the engine's post-processing phase.
    • Tree Resolver: A recursive walker that spawns goroutines for Parallel nodes and executes Sequence nodes in order.
    • Concurrency Control: Uses golang.org/x/sync/errgroup for execution and mutexes (on the Loader and during the tree-walk) to protect shared Resolvable and jsonArena resources.
  7. How the Abstract Selection Rewriter tracks field provenance

    master

    The Abstract Selection Rewriter uses a log-based approach to track the relationship between original field references and new field references created during fragment flattening. Instead of using expensive path-matching or scope-chain heuristics to determine if a field has moved, the rewriter records exact 'provenance' at two specific lifecycle points:

    1. Copying: When ast.Document.CopySelection or preserveTypeNameSelection creates a new field from an old one, the relationship is recorded.
    2. Merging: When Document.MergeFieldsDefer merges two fields (where one survives and the other is removed), the relationship is recorded.

    This allows the rewriter to build two critical maps for consumers:

    • fieldRefOrigins: Maps a new reference to all the pre-rewrite references it represents.
    • changedFieldRefs: Maps an old reference to its final, post-rewrite replacement.

    This design ensures that planner-added fields (like those in skipFieldsRefs) and user-requested fields are correctly disambiguated, even when they occupy the same logical path in the GraphQL response.

  8. Understand the @defer incremental response wire format

    master

    When using @defer, the client receives a stream of JSON payloads. The first payload contains the primary data and a pending list. Subsequent payloads contain the incremental data and a completed marker.

    Payload Structure

    1. Initial Response: Contains the primary data and a pending array announcing which fragments are being deferred. Each entry in pending includes an id and the path to the field.
    2. Incremental Payload: Contains the incremental data (correlated by id) and a completed array.
    3. Flow Control: The hasNext boolean indicates if more payloads are expected. The payload delivering the last outstanding fragment will have hasNext: false.

    Example Stream

    Initial response:

    {
      "data": {"user": {"name": "Alice"}},
      "pending": [{"id": "1", "path": ["user"]}],
      "hasNext": true
    }

    Incremental payload:

    {
      "incremental": [{"data": {"expensiveField": "..."}, "id": "1"}],
      "completed": [{"id": "1"}],
      "hasNext": false
    }
    // initial response
    {"data":{"user":{"name":"Alice"}},"pending":[{"id":"1","path":["user"]}],"hasNext":true}
    
    // incremental payload
    {"incremental":[{"data":{"expensiveField":"..."},"id":"1"}],"completed":[{"id":"1"}],"hasNext":false}
  9. Handle recoverable vs non-recoverable errors in @defer

    master

    When using @defer, errors are routed to different parts of the response envelope based on whether the error is recoverable (the fragment root survives) or non-recoverable (the fragment root becomes null).

    Recoverable Errors

    Occur when a nullable field within the deferred fragment errors. The fragment still delivers its data, with the errored field set to null.

    • Placement: The error is attached to the incremental item.
    • Example: incremental: [{ "data": { "hero": { "name": null } }, "errors": [...], "id": "0" }]

    Non-recoverable Errors

    Occur when a non-nullable field errors, causing the error to propagate to the fragment root and invalidate the entire fragment.

    • Placement: The incremental item is omitted entirely. The error is reported in the completed array.
    • Example: completed: [{ "id": "1", "errors": [{ "message": "Cannot return null for non-nullable field ..." }] }]
    // Recoverable error (incremental item emitted)
    {
      "incremental": [
        { "data": { "hero": { "name": null } }, "errors": [{ "message": "bad" }], "id": "0" }
      ],
      "completed": [{ "id": "0" }],
      "hasNext": false
    }
    
    // Non-recoverable error (incremental item omitted)
    {
      "completed": [
        { "id": "1", "errors": [{ "message": "Cannot return null for non-nullable field ..." }] }
      ],
      "hasNext": true
    }
  10. How DataLoader provides batching and request deduplication

    master

    The DataLoader is a request-scoped mechanism used to optimize data fetching in the GraphQL engine. It solves two primary performance issues:

    1. Batching: Instead of making individual network requests for every item in an array (the N+1 problem), DataLoader collects requests for siblings and combines them into a single batch request (e.g., using a single _entities query with multiple representations).

    2. Request Deduplication: If multiple parts of a query request the same data (the same arguments/keys), DataLoader ensures the data is only fetched once by requesting only the unique set of arguments.

    Example of deduplication: If a resolver requests [Product1, Product2, Product1], a batch without deduplication would send three representations. With DataLoader, it sends only [Product1, Product2], reducing payload size and backend load.

  11. Understand the incremental delivery format for @defer

    master

    The project implements a streamed response format for GraphQL @defer directives that aligns with the GraphQL incremental-delivery specification, with specific simplifications regarding nesting and ID allocation.

    Response Lifecycle

    1. Initial Response: Contains the primary data and a pending array. The pending array declares every @defer in the operation (both top-level and nested) immediately. It also includes hasNext: true.
    2. Subsequent Responses: Carry incremental data, completed status, and hasNext. No pending array is emitted in subsequent responses.

    Envelope Shapes

    Pending Entry

    Used only in the initial response to announce deferred fields.

    • id: string (the unique identifier for the defer).
    • path: string[] (the field-alias-or-name chain from the root to the field enclosing the fragment).
    • label: string (optional metadata).

    Incremental Entry

    Used in subsequent responses to deliver data slices.

    • data: object (the deferred data).
    • id: string (matches the pending ID).
    • errors: array (optional; contains recoverable errors).

    Completed Entry

    Used to signal a defer has finished.

    • Success: { "id": "string" }
    • Failure: { "id": "string", "errors": [...] } (contains non-recoverable errors).

    Key Deviations from Spec

    • Early Disclosure: All nested defers are listed in the initial pending array, rather than being announced lazily when their parent completes.
    • ID Granularity: One id is allocated per AST @defer directive, not per list-item. If a @defer is inside a list, all items are returned in a single incremental envelope with one ID.
    // Initial response example
    {
      "data": { "hero": { "id": "1" } },
      "pending": [
        { "id": "0", "path": ["hero"], "label": "DeferTop" }
      ],
      "hasNext": true
    }
    
    // Subsequent response example
    {
      "incremental": [
        { "data": { "name": "Luke" }, "id": "0" }
      ],
      "completed": [{ "id": "0" }],
      "hasNext": true
    }
  12. How `parentDeferId` works in GraphQL Defer

    master

    In the context of GraphQL @__defer_internal directives, parentDeferId represents the physical tree-ancestry of a deferred field.

    For a deferred field to be correctly resolved, it must be parented to the nearest enclosing deferred object. This allows the resolver to traverse into the deferred part of the tree.

    During the field merging process, the parentDeferId can become 'stale' if an ancestor's defer directive is removed or merged away. The system must ensure that:

    1. Missing parents are added: A field without a parentDeferId is assigned the ID of its nearest enclosing deferred ancestor.
    2. Valid parents are preserved: If the parentDeferId still references a live, existing defer ID, it is kept to preserve delivery ordering.
    3. Stale parents are repaired: If the parentDeferId references an ID that no longer exists in the live operation tree, it is repaired to point to the nearest enclosing deferred ancestor, or removed if no such ancestor exists.