Skip to content

feat(server): stream paged task results - #3144

Open
contrueCT wants to merge 2 commits into
apache:masterfrom
contrueCT:task/task-result-streaming-pagination-design
Open

feat(server): stream paged task results#3144
contrueCT wants to merge 2 commits into
apache:masterfrom
contrueCT:task/task-result-streaming-pagination-design

Conversation

@contrueCT

@contrueCT contrueCT commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Purpose of the PR

This PR implements Phase 1 of large task-result retrieval. It adds a dedicated, read-only result endpoint that streams the persisted LZ4 payload and supports resumable logical pagination for top-level JSON arrays and objects.

Current behavior and problem

The existing task-details endpoint, GET /graphspaces/{graphspace}/graphs/{graph}/tasks/{id}, reaches TaskAPI.get() and materializes the task through TaskScheduler.task(...).asMap(true, withResult). When with_result=true (the default), the task result becomes part of the response map before it is returned. That behavior remains unchanged for compatibility, but it is not a suitable retrieval path for large results.

This PR adds GET /graphspaces/{graphspace}/graphs/{graph}/tasks/{id}/result so a client can retrieve the result without loading a complete decompressed JSON string or a Java object tree on the server.

The result resource reuses HugeGraph's existing @Compress writer interceptor, so large result responses follow the established gzip transport path without buffering the complete response.

Scope and non-goals

  • This is a read-path change. It does not change task-result write logic, the persisted result format, task schemas, or the existing task-details response.
  • The snapshot still holds the compressed result byte[], because the current storage APIs return the result blob eagerly. The change removes the full decompressed result/object materialization, not every in-memory copy of the compressed blob.
  • Pagination is logical JSON pagination, not physical result chunking or random-access storage. A later page reopens the compressed snapshot and scans JSON tokens from the beginning to its offset; scan-byte, scan-time, and offset limits bound this work.
  • This PR does not claim a benchmarked latency, heap, or throughput improvement. Those need production-like profiling before making performance claims.

Main Changes

Read-path change

The diagrams show the preserved task-details path and the new result-only path. The new path deliberately bypasses HugeTask.asMap() while preserving task authorization through HugeGraphAuthProxy.

flowchart LR
    Client[REST client] --> Details["GET .../tasks/{id}\nwith_result=true"]
    Details --> Scheduler["TaskScheduler.task()"]
    Scheduler --> Task["HugeTask"]
    Task --> Map["asMap(true, true)"]
    Map --> Response["Task map including task_result"]
Loading
flowchart LR
    Client[REST client] --> Result["GET .../tasks/{id}/result"]
    Result --> Auth["HugeGraphAuthProxy\nverify READ permission"]
    Auth --> Snapshot["TaskScheduler.taskResultSnapshot()"]
    Snapshot --> Local["Standard scheduler:\ntask vertex P.RESULT"]
    Snapshot --> Distributed["Distributed scheduler:\nHugeTaskResult / ~taskresult vertex"]
    Local --> Detached["Detached compressed snapshot"]
    Distributed --> Detached
    Detached --> Streamer["TaskResultStreamer"]
    Streamer --> Decode["LZ4 InputStream + Jackson streaming parser"]
    Decode --> Response["Raw JSON stream or logical page"]
Loading

StandardTaskScheduler reads the local task vertex's compressed P.RESULT; TaskAndResultScheduler reads task metadata and the separate HugeTaskResult vertex used by the distributed scheduler. Both return a detached TaskResultSnapshot, so the HTTP streaming callback does not retain a database transaction, vertex iterator, or scheduler thread context. TaskScheduler.taskResultSnapshot() is a default SPI method: built-in schedulers override it, while existing custom schedulers remain binary/source compatible and explicitly report unsupported streaming when invoked.

Endpoint contract

Request Response Notes
GET .../tasks/{id}/result The original JSON value, streamed as application/json;charset=UTF-8 Returns Cache-Control: no-store.
GET .../tasks/{id}/result?limit=N A logical page N must be positive and no greater than restserver.task_result_page_size_max.
GET .../tasks/{id}/result?page=<token> The next logical page The opaque token carries the limit and offset. Clients must return it unchanged.

A page response keeps root_type and items for arbitrary JSON roots, while using the existing HugeGraph page field for continuation:

{
  "root_type": "array",
  "items": [1, 2],
  "page": "<opaque token or null>"
}

Only top-level arrays and objects are pageable. Array items are returned directly. Object entries are represented as { "key": ..., "value": ... } items so the streaming parser does not collapse duplicate JSON object keys. Scalar JSON values remain available through the complete-result form but reject pagination.

limit and page are mutually exclusive. A terminal page returns "page": null.

Page consistency and token protection

Before a pageable response commits HTTP 200, the server preflights the persisted JSON: it validates the root type, scans to the requested offset, checks the configured byte/time limits, and probes token encoding. The streaming pass then reopens the detached snapshot for output. This keeps malformed JSON, invalid offsets, unsupported roots, token-configuration failures, and scan-limit failures on the normal pre-commit error path.

A token has the form key-id.base64url(payload).base64url(hmac). It is HMAC-SHA256 signed with constant-time MAC comparison and binds all of the following values:

  • graphspace, graph, and task id;
  • JSON root type, next offset, and page size;
  • SHA-256 fingerprint of the compressed result snapshot; and
  • issued and expiry timestamps.

The server rejects malformed, expired, tampered, wrong-task, or oversized tokens. It returns HTTP 409 when a later request observes a different snapshot fingerprint or JSON root type, rather than silently continuing a page sequence over a changed result. The codec accepts a current and an optional previous key id/secret to support a bounded key rotation window.

Status, error, and transport behavior

Condition HTTP behavior before the response commits
Task is still running or otherwise not successful 409 Conflict
Task failed or was cancelled 409 Conflict; the task-details endpoint remains the place to read its error
Successful task has no persisted result 409 Conflict
Invalid request or page token; scalar pagination 400 Bad Request
Result changed between pages or persisted JSON is invalid 409 Conflict
Page offset / scan-byte / scan-time limit is exceeded 413 Request Entity Too Large
Server has no available stream permit 503 Service Unavailable

Before commit, task-result errors use HugeGraph's normal exception / message / cause envelope; internal reason labels remain metrics-only. After a response body is committed, HTTP status and JSON error fields can no longer be changed. TaskResultStreamingOutput therefore classifies slow-reader timeouts and client disconnects in metrics/logs, restores the prior Grizzly connection write timeout, and always releases the stream permit in finally.

Resource controls and observability

The new REST-server settings provide explicit bounds for page size, page offset, JSON scan bytes/time, stream duration, active streams, token lifetime, and token length. The packaged defaults are:

Setting Default Purpose
restserver.task_result_page_size_max 1,000 Maximum top-level values per page.
restserver.task_result_page_offset_max 100,000 Maximum logical offset accepted from a page token.
restserver.task_result_scan_uncompressed_bytes_max 256 MiB Maximum decompressed bytes scanned to locate a page.
restserver.task_result_scan_time_max 30 s Preflight scan deadline.
restserver.task_result_stream_time_max 60 s End-to-end write/decompression deadline.
restserver.task_result_active_streams_max 2 Per-server concurrent stream limit.
restserver.task_result_page_token_ttl 900 s Page-token lifetime.
restserver.task_result_page_token_length_max 4,096 Upper bound for encoded tokens.

The active-stream limit is initialized on the first request and is treated as a startup setting. Aggregate metrics distinguish complete versus paged requests and record active streams, success, pre-/post-commit failures, timeouts, disconnects, and duration. Failure logs add the backend, task id, logical offset, compressed/written byte counts, and error type needed for diagnosis.

For a multi-node deployment, operators must configure one shared Base64URL-encoded secret with at least 32 bytes for restserver.task_result_page_token_secret, together with a key id, on every serving node. The default is a per-JVM temporary secret and the server logs a warning; it is unsuitable for page tokens that may be served by another node or after a restart.

Review focus and rollout / rollback

  • Review the detached snapshot boundary in both scheduler implementations, especially the separate HugeTaskResult read in distributed mode.
  • Review the preflight-then-stream sequence, token bindings, and status mapping before the response is committed.
  • Review timeout restoration and permit release for slow readers and client disconnects.
  • Proxy deployments must preserve streaming behavior; an intermediary that buffers the full response removes the client-visible streaming benefit.

The endpoint is additive and requires no data migration. Rollback is a code rollback: existing task-result storage and the established task-details endpoint remain compatible. Clients must stop calling /tasks/{id}/result before rolling back a server that no longer exposes it.

Verifying these changes

  • Trivial rework / code cleanup without any test coverage. (No Need)
  • Already covered by existing tests, such as (please modify tests here).
  • Need tests and can be verified as follows:
    • mvn clean compile "-Dmaven.javadoc.skip=true"
    • mvn test -pl hugegraph-server/hugegraph-test -am -P unit-test "-Dtest=TaskResultExceptionsTest,TaskResultGrizzlyIntegrationTest,TaskResultStreamingOutputTest,TaskResultPageTokenCodecTest,TaskResultSnapshotTest,TaskResultStreamerTest,TaskAndResultSchedulerTest" "-Dsurefire.failIfNoSpecifiedTests=false" ? 43 tests, 0 failures/errors.
    • TaskApiTest covers full streaming (including gzip transport), array/object pages with limit / page, scalar rejection, the standard error envelope, running/failed-task semantics, and tampered page tokens under the API profile.
    • TaskResultPageTokenCodecTest covers signatures, expiry, binding, and key rotation; TaskResultSnapshotTest and TaskAndResultSchedulerTest cover local/distributed snapshot reads.
    • TaskResultStreamingOutputTest and a real Grizzly integration test cover deadline handling, slow readers, write timeouts, RST disconnects, timeout restoration, metrics, and permit release.
    • git diff --check

The focused local regression suite passed after rebasing this branch onto 4f1a8b34169e021717906167444c75c4c22d426b (upstream/master). CI is the remaining full-project validation. The local distribution package has a known Windows-only Swagger installation issue (cp is unavailable), so final API-profile execution is expected to run in CI/Linux rather than relying on that packaging path.

Does this PR potentially affect the following parts?

Documentation Status

  • Doc - TODO
  • Doc - Done
  • Doc - No Need

@contrueCT
contrueCT marked this pull request as ready for review August 8, 2026 04:31
@dosubot dosubot Bot added size:XXL This PR changes 1000+ lines, ignoring generated files. api Changes of API feature New feature labels Aug 8, 2026
@contrueCT contrueCT changed the title feat(api): stream paged task results feat(server): stream paged task results Aug 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

api Changes of API feature New feature size:XXL This PR changes 1000+ lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant