hllqkb commented on issue #3071:
URL: https://github.com/apache/hugegraph/issues/3071#issuecomment-4825000587
Implementation Plan
Breaking the work into independently reviewable and testable chunks. Each
chunk has its own test, can be reviewed in isolation, and builds on the
previous one.
---
**Chunk 1 — Add task.result_chunk_size config option**
File:
`hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/config/CoreOptions.java`
Problem: No configuration exists to control result chunking behavior. The
system always serializes and stores the entire task result as a single vertex
property regardless of size.
Fix: Add `TASK_RESULT_CHUNK_SIZE` static option with default `1048576` (1
MB), range `[0, Bytes.GB]`. Value `0` disables chunking entirely (preserves
current behavior). Register the option in `registerOptions()`. The existing
`TASK_RESULT_SIZE_LIMIT` (16 MB default) remains as the hard upper bound —
chunking operates within this limit to avoid hitting per-property storage
ceilings.
```java
public static final ConfigOption<Long> TASK_RESULT_CHUNK_SIZE =
ConfigOption.builder("task.result_chunk_size")
.description("Max size in bytes per result chunk. " +
"0 disables chunking. Default 1MB.")
.range(0L, Bytes.GB)
.defaultValue(Bytes.MB)
.build();
```
Test: Verify option is registered and default value is 1 MB. Verify range
validation rejects negative values and values > 1 GB.
---
**Chunk 2 — Add property name helpers to HugeTask**
File:
`hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/task/HugeTask.java`
Problem: There is no infrastructure for naming, detecting, or counting
chunked properties. All result properties are currently accessed via a single
`P.RESULT` key.
Fix: Add three static helper methods:
- `chunkKey(int index)` — returns `"~task_result_" + index` for chunk
properties; returns `P.RESULT` (`"~task_result"`) for `index < 0` (the legacy
key)
- `isChunkedProperty(String key)` — returns `true` if key matches
`~task_result_` followed by one or more digits
- `chunkCountKey()` — returns `"~task_result_count"`, a dedicated metadata
property storing the number of chunks
No behavior change — these are pure helpers used by subsequent chunks.
Test: Verify key generation: `chunkKey(0)` → `"~task_result_0"`,
`chunkKey(-1)` → `"~task_result"`. Verify `isChunkedProperty("~task_result_0")`
→ `true`, `isChunkedProperty("~task_result")` → `false`,
`isChunkedProperty("~task_name")` → `false`.
---
**Chunk 3 — Implement chunked write in HugeTask.asArray()**
File:
`hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/task/HugeTask.java`
Problem: `HugeTask.asArray()` (line 568) always writes the entire compressed
result as a single `~task_result` vertex property, with no mechanism to split
large results.
Fix: After compressing `this.result` into `byte[] bytes`, check if chunking
is enabled (`task.result_chunk_size > 0`) and the compressed size exceeds the
threshold. If so:
1. Decompress `bytes` back to the JSON string
2. Parse the JSON string. If it's a JSON array, split along element
boundaries so each chunk stays under the threshold without cutting elements in
half. If it's a single object, split at byte boundaries.
3. Compress each chunk individually
4. Write `~task_result_0`, `~task_result_1`, ... `~task_result_N` to the
property list
5. Write `~task_result_count = N + 1` as a metadata marker
If chunking is disabled or the result is below the threshold, the original
single-property path runs unchanged.
```java
if (this.result != null) {
byte[] bytes = StringEncoding.compress(this.result);
long chunkSize = this.config.get(CoreOptions.TASK_RESULT_CHUNK_SIZE);
if (chunkSize > 0 && bytes.length > chunkSize) {
// Chunked path
List<String> chunks = splitJsonArray(this.result, chunkSize);
for (int i = 0; i < chunks.size(); i++) {
list.add(chunkKey(i));
list.add(StringEncoding.compress(chunks.get(i)));
}
list.add(chunkCountKey());
list.add(chunks.size());
} else {
// Original single-property path
list.add(P.RESULT);
list.add(bytes);
}
}
```
Test: Create a task with result string > 1 MB. Verify vertex has
`~task_result_0` through `~task_result_N` properties plus `~task_result_count`.
Verify small result (< 1 MB) still uses single `~task_result` property.
---
**Chunk 4 — Implement chunked read in HugeTask.property()**
File:
`hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/task/HugeTask.java`
Problem: `HugeTask.property()` (line 463) only handles the single `P.RESULT`
key. There is no logic to detect and reassemble chunked properties when loading
a task from the backend.
Fix: In `property(String key, Object value)`:
1. When `key` matches `isChunkedProperty(key)`, collect the chunk value into
a `List<String>` buffer (sorted by chunk index).
2. When `key` matches `chunkCountKey()`, record the expected chunk count.
3. When all expected chunks have arrived (buffer size == chunk count), sort
by index, decompress each, concatenate into the full JSON string, set
`this.result`.
4. When `key.equals(P.RESULT)`, handle as before (legacy single-property
path).
5. When `key` is not a chunk or result key, delegate to the existing
property handling.
Test: Verify chunked task can be loaded from backend — `this.result` is
correctly reassembled. Verify legacy single-property task still loads
correctly. Verify partial chunk load (e.g., task still being written) does not
produce corrupted result.
---
**Chunk 5 — Update HugeTask.asMap() for chunked/legacy compatibility**
File:
`hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/task/HugeTask.java`
Problem: `HugeTask.asMap()` (line 628) calls `this.result()` to get the
stored result string. This path must work identically whether the underlying
storage is chunked or legacy single-property.
Fix: No code change needed here if Chunk 4 correctly restores `this.result`
during `property()` processing. The `asMap()` method's existing `this.result()`
call will return the full reassembled result. Add a guard: if chunked result is
being reassembled (buffer not yet complete), `this.result()` should return
`null` until all chunks are loaded.
This is primarily a verification chunk: confirm the existing `asMap()` logic
works through the chunked read path from Chunk 4.
Test: Verify `asMap(true, true)` returns full result for chunked task.
Verify `asMap(true, false)` (the `with_result=false` path from #3060) returns
metadata without result for chunked task. Verify `asMap()` with legacy
single-property task still works.
---
**Chunk 6 — Add unit tests for chunked storage**
File:
`hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/TaskCoreTest.java`
Problem: No test coverage for chunked result storage, reassembly, or
backward compatibility.
Fix: Add four test methods:
- `testTaskResultChunked()` — create task with result JSON string > 1 MB.
Verify task vertex stores `~task_result_0`, `~task_result_1`, ... plus
`~task_result_count`. Verify `task.result()` returns the full reassembled
result.
- `testTaskResultSmall()` — create task with result < 1 MB. Verify task
vertex stores single `~task_result` property (backward compat). Verify
`task.result()` returns correct value.
- `testTaskResultChunkReassembly()` — create task with known result, verify
reassembled result equals original exactly (byte-level).
- `testTaskResultBackwardCompat()` — directly construct a task vertex with
legacy single `~task_result` property. Load it. Verify `task.result()` works.
- `testTaskResultChunkDisabled()` — set `task.result_chunk_size` to `0`.
Create large task. Verify single-property path used.
Test data: Generate result strings of known sizes using a helper method that
creates a JSON array with N elements, e.g., `["result-item-00000",
"result-item-00001", ...]`.
---
**Chunk 7 — Add pagination fields and logic to HugeTask**
File:
`hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/task/HugeTask.java`
Problem: `HugeTask` has no concept of pagination. The entire result is
always returned.
Fix: Add two transient fields: `private int page = -1` and `private int
pageSize = -1`. When both are `>= 0`, `asMap(true, true)` should apply
pagination:
1. Parse `this.result` into a list of objects (the Gremlin result is always
a list)
2. Compute `start = page * pageSize`, `end = Math.min(start + pageSize,
list.size())`
3. Return `list.subList(start, end)` as the result
4. Include a `pagination` metadata map: `{"page": page, "page_size":
pageSize, "total": list.size()}`
5. When both fields are `-1` (default), return the full result — backward
compatible
6. Add getter/setter for `page` and `pageSize`.
Test: Verify pagination with known list size: page=0, pageSize=10 returns
first 10 items. page=2, pageSize=10 returns items 20-29. Verify metadata shows
correct total. Verify page=-1 returns all items.
---
**Chunk 8 — Add page and page_size query params to TaskAPI.get()**
File:
`hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/job/TaskAPI.java`
Problem: `GET /tasks/{id}` has no pagination query parameters.
Fix: Add two new `@QueryParam` annotations to the existing `get()` method:
```java
@QueryParam("page") @DefaultValue("-1") int page,
@QueryParam("page_size") @DefaultValue("-1") int pageSize
```
When both `page >= 0` and `pageSize >= 0`, pass them to the task before
calling `asMap()`:
```java
HugeTask<?> task = scheduler.task(IdGenerator.of(id), withResult);
if (page >= 0 && pageSize >= 0) {
task.page(page);
task.pageSize(pageSize);
}
return task.asMap(true, withResult);
```
When either is `-1` (default), behavior is unchanged — full result returned.
Test: Verify `GET /tasks/{id}?with_result=true&page=0&page_size=10` returns
first page. Verify `GET /tasks/{id}?with_result=true` (no pagination params)
returns full result. Verify `GET
/tasks/{id}?with_result=false&page=0&page_size=10` does not include result
(metadata-only takes precedence).
---
**Chunk 9 — Add task(Id, boolean, int, int) overload to TaskScheduler**
File:
`hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/task/TaskScheduler.java`
(interface) + `StandardTaskScheduler.java` (implementation)
Problem: The `TaskScheduler.task(Id, boolean withResult)` method has no way
to pass pagination parameters to the task. The current workaround uses setters
on the returned task object, but this creates a race condition if the task's
`asMap()` is called before the setters execute.
Fix: Add a new overload:
```java
// Interface
<V> HugeTask<V> task(Id id, boolean withResult, int page, int pageSize);
// Implementation (StandardTaskScheduler)
@Override
public <V> HugeTask<V> task(Id id, boolean withResult, int page, int
pageSize) {
HugeTask<V> task = this.task(id, withResult);
if (page >= 0 && pageSize >= 0) {
task.page(page);
task.pageSize(pageSize);
}
return task;
}
```
Update `TaskAPI.get()` (Chunk 8) to use this overload instead of manual
setter calls.
Test: Verify `task(123, true, 0, 10).asMap(true, true)` returns first 10
results with pagination metadata.
---
**Chunk 10 — Add API integration tests for pagination**
File:
`hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/TaskApiTest.java`
Problem: No test coverage for paginated result retrieval via REST API.
Fix: Add three test methods:
- `testGetWithPagination()` — create a Gremlin task that returns 100 items.
Call `GET /tasks/{id}?with_result=true&page=0&page_size=30`. Assert 30 items
returned. Call `page=3&page_size=30`. Assert 10 items returned (last page has
remainder).
- `testGetPaginationMetadata()` — create task with known result. Verify
response includes `pagination` object with correct `total`, `page`, `page_size`.
- `testGetWithoutPagination()` — create task with pagination-capable result.
Call without `page`/`page_size` params. Assert full result returned (backward
compat — no pagination object present).
- `testGetInvalidPage()` — call with `page=-1&page_size=10`. Verify full
result returned (graceful fallback).
---
**Chunk 11 — Manual verification: chunked storage**
Manual test procedure (no automated test — verifies the full end-to-end
storage path):
1. Start HugeGraph server with RocksDB backend
2. Create a schema and insert test data (e.g., 50K vertices with labels)
3. Submit a Gremlin async task: `g.V().hasLabel("test").valueMap()`
4. Wait for task to SUCCESS
5. Inspect task vertex properties via `GET
/graphs/hugegraph/graph/vertices/{task_id}` (raw vertex API or debug log)
6. Verify `~task_result_0`, `~task_result_1`, ... `~task_result_N` exist (if
> 1 MB) or single `~task_result` exists (if ≤ 1 MB)
7. Verify `~task_result_count` = N + 1 (if chunked)
8. Run `GET /tasks/{id}?with_result=true` and verify the full result equals
the query result
---
**Chunk 12 — Manual verification: paginated REST API**
Manual test procedure:
1. Use the task from Chunk 11 (with chunked result)
2. `GET /tasks/{id}?with_result=true&page=0&page_size=100` → verify first
100 items
3. `GET /tasks/{id}?with_result=true&page=1&page_size=100` → verify items
100-199 (different from page 0)
4. `GET /tasks/{id}?with_result=true&page=999&page_size=100` → verify empty
array returned with correct `total`
5. `GET /tasks/{id}?with_result=true` (no pagination) → verify full result,
no `pagination` key in response
6. Verify each page response includes `pagination: { page, page_size, total
}`
---
**Chunk 13 — Run full test suite**
Commands:
```bash
# Core tests (task framework)
mvn test -pl hugegraph-server/hugegraph-test -am -P core-test,rocksdb
# API tests (REST endpoints)
mvn test -pl hugegraph-server/hugegraph-test -am -P api-test,rocksdb
# Full test suite
mvn test -pl hugegraph-server/hugegraph-test -am -P
core-test,api-test,rocksdb
```
Expected: All existing tests pass. New tests from Chunk 6 and Chunk 10 pass.
No regressions in unrelated tests.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]