feat(server): stream paged task results - #3144
Open
contrueCT wants to merge 2 commits into
Open
Conversation
contrueCT
marked this pull request as ready for review
August 8, 2026 04:31
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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}, reachesTaskAPI.get()and materializes the task throughTaskScheduler.task(...).asMap(true, withResult). Whenwith_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}/resultso 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
@Compresswriter interceptor, so large result responses follow the established gzip transport path without buffering the complete response.Scope and non-goals
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.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 throughHugeGraphAuthProxy.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"]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"]StandardTaskSchedulerreads the local task vertex's compressedP.RESULT;TaskAndResultSchedulerreads task metadata and the separateHugeTaskResultvertex used by the distributed scheduler. Both return a detachedTaskResultSnapshot, so the HTTP streaming callback does not retain a database transaction, vertex iterator, or scheduler thread context.TaskScheduler.taskResultSnapshot()is adefaultSPI method: built-in schedulers override it, while existing custom schedulers remain binary/source compatible and explicitly report unsupported streaming when invoked.Endpoint contract
GET .../tasks/{id}/resultapplication/json;charset=UTF-8Cache-Control: no-store.GET .../tasks/{id}/result?limit=NNmust be positive and no greater thanrestserver.task_result_page_size_max.GET .../tasks/{id}/result?page=<token>A page response keeps
root_typeanditemsfor arbitrary JSON roots, while using the existing HugeGraphpagefield 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.limitandpageare 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: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
409 Conflict409 Conflict; the task-details endpoint remains the place to read its error409 Conflict400 Bad Request409 Conflict413 Request Entity Too Large503 Service UnavailableBefore commit, task-result errors use HugeGraph's normal
exception/message/causeenvelope; internal reason labels remain metrics-only. After a response body is committed, HTTP status and JSON error fields can no longer be changed.TaskResultStreamingOutputtherefore classifies slow-reader timeouts and client disconnects in metrics/logs, restores the prior Grizzly connection write timeout, and always releases the stream permit infinally.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:
restserver.task_result_page_size_maxrestserver.task_result_page_offset_maxrestserver.task_result_scan_uncompressed_bytes_maxrestserver.task_result_scan_time_maxrestserver.task_result_stream_time_maxrestserver.task_result_active_streams_maxrestserver.task_result_page_token_ttlrestserver.task_result_page_token_length_maxThe 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
HugeTaskResultread in distributed mode.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}/resultbefore rolling back a server that no longer exposes it.Verifying these changes
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.TaskApiTestcovers full streaming (including gzip transport), array/object pages withlimit/page, scalar rejection, the standard error envelope, running/failed-task semantics, and tampered page tokens under the API profile.TaskResultPageTokenCodecTestcovers signatures, expiry, binding, and key rotation;TaskResultSnapshotTestandTaskAndResultSchedulerTestcover local/distributed snapshot reads.TaskResultStreamingOutputTestand a real Grizzly integration test cover deadline handling, slow readers, write timeouts, RST disconnects, timeout restoration, metrics, and permit release.git diff --checkThe 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 (cpis 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 - TODODoc - DoneDoc - No Need