[Proposal] RFC: evaluating a first Rust proof of concept in the Server
维护者通常 1 天内回复
还没有人认领这个 Issue。
评估
- 难度
- 5/5
- 预计耗时
- 一周以上
- 新手友好度
- 30/100
- Issue 类型
- 功能
- 描述清晰度
- 基本清楚
- 活跃度
- 活跃
- 领域
- backend, performance
调研方向
Review the evidence repository dpol1/hugegraph-rust-rfc, especially the README and rfc/ directories. Understand the benchmark harness in hugegraph-server/hugegraph-test/src/test/java/org/apache/hugegraph/benchmark/rfc. The work involves evaluating JNI performance against Java alternatives, not writing code. The outcome is a recommendation based on latency measurements.
由索引模型根据 Issue 内容生成。
描述
Status: draft for review. Parent: apache/hugegraph#3110. Groundwork: #3145, #3146. Snapshot: apache/hugegraph 83ef9f3fa.
- Evidence repository: dpol1/hugegraph-rust-rfc at commit
b094369. Its README explains how to reproduce the runs. Paths below in itsrfc,spikeandresultsdirectories link to it. - Benchmarks and harness:
benchmark/rfcon the branchprototype/rfc-spikeof dpol1/hugegraph (commit1c62d1fe7), not proposed for merge. - Paths that start with a HugeGraph module name are in apache/hugegraph at
83ef9f3fa.
1. Objectives, scope, non-goals
Question: which Server-core candidate offers a verifiable benefit large enough to justify the interoperability, compatibility, and maintenance cost of a Rust implementation?
Latency criterion, committed before any measurement (rfc/thresholds.md): T_native / T_Java <= 0.5. T_native covers the crossing, conversions, copies, the operation, and building the result.
Scope: the candidates of the #3145 inventory (LongEncoding, NumericUtil, both BytesBuffer copies, IntSet/IntMap, IntMapByDynamicHash) and one non-codec candidate, the Jieba analyzer. Server core is the perimeter this RFC proposes; the maintainers have not chosen it.
Non-goals:
- Rust code in
apache/hugegraph - storage baselines (#3223 covers them)
- toolchain client work
- a proof of concept on
BytesBufferbefore #3090 is decided
2. Candidates and decision
All numbers are JMH average times per call on one Linux x86_64 laptop (section 6). The table uses these terms:
- Java cost (stage A): time per call of the current Java operation.
- Calibration (stage B): time per call of the Java-to-Rust crossings that a native version of the operation would need.
- Declared path: how those crossings are made, JNI through the jni-rs
with_envwrapper around raw JNI calls on stack buffers (section 3). - Loads:
smalldraws values from 0 to 999,mixedfrom the fulllongrange, andlargefrom the 1000 values belowLong.MAX_VALUE. ForvalidB64Charthe character is early in the alphabet, late in it, or absent. - Stop and explore: for one operation and load, the declared path stops when the lower bound of the calibration exceeds half the upper bound of the Java cost; both bounds come from 99.9% confidence intervals (
rfc/thresholds.md). Otherwise the verdict is explore, which means only that the rule does not allow a stop. A stop applies to the declared path and that load, not to every possible native implementation. - Stage C: measurement of a complete native implementation for an operation that reaches explore. It was not run.
- Not distinguishable from the harness: the Java cost does not separate from the harness baseline, a benchmark method that reads the same inputs without running the operation. The rule is not applied to such a case.
- Outcomes: evidence contrary when the declared path stops; evidence insufficient when the available evidence leaves the question open; not evaluated when nothing was measured, for the reason given in the row.
| Candidate | Outcome | Evidence |
|---|---|---|
LongEncoding encoders (encodeNumber, encodeSortable, encodeSignedB64) |
Evidence contrary for the declared path and loads | Java 38 to 182 ns. Creating the result string across the boundary alone costs 166 to 171 ns (rfc/stage-c-gate.md) |
LongEncoding.decodeSignedB64 |
Against the current Java: stop on small and large; evidence insufficient on mixed (the gate says explore; stage C was not run). Against an improved Java decoder (same behaviour on the verified cases, section 4): evidence contrary for the declared path on all loads | Current Java 25 / 280 / 263 ns; improved Java 9.5 / 21.7 / 23.2 ns (small / mixed / large, same campaign); native read calibration 128 to 143 ns (rfc/stage-c-gate-d1.md) |
LongEncoding.validB64Char |
Evidence contrary for the declared path | Java 5.7 to 25.4 ns; calibration 61 ns (rfc/stage-c-gate.md) |
NumericUtil (numberToSortableLong, numberToSortableBytes, sortableBytesToNumber) |
Evidence contrary for the declared path; one case not distinguishable from the harness | Java 3.8 to 11.5 ns; calibrations 59 to 158 ns (rfc/stage-c-gate.md) |
BytesBuffer (core and struct) |
Not evaluated | Blocked by #3090. The two copies already meet on one live path: the store ScanPartition service decodes server-written rows with the struct copy and passes no schema, while the core property format relies on schema metadata. rfc/matrix.md gives the call chain. Vermeer loads graphs through this service. apache/hugegraph-computer#361 reports Can't construct Cardinality from code 0 in the struct readProperty there, an error consistent with the mismatch; it was not reproduced here, and the report lists Store and PD images tagged latest |
IntSet / IntMap |
Evidence insufficient for Rust | The motivation is maintenance (sun.misc.Unsafe) and memory, both outside the latency criterion. JEP 471 (https://openjdk.org/jeps/471) maps every member they use to VarHandle; that Java path was not measured here |
IntMapByDynamicHash |
Not a candidate today | No production instantiation at 83ef9f3fa (rfc/matrix.md) |
| Jieba analyzer | Evidence insufficient | Tokens are persisted as search-index row keys. The jieba-rs README says nothing about compatibility with jieba-analysis 1.0.2, and a token-equality run on real text is missing (rfc/matrix.md) |
Recommendation: on the measured loads, the Java optimization makes it unjustified to continue the studied JNI path with respect to the 2x objective. Every mapped operation and load stops on this path, with two exceptions:
numberToSortableLong(Long)on the small load, whose cost is not distinguishable from the harness.- The current
decodeSignedB64on the mixed load, which ends as evidence insufficient (the gate says explore; stage C was not run). Against the improved Java decoder (21.7 ns instead of 280 ns on that load), the declared path stops on every load.
The spike on decodeSignedB64 therefore ends here: no stage C, and the wrapper question stays deferred. The other candidates keep the outcomes in the table.
The conclusion is unfavourable to the measured JNI path, on the measured loads, with respect to the 2x objective; it does not extend to every fine-grained call or to every native implementation. Nor does it show that Rust is without value for codecs in general.
The conclusion is compatible with the boundary principle of the refactor/rust-rewrite-design branch: explicit RPC or a C ABI, avoiding fine-grained cross-language calls (principle 5 of its design document, linked in the next section). Coarse-grained or batched boundaries were not measured here; a later candidate of that kind would need its own evidence, against the #3223 baselines.
Relation to refactor/rust-rewrite-design
The upstream branch refactor/rust-rewrite-design (apache/hugegraph, head a92da16a3 of 2026-09-13) is earlier work on the same roadmap. It covers PD and storage; this RFC does not.
- What it proposes (review drafts, in Chinese):
- a phased rewrite in which Rust services coexist with Java behind gRPC (
docs/rust-rewrite-design.md, sections 3 and 5) - the boundary principle "explicit RPC or C ABI, avoiding fine-grained cross-language calls" (principle 5 in section 2, line 21)
- a phase 0 of baselines and contracts, with golden samples generated by the Java version that a Rust version must pass (section 8.1)
- data semantics compared through normalized comparators rather than internal bytes (section 8.2)
- an engineering standard for Rust code (
docs/rust-engineering-standard.md)
- a phased rewrite in which Rust services coexist with Java behind gRPC (
- What it contains:
- a standalone model of PD partition ranges (
tools/rust-partition-poc) - a separate process that reads RocksDB files (
tools/rust-store-reader) - cluster smoke scripts (
tools/rust-rewrite-cluster) - a linearizability checker (
tools/raft-linearizability)
- a standalone model of PD partition ranges (
Where the two meet: on the measured path and loads, this RFC's measurements are compatible with principle 5. The byte-exact corpus of #3146 and the branch's normalized comparison answer different questions: persisted encoded keys need exact bytes; external semantics can be compared after normalization.
3. Boundary
Each candidate faced three options:
- Improved Java. Argued first; measured when it decides the recommendation, as it did for
decodeSignedB64(section 2). ForIntSet/IntMapit is argued only (JEP 471). - In-process JNI. The declared path: the jni-rs 0.22.4
with_envwrapper around raw JNI calls on stack buffers (rfc/thresholds.md). The wrapper catches Rust panics that unwind and turns them into Java exceptions.spike/rust/src/lib.rsstates the JNI obligations of each call; section 4 lists the checks. FFM is final only from JDK 22 (JEP 454), so it needs an explicit baseline decision; it was not measured. - Out-of-process. Excluded from the initial perimeter of scalar operations as a scope choice, not by measurement.
A crossing through the declared path with no JNI call inside the closure (passLong) costs 59 ns (57.9 to 60.1), against a harness baseline of 3.6 ns (results/20260923T134724Z-stage-b). That cost belongs to the measured path; it is not attributed to JNI or to the panic capture. It sets the verdicts for the scalar operations, whose budgets (half the Java cost) are about 2 to 13 ns.
Removing with_env would not force panic = "abort": a directly written boundary could keep catch_unwind with panic = "unwind". Its exception handling, references, and error paths would then need their own design and checks, and nothing here shows it would be faster or as robust.
If a future candidate is recommended, its shape would be:
- The existing Java class keeps its interface.
- A Java implementation and a native one sit behind an internal seam chosen at class initialisation.
- The Java implementation is the fallback for availability and compatibility, not for recovery from a fatal native failure.
- The native library loads where
HugeSecurityManagerallows it; it refuses loads from Gremlin workers (hugegraph-server/hugegraph-core/.../security/HugeSecurityManager.java:144-149). Loading at startup is one option.
Unpaired surrogates: on the declared read path (GetStringRegion), UTF-16 units arrive unchanged (results/20260923T134724Z-stage-b/check.txt).
4. Validation
Verified by the spike (spike/VALIDATION.md, results/):
- Corpus: the #3146 corpus is an oracle of recorded outcomes; it records values and exception classes, not messages. On its 740
decodeSignedB64cases the improved decoder gives the recorded values and exception classes. - Differential test: a separate comparison with the original decoder on 20,020 generated cases, including invalid input, overflow, null, and surrogates. Values, exception classes, and messages are the same, except the NullPointerException message, which the check does not compare. Both checks cover the verified cases, not every possible input.
- Measurement guards: each run refuses uncommitted changes, rebuilds from the recorded revision, checks the discovered benchmarks against the mapping and the loaded code against the recorded build, and records its commits (
rfc/appendix-method.md). - Boundary checks: deterministic results, null references, a panic, and an out-of-range array each give the expected Java outcome, with and without
-Xcheck:jni, which reported nothing. The out-of-memory branches (a NULL result fromNewStringUTForNewByteArray) were not exercised.
Criteria for a future proof of concept, not verified here:
- concurrency and resource release for stateful candidates
- fallback when the native library is absent or of the wrong version
- observability of which implementation runs
- upgrade and rollback where a persisted format is involved
5. CI, packaging, security
No candidate is recommended, so this section sizes nothing. Constraints for any later candidate:
- External GitHub Actions are pinned to commit SHAs (https://infra.apache.org/github-actions-policy.html). The rust-toolchain action is allowlisted only at a fixed SHA in
approved_patterns.ymlof https://github.com/apache/infrastructure-actions. - Convenience binaries contain only what is built from the source release (https://www.apache.org/legal/release-policy.html).
- The jar's
LICENSE/NOTICEmatch its contents. - Rust dependency licences are listed, with MPL-2.0 crates treated as Category B (https://www.apache.org/legal/resolved.html#category-b).
--enable-native-accessis documented (JEP 472, JDK 24, https://openjdk.org/jeps/472).
The engineering standard on refactor/rust-rewrite-design (docs/rust-engineering-standard.md) already lists toolchain pinning, clippy -D warnings, cargo audit, cargo deny, an unsafe policy, and no panic across FFI.
6. Risks, limits, open questions
Limits of the evidence:
- One laptop (Intel Core Ultra 7 155H,
powersavegovernor, no CPU pinning), one JDK (Temurin 11.0.30), one Rust toolchain (1.85.0); aarch64 not measured. - Every measurement ran on JDK 11. If #3117 moves the baseline to Java 17, the Java costs, and with them the budgets, need measuring again; FFM would still need JDK 22.
- Stage A and stage B ran in different campaigns, and other work ran on the machine during stage A (
rfc/appendix-method.md). - The borderline
decodeSignedB64large case changed verdict between two campaigns:explorein the first,stopin the second.
How the loads relate to production: on the index read path, decodeSignedB64 decodes the index label id, which is a schema id. User schema ids need at most 6 characters in this encoding. The other ofStoredString(..., LONG) callers are reached only through TableSerializer.readId, and no concrete TableSerializer exists at 83ef9f3fa. Section 6 of rfc/RFC.md gives the full chain. This bounds the length, not the distribution of lengths in production. The mixed load covers the full long range, the large load the 1000 values below Long.MAX_VALUE, and the small load values from 0 to 999 (1 to 2 characters). On the small load the improved Java costs 9.5 ns instead of 25.2 ns; that saving is measured on that load and not established per index row in production.
Java-side findings, not Rust questions (rfc/java-findings.md):
- Secondary-index prefixes convert n(n+1)/2 elements per update.
CollectionFactoryalways returns the concurrent segmentedIntSet/IntMap.- Server and store declare different analyzer versions, with no server path in the repository that triggers a store-side index build.
Open questions for the maintainers:
- Is Server core still the preferred first area, given the storage-side exploration on
refactor/rust-rewrite-designand the toolchain priority in apache/hugegraph-toolchain#748? - Mentor and Rust reviewers: to be defined. This RFC names no one as responsible.
- Is the improved
decodeSignedB64wanted as a small optimization? A checked prototype exists:ImprovedLongEncoding, a class inhugegraph-testwith a copy of the alphabet. This RFC claims no application-level benefit. The prefix conversion has a different motivation and would be evaluated separately. - Is a Jieba token-equality run worth doing?
- #3117 timing: when the Java baseline moves, should the Java measurements be repeated before the maintainers decide? A raw
extern "system"no-op control withoutwith_envwould also show whether thevalidB64Charverdicts on the late and absent loads depend on the wrapper. That control is deferred (section 3). - How do the maintainers want to bring this proposal to discussion and decision on the public mailing list?
Guardrails from #3110
| Guardrail | Where |
|---|---|
| Scope, owner, mentor, non-goals | Sections 1 and 6 (mentor open) |
| API, protocol, storage-format, operational compatibility | Sections 2 and 3; persisted formats (BytesBuffer rows, LongEncoding/NumericUtil outputs inside index keys, analyzer tokens) not changed |
| Differential, concurrency, fault-injection, upgrade/rollback tests | Section 4; concurrency, fault injection, and upgrade are criteria for a future PoC, out of scope here |
| Latency, throughput, memory, recovery benchmarks | Latency measured; throughput, memory, and recovery out of scope for a first selection (#3223 covers storage) |
| Security/dependency checks and platforms | Section 5; only x86_64 measured |
| Observability, deployment, coexistence, migration, rollback | Section 3 (fallback shape) and section 4 (criteria); no deployment proposed |
| Acceptance criteria and documentation | rfc/thresholds.md; this document and rfc/appendix-method.md |
Changes since publication
- 2026-09-24: section 2 defines its terms before the table, and the text is reorganised into shorter sentences and lists. Verification chains now link to the evidence repository instead of being repeated here. The evidence links point to commit
b094369. Since publication, the evidence repository marksrfc/RFC.mdas the text first published here, corrects the load-average note in the appendix, states the stage A window in UTC, and removes repeated tables fromrfc/matrix.md. No number, source or conclusion changed.
- 主要语言
- Java
- 星标
- 3.2k
- 派生
- 637
- 平均合并
- 3 天 17 小时
- 30 天内合并 PR
- 22
环境准备
从这里开始
- 先读完整个 Issue,再读项目的贡献指南。
- 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
- Fork 仓库,在一个分支上完成修改。
- 提交 Pull Request,并在描述里引用这个 Issue 编号。
apache/hugegraph 的其他 Issue
-
难度 2/5 1-3 小时 新手友好度 70/100
apache/hugegraph#3231 · 1 条评论 ·
维护者通常 1 天内回复
-
bug
难度 2/5 1-3 小时 新手友好度 64/100
apache/hugegraph#3142 · 7 条评论 ·
维护者通常 1 天内回复
-
难度 5/5 一周以上 新手友好度 15/100
维护者通常 1 天内回复
-
难度 4/5 3-5 天 新手友好度 35/100
维护者通常 1 天内回复
-
feature
难度 3/5 1-2 天 新手友好度 65/100
apache/hugegraph#3236 · 3 条评论 ·
维护者通常 1 天内回复
相似的 Issue
-
难度 1/5 1 小时以内 新手友好度 92/100
-
难度 2/5 1-3 小时 新手友好度 68/100
-
难度 2/5 1-3 小时 新手友好度 72/100
oracle/javavscode#652 ·
-
难度 2/5 1-3 小时 新手友好度 78/100
OpenAPITools/openapi-generator#25014 ·
维护者通常 1 天内回复
-
难度 1/5 1 小时以内 新手友好度 92/100
AloisSeckar/demos-java#380 ·