Skip to content

optimize: Optimize rockdb batch query performance#2982

Open
lokidundun wants to merge 19 commits into
apache:masterfrom
lokidundun:improvequery
Open

optimize: Optimize rockdb batch query performance#2982
lokidundun wants to merge 19 commits into
apache:masterfrom
lokidundun:improvequery

Conversation

@lokidundun
Copy link
Copy Markdown
Contributor

@lokidundun lokidundun commented Mar 29, 2026

Purpose of the PR

Main Changes

The core change in this PR is a refactor/optimization of the RocksDB batch query path in three main areas: request coalescing, result backfilling, and shard-level parallelism control.
The focus is not on the API layer, but on how batch get is executed internally and how objects are reused, with the goal of reducing query latency and memory jitter under high concurrency.

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:
    • xxx

Does this PR potentially affect the following parts?

Documentation Status

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

@dosubot dosubot Bot added size:XS This PR changes 0-9 lines, ignoring generated files. perf store Store module labels Mar 29, 2026
@lokidundun lokidundun closed this Mar 30, 2026
@github-project-automation github-project-automation Bot moved this from In progress to Done in HugeGraph PD-Store Tasks Mar 30, 2026
@lokidundun lokidundun reopened this Mar 30, 2026
@github-project-automation github-project-automation Bot moved this from Done to In progress in HugeGraph PD-Store Tasks Mar 30, 2026
@lokidundun
Copy link
Copy Markdown
Contributor Author

This CI failure is unrelated to the changes in this PR. The PR focuses on optimizing RocksDB batch query performance, and the failing build check does not involve the code modified here.

@imbajin
Copy link
Copy Markdown
Member

imbajin commented Mar 31, 2026

Already rerun CI (also could check the tests could pass in local env)

@imbajin
Copy link
Copy Markdown
Member

imbajin commented Mar 31, 2026

seems a little strange (after rerun 3 times still failed)

image

@dosubot dosubot Bot added size:M This PR changes 30-99 lines, ignoring generated files. and removed size:XS This PR changes 0-9 lines, ignoring generated files. labels Apr 6, 2026
@lokidundun
Copy link
Copy Markdown
Contributor Author

@imbajin could you please take another look when you are convenient❤️

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces batched backend fetching for queryVerticesByIds() to reduce overhead when querying many vertex ids (e.g., g.V(id1, id2, ...)) by splitting backend id lookups into multiple IdQuery requests.

Changes:

  • Collect backend-only vertex ids during queryVerticesByIds() and issue backend queries in batches using query.batch_size.
  • Add per-batch capacity checks before executing backend queries.
  • Minor formatting adjustment in RocksDB table iterator helper signature.

Reviewed changes

Copilot reviewed 1 out of 2 changed files in this pull request and generated 5 comments.

File Description
hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/tx/GraphTransaction.java Batch backend id queries in queryVerticesByIds() instead of building a single IdQuery for all backend ids.
hugegraph-server/hugegraph-rocksdb/src/main/java/org/apache/hugegraph/backend/store/rocksdb/RocksDBTable.java Formatting-only change to method parameter indentation.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +809 to +814
if (!backendIds.isEmpty()) {
// Query from backend store
query.mustSortByInput(false);
Iterator<HugeVertex> it = this.queryVerticesFromBackend(query);
QueryResults.fillMap(it, vertices);
final int batch = this.batchSize > 0 ? this.batchSize : backendIds.size();
for (int i = 0; i < backendIds.size(); i += batch) {
int end = Math.min(i + batch, backendIds.size());
IdQuery query = new IdQuery(type);
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR title says this is a RocksDB optimization, but the functional change here is backend-agnostic batching in GraphTransaction (affecting all stores, especially RPC-based ones). Please align the PR title (or add RocksDB-specific changes) so the intent matches what’s actually being modified.

Copilot uses AI. Check for mistakes.
Comment on lines 787 to 805
@@ -799,17 +800,30 @@ protected Iterator<Vertex> queryVerticesByIds(Object[] vertexIds, boolean adjace
// Found from local tx
vertices.put(vertex.id(), vertex);
} else {
// Prepare to query from backend store
query.query(id);
// store the IDs queried from backend
backendIds.add(id);
}
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change allocates backendIds as a second full list of ids (in addition to ids), which doubles memory usage for large g.V(id1,id2,...) calls (up to Query.DEFAULT_CAPACITY). Consider batching/issuing backend IdQuery requests incrementally during the main loop (flush when reaching batch size) to avoid retaining all backend ids at once.

Copilot uses AI. Check for mistakes.
Comment on lines +811 to +818
final int batch = this.batchSize > 0 ? this.batchSize : backendIds.size();
for (int i = 0; i < backendIds.size(); i += batch) {
int end = Math.min(i + batch, backendIds.size());
IdQuery query = new IdQuery(type);
for (int j = i; j < end; j++) {
Id id = backendIds.get(j);
query.query(id);
}
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With batching, duplicated ids that fall into different batches will trigger repeated backend reads/RPCs for the same id. You can keep the output behavior (duplicates preserved via ids) while deduplicating backend fetches (e.g., track a seen-set for backendIds or build per-batch unique ids) to avoid redundant backend queries.

Copilot uses AI. Check for mistakes.
query.mustSortByInput(false);
Iterator<HugeVertex> it = this.queryVerticesFromBackend(query);
QueryResults.fillMap(it, vertices);
final int batch = this.batchSize > 0 ? this.batchSize : backendIds.size();
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

final int batch = this.batchSize > 0 ? this.batchSize : backendIds.size(); is effectively always this.batchSize because query.batch_size is configured with a minimum of 1 (see CoreOptions.QUERY_BATCH_SIZE). Removing the dead fallback branch will simplify the code.

Suggested change
final int batch = this.batchSize > 0 ? this.batchSize : backendIds.size();
final int batch = this.batchSize;

Copilot uses AI. Check for mistakes.
Comment on lines +809 to +826
if (!backendIds.isEmpty()) {
// Query from backend store
query.mustSortByInput(false);
Iterator<HugeVertex> it = this.queryVerticesFromBackend(query);
QueryResults.fillMap(it, vertices);
final int batch = this.batchSize > 0 ? this.batchSize : backendIds.size();
for (int i = 0; i < backendIds.size(); i += batch) {
int end = Math.min(i + batch, backendIds.size());
IdQuery query = new IdQuery(type);
for (int j = i; j < end; j++) {
Id id = backendIds.get(j);
query.query(id);
}
// Single batch capacity check
Query.checkForceCapacity(query.idsSize());

// Query from backend store
query.mustSortByInput(false);
Iterator<HugeVertex> it = this.queryVerticesFromBackend(query);
QueryResults.fillMap(it, vertices);
}
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new multi-batch path isn’t covered by tests. Please add a unit/integration test that exercises queryVerticesByIds() with vertexIds.length > query.batch_size, including (1) duplicates across a batch boundary and (2) mixed local-tx + backend ids, to ensure results and NotFoundException behavior remain unchanged.

Copilot uses AI. Check for mistakes.
@lokidundun lokidundun changed the title optimize: Optimize RocksDB batch query performance optimize: Optimize batch query performance Apr 7, 2026
// NOTE: allowed duplicated vertices if query by duplicated ids
List<Id> ids = InsertionOrderUtil.newList();
Map<Id, HugeVertex> vertices = new HashMap<>(vertexIds.length);
Set<Id> fetchedIds = InsertionOrderUtil.newSet();
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ 这里新增 fetchedIds 之后,非相邻的重复 id 也会被全局去重;而旧逻辑里 IdQuery.query() 只会折叠相邻重复 id。最终返回结果看起来应该还是保持重复输出,但真实的后端访问路径已经变了。建议补一个回归测试,至少覆盖 超过 query.batch_size跨 batch 的重复 id缺失 id + checkMustExist 这几个组合场景,避免后面再改这里时把语义悄悄带偏。

Map<Id, HugeVertex> vertices = new HashMap<>(vertexIds.length);
Set<Id> fetchedIds = InsertionOrderUtil.newSet();
IdQuery batchQuery = null;
final int batchSize = this.batchSize;
Copy link
Copy Markdown
Member

@imbajin imbajin Apr 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ 这里把 batching 放在 GraphTransaction 通用层后,会影响所有 backend,而不只是 issue #2674 里提到的 RPC backend。

以 RocksDB 为例,当前 queryByIds() 仍然是逐 id 展开查询,并没有真正走 multi-get;现在强制按 query.batch_size 拆成多个 IdQuery,很可能只是增加额外的 query/iterator 次数。建议把这类分批策略下沉到具体 backend,或者至少通过 feature/store type 把它限定在 HBase/HStore 这类 RPC backend 上,避免把针对性优化变成全局行为变化。

PS: 后续我们应该让 RocksDB 使用上原生的 multi-get API (这应该是之前的 TODO)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. GraphTransaction 不再按 batch_size 拆分 ID,一次性下发完整 IdQuery;
  2. RocksDBTables.Vertex/Edge 已覆写 queryByIds(),在 !session.hasChanges() 时走 multiGetAsList() 原生批量读取,脏 session 安全回退到逐 id scan;

@dosubot dosubot Bot removed the size:M This PR changes 30-99 lines, ignoring generated files. label Apr 16, 2026
}
return super.queryByIds(session, ids);
}
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Vertex 和 Edge 的 queryByIds 逻辑完全重复

Vertex.queryByIds()Edge.queryByIds() 代码完全一致(5 行相同逻辑)。可以考虑:

  1. 提取到父类 RocksDBTable.queryByIds() 中统一做 hasChanges() 判断
  2. 或者提供一个公共的 helper 方法

这样可以减少重复,也降低后续维护时两处不一致的风险。

Collection<Id> ids) {
// TODO: use getByIds() after batch version multi-get is ready
if (!session.hasChanges()) {
return this.getByIds(session, ids);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ fallback 路径的实际效果需确认

hasChanges() == true 时,fallback 到 super.queryByIds() 会对每个 ID 调用 this.queryById()。而 Vertex/Edge 已经 override queryByIdgetById(point get),所以 fallback 路径实际也会调用 getById

RocksDBStdSessionsget() 方法有 assert !this.hasChanges() 断言——也就是说 fallback 路径在开启断言的环境下依然会触发 assert 失败。

需要确认:hasChanges() == true 时是否真的能安全走 fallback?还是说这个分支在 RocksDB 后端实际上永远不会被触发?


@Override
protected BackendColumnIterator queryByIds(RocksDBSessions.Session session,
Collection<Id> ids) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 PR 描述与实际改动不完全匹配

PR 描述提到 "improves the performance of Gremlin queries... when using RPC-based backends such as HBase and HStore",但代码改动全在 hugegraph-rocksdb 模块。建议更新 PR 描述,准确反映本次优化的范围是 RocksDB 后端。

import org.apache.hugegraph.unit.id.SplicingIdGeneratorTest;
import org.apache.hugegraph.unit.mysql.MysqlUtilTest;
import org.apache.hugegraph.unit.mysql.WhereBuilderTest;
import org.apache.hugegraph.unit.rocksdb.RocksDBTableQueryByIdsTest;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 import 未按字母序排列

RocksDBTableQueryByIdsTest 的 import 插在 RocksDBCountersTest 之前,不符合字母顺序。建议调整为:

Suggested change
import org.apache.hugegraph.unit.rocksdb.RocksDBTableQueryByIdsTest;
import org.apache.hugegraph.unit.rocksdb.RocksDBCountersTest;
import org.apache.hugegraph.unit.rocksdb.RocksDBSessionTest;
import org.apache.hugegraph.unit.rocksdb.RocksDBSessionsTest;
import org.apache.hugegraph.unit.rocksdb.RocksDBTableQueryByIdsTest;

/**
* Subclass that exposes the protected queryByIds for testing.
*/
private static class TestEdgeTable extends RocksDBTables.Edge {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 只测了 out edge 方向

TestEdgeTable 固定传 true(out edge),in edge 方向未覆盖。虽然逻辑相同,但建议至少补一个 in 方向的 case 或在注释中说明为什么只测 out。

Collection<Id> ids) {
if (ids.size() == 1) {
return this.queryById(session, ids.iterator().next());
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

‼️ 这里把 getByIds() 放到 RocksDBTable 父类会改变非 Vertex/Edge 表的查询语义。父类 queryById() 仍然使用 session.scan(table, id.asBytes()),并且注释里说明 vertex/schema 目前还不能统一改成 point get;但这个分支会让所有未 override queryByIds() 的 RocksDB table 在多 id 查询时改走 exact multi-get。

这对 Vertex / Edge 是合理的,因为它们的 queryById() 已经是 getById();但 schema/index 等表原本依赖 prefix scan,放在父类可能导致多 id 查询查不到本应返回的列。建议父类保留旧的 scan-based 实现,把 multi-get 下沉到 Vertex / Edge 的 override 中。

Suggested change
}
// NOTE: this will lead to lazy create rocksdb iterator
return BackendColumnIterator.wrap(new FlatMapperIterator<>(
ids.iterator(), id -> this.queryById(session, id)
));

然后在 RocksDBTables.Vertex / RocksDBTables.Edge 中分别 override queryByIds(),仅在 !session.hasChanges() 时调用 getByIds()

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

好的,还是回到最初的方案,将重复的代码抽成一个公共的方法调用

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

Comments suppressed due to low confidence (1)

hugegraph-server/hugegraph-rocksdb/src/main/java/org/apache/hugegraph/backend/store/rocksdb/RocksDBTable.java:220

  • The new hasChanges() branch doesn’t actually make reads safe when the session has pending write-batch changes: the fallback path still calls queryById() which (in RocksDBStdSessions.StdSession) ultimately uses get()/scan() methods that assert !this.hasChanges() as well. So with assertions enabled, queryByIds() will still fail for hasChanges()==true, and the branch is effectively dead/false confidence.

Consider either (1) removing the hasChanges() special-casing and keeping a single fast path (letting the existing assertions guard misuse), or (2) implementing a real read-with-writebatch strategy (e.g., using a separate read-only session/snapshot, or a batch-with-index approach) and adapting both single-get and multi-get accordingly.

        if (!session.hasChanges()) {
            return this.getByIds(session, ids);
        }

        // NOTE: this will lead to lazy create rocksdb iterator
        return BackendColumnIterator.wrap(new FlatMapperIterator<>(
                ids.iterator(), id -> this.queryById(session, id)
        ));

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +179 to +186
};

BackendColumnIterator iter = this.vertexTable.queryByIds(mockSession, ids);

Map<String, String> results = toResultMap(iter);
Assert.assertEquals(2, results.size());
Assert.assertEquals("value1", results.get("v1"));
Assert.assertEquals("value2", results.get("v2"));
Copy link

Copilot AI Apr 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

testVertexQueryByIdsFallbackWhenHasChanges() forces hasChanges() to return true, but all read operations still delegate to an underlying real session whose hasChanges() is false. This doesn’t model the actual “pending write batch” state (in RocksDBStdSessions.StdSession, get/scan assert !hasChanges()), so the test can pass even though reads would still be invalid when the real session has pending changes.

Suggestion: either change the test to expect failure when hasChanges()==true (if reads are intentionally unsupported in that state), or rework it to use a session implementation that truly supports reads with pending writes (if that behavior is desired).

Suggested change
};
BackendColumnIterator iter = this.vertexTable.queryByIds(mockSession, ids);
Map<String, String> results = toResultMap(iter);
Assert.assertEquals(2, results.size());
Assert.assertEquals("value1", results.get("v1"));
Assert.assertEquals("value2", results.get("v2"));
@Override
public BackendColumn get(String table, byte[] key) throws RocksDBException {
throw new AssertionError(
"reads should not be performed when hasChanges");
}
};
try {
this.vertexTable.queryByIds(mockSession, ids);
Assert.fail("queryByIds should fail when session has pending changes");
} catch (AssertionError e) {
Assert.assertTrue(e.getMessage().contains("hasChanges"));
}

Copilot uses AI. Check for mistakes.
@lokidundun lokidundun requested a review from imbajin April 26, 2026 14:28
@github-actions
Copy link
Copy Markdown

Due to the lack of activity, the current pr is marked as stale and will be closed after 180 days, any update will remove the stale label

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 5 comments.

Comment on lines +174 to +179
@Override
public byte[] get(String table, byte[] key) {
throw new AssertionError(
"reads should not be performed when hasChanges");
}

Comment on lines +187 to +195
try {
BackendColumnIterator iter = this.vertexTable.queryByIds(mockSession, ids);
// FlatMapperIterator is lazy; trigger evaluation to hit the mock
iter.hasNext();
Assert.fail("queryByIds should fail when session has pending changes");
} catch (AssertionError e) {
Assert.assertTrue(e.getMessage().contains("hasChanges"));
}
}
Comment on lines +219 to +233
protected BackendColumnIterator queryByIdsWithGet(RocksDBSessions.Session session,
Collection<Id> ids) {
if (ids.size() == 1) {
return this.queryById(session, ids.iterator().next());
}

if (!session.hasChanges()) {
return this.getByIds(session, ids);
}

// NOTE: this will lead to lazy create rocksdb iterator
return BackendColumnIterator.wrap(new FlatMapperIterator<>(
ids.iterator(), id -> this.queryById(session, id)
));
}
Comment on lines +174 to +179
@Override
public byte[] get(String table, byte[] key) {
throw new AssertionError(
"reads should not be performed when hasChanges");
}

Comment on lines +187 to +195
try {
BackendColumnIterator iter = this.vertexTable.queryByIds(mockSession, ids);
// FlatMapperIterator is lazy; trigger evaluation to hit the mock
iter.hasNext();
Assert.fail("queryByIds should fail when session has pending changes");
} catch (AssertionError e) {
Assert.assertTrue(e.getMessage().contains("hasChanges"));
}
}
Copy link
Copy Markdown
Member

@imbajin imbajin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found one additional test coverage issue on the current head. I also +1'd the existing Copilot comments that already cover the hasChanges fallback problem.

List<Id> ids = Arrays.asList(id1, id2, id1);
BackendColumnIterator iter = this.vertexTable.queryByIds(this.rocks.session(), ids);

Map<String, String> results = toResultMap(iter);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test does not actually protect the duplicate-id behavior because toResultMap() collapses duplicate rows by key before the assertion. If queryByIds() regressed and returned v1, v2, v1, the map would still contain only two entries and this test would pass. Since this PR explicitly deduplicates ids before multi-get, please assert the iterator output sequence/count directly (for example collect the column names into a list) so duplicate backend results would fail the test.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

Comments suppressed due to low confidence (1)

hugegraph-server/hugegraph-rocksdb/src/main/java/org/apache/hugegraph/backend/store/rocksdb/RocksDBTable.java:250

  • getByIds() checks ids.size()==1 before de-duplicating, so an input like [id1, id1] takes the multi-get path even though there's only one unique key. Since this PR explicitly de-dups ids for performance, it would be more consistent (and slightly cheaper) to de-duplicate first and then apply the single-key fast path.
    protected BackendColumnIterator getByIds(RocksDBSessions.Session session,
                                             Collection<Id> ids) {
        if (ids.size() == 1) {
            return this.getById(session, ids.iterator().next());
        }

        Collection<Id> uniqueIds = ids instanceof Set ? ids : new LinkedHashSet<>(ids);
        List<byte[]> keys = new ArrayList<>(uniqueIds.size());
        for (Id id : uniqueIds) {
            keys.add(id.asBytes());
        }
        return session.get(this.table(), keys);
    }

Comment on lines 182 to 186
@Override
protected BackendColumnIterator queryByIds(RocksDBSessions.Session session,
Collection<Id> ids) {
// TODO: use getByIds() after batch version multi-get is ready
return super.queryByIds(session, ids);
return this.queryByIdsWithGet(session, ids);
}
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Catch! This PR is RocksDB-only. I’ve updated the description to change closes 2674 to related 2674


protected BackendColumnIterator queryByIdsWithGet(RocksDBSessions.Session session,
Collection<Id> ids) {
E.checkState(!session.hasChanges(),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking: enabling queryByIdsWithGet() for vertex/edge changes pending-write behavior: any session.hasChanges() now throws before even the single-id path. Please confirm all callers reach this only after commit/rollback, or keep the old per-id fallback for pending sessions and add a test through a public query path.

@codecov
Copy link
Copy Markdown

codecov Bot commented Jun 1, 2026

Codecov Report

❌ Patch coverage is 66.66667% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 39.02%. Comparing base (9126c80) to head (cb98e63).
⚠️ Report is 22 commits behind head on master.

Files with missing lines Patch % Lines
.../hugegraph/backend/store/rocksdb/RocksDBTable.java 57.14% 1 Missing and 2 partials ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##             master    #2982      +/-   ##
============================================
+ Coverage     35.57%   39.02%   +3.44%     
- Complexity      333      622     +289     
============================================
  Files           801      814      +13     
  Lines         67592    69210    +1618     
  Branches       8790     9106     +316     
============================================
+ Hits          24045    27008    +2963     
+ Misses        40985    39377    -1608     
- Partials       2562     2825     +263     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Copy Markdown
Member

@imbajin imbajin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

当前 head 上还有一个阻塞合入的 correctness 问题:Edge 的公开查询路径可能因为 RocksDB table 层全局去重而吞掉非相邻重复 id。hasChanges() 行为变化已有现有线程覆盖,这里不重复发 inline。


List<byte[]> keys = new ArrayList<>(ids.size());
for (Id id : ids) {
Collection<Id> uniqueIds = ids instanceof Set ? ids : new LinkedHashSet<>(ids);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

‼️ 这里的全局去重会让 Edge 的公开查询路径吞掉非相邻重复 id,属于用户可见的结果集回归。

完整路径是:

graph.edges(e1, e2, e1)
        |
        v
GraphTransaction.queryEdgesByIds()
        |
        |  IdQuery.query() 只折叠相邻重复 id
        |  [e1, e2, e1] => query.idsSize()==3, ids.size()==3
        v
edges.isEmpty() && query.idsSize() == ids.size()
        |
        |  命中 fast path,直接返回 backend iterator
        v
RocksDBTables.Edge.queryByIds()
        |
        v
RocksDBTable.getByIds()
        |
        |  new LinkedHashSet<>(ids) 全局去重
        v
[e1, e2, e1] => [e1, e2]

也就是说,GraphTransaction 只有在检测到重复 id 时才会回到按原始 ids 重建结果的慢路径;但当前 fast path 只能识别相邻重复,识别不了 e1, e2, e1 这种非相邻重复。旧的 scan/flat-map 路径会按输入 id 展开 3 次查询,新路径在这里被压成 2 个 key,最终返回结果会少一条。

建议不要在 Edge 的这条 fast path 上做 table-level 全局去重,或者在 transaction 层改成能检测任意重复 id 后再决定是否直接返回 backend iterator。

this.rocks.session().put(this.vertexTable.table(), id2.asBytes(), getBytes("value2"));
this.commit();

List<Id> ids = Arrays.asList(id1, id2, id1);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ 这个 helper-level 测试把 [id1, id2, id1] 固化成“RocksDB table 层去重后只返回 2 条”,但它没有覆盖真正会出问题的公开 Edge 查询路径。

需要补一个从 Graph API / GraphTransaction 进入的回归测试,至少覆盖非相邻重复 edge id:

Object e1 = ...;
Object e2 = ...;
List<Edge> edges = ImmutableList.copyOf(graph.edges(e1, e2, e1));
Assert.assertEquals(3, edges.size());

图例:

helper test:
  TestEdgeTable.queryByIds([e1, e2, e1])
          -> 只验证 table 层去重行为

真实风险路径:
  graph.edges(e1, e2, e1)
          -> GraphTransaction fast path
          -> RocksDB Edge queryByIds
          -> table 层去重
          -> 返回 2 条而不是 3 条

如果保留当前测试,它会让“后端吞掉重复 id”看起来像预期行为;但对 graph.edges(...) 的公开语义来说,重复输入应保留重复输出,现有 graph.edges(id, id) 测试已经固定了这个契约。

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

inactive perf size:L This PR changes 100-499 lines, ignoring generated files. store Store module

Projects

Status: In progress

Development

Successfully merging this pull request may close these issues.

5 participants