Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,9 @@ public void onSnapshotSave(final SnapshotWriter writer) throws HgStoreException
Integer groupId = partitionEngine.getGroupId();
AtomicInteger state = businessHandler.getState(groupId);
if (state != null && state.get() == BusinessHandler.doing) {
return;
throw new HgStoreException(
Comment thread
vaijosh marked this conversation as resolved.

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.

⚠️ Blocking: yes. This guard is a non-atomic check followed by the save at line 105. dbCompaction runs asynchronously, initializes compactionState at BusinessHandlerImpl.java:1414, then sets doing at 1418 before compactRange() at 1420; it does not synchronize with this check. If compaction starts after getState() returns (including while the state entry is still null), this path still calls saveSnapshot during compaction, so the change does not guarantee that overlapping snapshots are rejected. Please coordinate the state transition and snapshot save with the same per-partition lock or another atomic protocol, or re-check the state under such a lock immediately before saving. Evidence: exact head SnapshotHandler.java:97-105 and BusinessHandlerImpl.java:1408-1421.

String.format("Partition %d is busy (compaction in progress), " +
"snapshot save skipped", groupId));

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.

🧹 Three small things about this exception, assuming the throw survives the discussion on line 99.

The text says the save was skipped, but the save now fails and reaches the operator as a raft EIO status (PartitionStateMachine.java:199-201). "Skipped" describes the behaviour this PR removes; say what happened instead, for example Partition %d snapshot save failed: compaction in progress.

new HgStoreException(String) resolves to EC_FAIL (1000). The neighbouring save failure uses a specific code, EC_RKDB_EXPORT_SNAPSHOT_FAIL (BusinessHandlerImpl.java:1123); a dedicated code here would be easier to grep for in the field.

Line 182 embeds a non-ASCII em dash in the new log.warn. At this head the only Java files under hugegraph-store containing one are the three this PR touches (git grep -l '—' 0e1c319 -- 'hugegraph-store/*.java'); please keep log text ASCII.

}
// rocks db snapshot
final String graphSnapshotDir = snapshotDir + File.separator + SNAPSHOT_DATA_PATH;
Expand Down Expand Up @@ -172,8 +174,14 @@ public void onSnapshotLoad(final SnapshotReader reader, long committedIndex) thr

// No need to load locally saved snapshots
if (shouldNotLoad(reader)) {
log.info("skip to load snapshot because of should_not_load flag");
return;
final File dataDir = new File(snapshotDir + File.separator + SNAPSHOT_DATA_PATH);

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.

⚠️ Nested inside the should_not_load branch, this covers only the flag-present variant of the signature #3162 records.

The issue describes the bad directory as __raft_snapshot_meta present, data/ missing, and should_not_load "often missing for early-return path". With the flag absent, shouldNotLoad(reader) is false (SnapshotHandler.java:217-220), this block is skipped, and control reaches businessHandler.loadSnapshot exactly as before. That call already fails: RocksDBSession.loadSnapshot throws Snapshot file %s not exists (RocksDBSession.java:740-745), wrapped by BusinessHandlerImpl.java:1128-1137. So the more common variant is unaffected, and it fails with a RocksDB path error rather than a corruption diagnosis.

Requested change: hoist the data/ check above the shouldNotLoad test and throw with a message naming the corrupt snapshot directory, so both variants are reported the same way.

if (dataDir.isDirectory()) {
log.info("skip to load snapshot because of should_not_load flag");
return;
}
log.warn("Raft {} should_not_load flag present but data dir {} is missing — " +
"snapshot is corrupt, proceeding to load path",
partitionEngine.getGroupId(), dataDir);
}

// Use snapshot directly
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import org.apache.hugegraph.store.meta.Partition;
import org.apache.hugegraph.store.meta.ShardGroup;
import org.apache.hugegraph.store.options.HgStoreEngineOptions;
import org.apache.hugegraph.store.options.JobOptions;
import org.apache.hugegraph.store.options.RaftRocksdbOptions;
import org.apache.hugegraph.store.pd.FakePdServiceProvider;
import org.junit.AfterClass;
Expand Down Expand Up @@ -61,6 +62,11 @@ public static void initEngine() {
options.setGrpcAddress("127.0.0.1:6511");
options.setRaftAddress("127.0.0.1:6510");
options.setDataTransfer(new DataManagerImpl());
JobOptions jobOptions = new JobOptions();
jobOptions.setUninterruptibleCore(2);
jobOptions.setUninterruptibleMax(8);
jobOptions.setUninterruptibleQueueSize(1024);
options.setJobConfig(jobOptions);

options.setFakePdOptions(new HgStoreEngineOptions.FakePdOptions() {{
setStoreList("127.0.0.1");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,12 @@
package org.apache.hugegraph.store.core.snapshot;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThrows;

import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.List;
import java.util.Map;
import java.util.Set;
Expand All @@ -29,8 +32,17 @@
import org.apache.hugegraph.store.core.StoreEngineTestBase;
import org.apache.hugegraph.store.meta.Partition;
import org.apache.hugegraph.store.snapshot.HgSnapshotHandler;
import org.apache.hugegraph.store.snapshot.SnapshotHandler;
import org.apache.hugegraph.store.util.HgStoreException;

import com.alipay.sofa.jraft.entity.RaftOutter;

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.

🧹 This block duplicates the four imports already present below it.

Lines 38-41 add RaftOutter, SnapshotReader, SnapshotWriter and Message; lines 47-50 import the same four types. Duplicate single-type imports compile, and the checkstyle plugin is not bound for hugegraph-store (only hugegraph-server and hugegraph-commons configure it), but style/checkstyle.xml:54 does flag RedundantImport. The new block also splits the org.junit imports out of their group.

Requested change: drop the newly added lines 38-41 rather than the pre-existing 47-50. That removes the duplication and restores the original import grouping in one edit.

import com.alipay.sofa.jraft.storage.snapshot.SnapshotReader;
import com.alipay.sofa.jraft.storage.snapshot.SnapshotWriter;
import com.google.protobuf.Message;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;

import com.alipay.sofa.jraft.entity.RaftOutter;
import com.alipay.sofa.jraft.storage.snapshot.SnapshotReader;
Expand All @@ -42,6 +54,9 @@ public class HgSnapshotHandlerTest extends StoreEngineTestBase {

private static HgSnapshotHandler hgSnapshotHandlerUnderTest;

@Rule
public TemporaryFolder tmpDir = new TemporaryFolder();

@Before
public void setUp() throws IOException {
hgSnapshotHandlerUnderTest = new HgSnapshotHandler(createPartitionEngine(0));
Expand Down Expand Up @@ -184,4 +199,58 @@ public void testFindFileList() {

// Verify the results
}

/**
* Test that onSnapshotLoad validates corruption when should_not_load is present but data/ missing.
*/
@Test
public void testOnSnapshotLoadFallsThroughWhenShouldNotLoadPresentButDataMissing()
throws Exception {
// Arrange: snapshot dir has should_not_load but NO data/ subdirectory.
File snapDir = tmpDir.newFolder("snapshot-corrupt");
File shouldNotLoad = new File(snapDir, "should_not_load");
Files.write(shouldNotLoad.toPath(), "saved snapshot".getBytes(StandardCharsets.UTF_8));
// data/ deliberately not created

SnapshotHandler handler = new SnapshotHandler(createPartitionEngine(1));
SnapshotReader stubReader = stubReader(snapDir.getAbsolutePath());

// The fix causes execution to fall through shouldNotLoad() and call
// businessHandler.loadSnapshot(missingDataDir) which throws HgStoreException.
assertThrows(
"onSnapshotLoad must throw when should_not_load present but data/ missing",
HgStoreException.class,
() -> handler.onSnapshotLoad(stubReader, 0L));
}

/**
* Test that onSnapshotLoad skips loading when snapshot is locally saved (both flags present).
*/
@Test
public void testOnSnapshotLoadSkipsWhenShouldNotLoadPresentAndDataExists() throws Exception {
// Arrange: a healthy local snapshot — both should_not_load and data/ present.
File snapDir = tmpDir.newFolder("snapshot-healthy");
File shouldNotLoad = new File(snapDir, "should_not_load");
Files.write(shouldNotLoad.toPath(), "saved snapshot".getBytes(StandardCharsets.UTF_8));
FileUtils.forceMkdir(new File(snapDir, "data"));

SnapshotHandler handler = new SnapshotHandler(createPartitionEngine(2));
SnapshotReader stubReader = stubReader(snapDir.getAbsolutePath());

// Must not throw — should return early at the should_not_load + data-exists check.
handler.onSnapshotLoad(stubReader, 0L);
}

private static SnapshotReader stubReader(String path) {
return new SnapshotReader() {
@Override public RaftOutter.SnapshotMeta load() { return null; }
@Override public String generateURIForCopy() { return null; }
@Override public boolean init(Void opts) { return false; }
@Override public void shutdown() {}
@Override public String getPath() { return path; }
@Override public Set<String> listFiles() { return null; }
@Override public Message getFileMeta(String fileName) { return null; }
@Override public void close() {}
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.apache.hugegraph.store.core.snapshot;

import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.contains;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import java.io.File;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;

import org.apache.hugegraph.store.HgStoreEngine;
import org.apache.hugegraph.store.PartitionEngine;
import org.apache.hugegraph.store.business.BusinessHandler;
import org.apache.hugegraph.store.snapshot.SnapshotHandler;
import org.apache.hugegraph.store.util.HgStoreException;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;

import com.alipay.sofa.jraft.entity.RaftOutter;
import com.alipay.sofa.jraft.storage.snapshot.SnapshotWriter;
import com.google.protobuf.Message;

public class SnapshotHandlerTest {

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.

⚠️ None of the new tests runs in any build, so the fix ships without an executed guard.

hg-store-test runs its src/main/java tests through six surefire executions with fixed include lists (hugegraph-store/hg-store-test/pom.xml:225-302): ClientSuiteTest, CoreSuiteTest plus BatchGraphIsolationTest, CommonSuiteTest, RocksDbSuiteTest, ServerSuiteTest, RaftSuiteTest. SnapshotHandlerTest matches none. The two new methods in HgSnapshotHandlerTest would only run through CoreSuiteTest, whose @RunWith/@Suite.SuiteClasses block is commented out with HgSnapshotHandlerTest.class inside it (CoreSuiteTest.java:22-44), and the workflow never runs that profile (.github/workflows/pd-store-ci.yml:281-296). Codecov agrees: 0% patch coverage, 8 lines missing.

Requested change, both parts. This class is pure Mockito, so add it to RaftSuiteTest's @Suite.SuiteClasses, which store-raftcore-test executes. The HgSnapshotHandlerTest methods need a live engine via StoreEngineTestBase, so they additionally need CoreSuiteTest re-enabled and a -P store-core-test step in the workflow.


@Rule
public TemporaryFolder tmpDir = new TemporaryFolder();

/**
* When state is doing (compaction in progress), onSnapshotSave must throw
* immediately. The exception signals jRaft, which will retry the snapshot later.
* jRaft's snapshot scheduler runs independently and frequently (default 300s, user config 1800s),
* so the next snapshot attempt will succeed after compaction completes.
*/
@Test
public void testOnSnapshotSaveThrowsWhenCompactionInProgress() {
PartitionEngine mockEngine = mock(PartitionEngine.class);
HgStoreEngine mockStoreEngine = mock(HgStoreEngine.class);
BusinessHandler mockBusinessHandler = mock(BusinessHandler.class);

AtomicInteger doingState = new AtomicInteger(BusinessHandler.doing);

when(mockEngine.getGroupId()).thenReturn(0);
when(mockEngine.getStoreEngine()).thenReturn(mockStoreEngine);
when(mockStoreEngine.getBusinessHandler()).thenReturn(mockBusinessHandler);
when(mockBusinessHandler.getState(0)).thenReturn(doingState);

SnapshotHandler handler = new SnapshotHandler(mockEngine);
SnapshotWriter stubWriter = stubWriter("/tmp/snapshot");

HgStoreException ex = assertThrows(
"onSnapshotSave must throw when state == doing",
HgStoreException.class,
() -> handler.onSnapshotSave(stubWriter));

assertTrue("Exception message must mention the partition",
ex.getMessage().contains("0"));
assertTrue("Exception message must mention compaction is in progress",
ex.getMessage().contains("compaction in progress"));
}

/**
* When state is NOT doing (e.g. compactionDone or null), onSnapshotSave must not throw.
* It should proceed and call saveSnapshot with concrete path verification.
*/
@Test
public void testOnSnapshotSaveCallsSaveSnapshotWhenNotBusy() throws Exception {
PartitionEngine mockEngine = mock(PartitionEngine.class);
HgStoreEngine mockStoreEngine = mock(HgStoreEngine.class);
BusinessHandler mockBusinessHandler = mock(BusinessHandler.class);

final String snapshotPath = tmpDir.newFolder("snap-not-busy").getAbsolutePath();

// state == compactionDone (not doing) — save should proceed immediately
AtomicInteger doneState = new AtomicInteger(BusinessHandler.compactionDone);

when(mockEngine.getGroupId()).thenReturn(0);
when(mockEngine.getStoreEngine()).thenReturn(mockStoreEngine);
when(mockStoreEngine.getBusinessHandler()).thenReturn(mockBusinessHandler);
when(mockBusinessHandler.getState(0)).thenReturn(doneState);

SnapshotHandler handler = new SnapshotHandler(mockEngine);
SnapshotWriter stubWriter = stubWriter(snapshotPath);

handler.onSnapshotSave(stubWriter);

// Verify: saveSnapshot was called with concrete path containing expected data dir
String expectedDataDir = snapshotPath + File.separator + "data";
verify(mockBusinessHandler).saveSnapshot(
contains(expectedDataDir), // Must contain the snapshot path + /data
eq(""), // graphName (empty string)
eq(0)); // groupId (partition 0)
}

private static SnapshotWriter stubWriter(String path) {
return new SnapshotWriter() {
@Override public boolean saveMeta(RaftOutter.SnapshotMeta meta) { return false; }
@Override public boolean addFile(String fileName, Message fileMeta) { return false; }
@Override public boolean removeFile(String fileName) { return false; }
@Override public void close(boolean keepDataOnError) {}
@Override public boolean init(Void opts) { return false; }
@Override public void shutdown() {}
@Override public String getPath() { return path; }
@Override public Set<String> listFiles() { return null; }
@Override public Message getFileMeta(String fileName) { return null; }
@Override public void close() {}
};
}
}
Loading