feat(#4446): add arcadedb.ha.raftStorageDirectory for configurable Raft storage path#4447
Conversation
…es readOnlyRootFilesystem support
Up to standards ✅🟢 Issues
|
| Metric | Results |
|---|---|
| Complexity | 0 |
🟢 Coverage 100.00% diff coverage
Metric Results Coverage variation Report missing for 846b39d1 Diff coverage ✅ 100.00% diff coverage Coverage variation details
Coverable lines Covered lines Coverage Common ancestor commit (846b39d) Report Missing Report Missing Report Missing Head commit (eeb00d5) 156026 104035 66.68% Coverage variation is the difference between the coverage for the head and common ancestor commits of the pull request branch:
<coverage of head commit> - <coverage of common ancestor commit>Diff coverage details
Coverable lines Covered lines Diff coverage Pull request (#4447) 1 1 100.00% Diff coverage is the percentage of lines that are covered by tests out of the coverable lines that the pull request added or modified:
<covered lines added or modified>/<coverable lines added or modified> * 100%1 Codacy didn't receive coverage data for the commit, or there was an error processing the received data. Check your integration for errors and validate that your coverage setup is correct.
NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.
There was a problem hiding this comment.
Code Review
This pull request introduces a configurable Raft storage directory via the new arcadedb.ha.raftStorageDirectory configuration option, enabling Kubernetes deployments with read-only root filesystems to mount a fixed path for Raft persistence. The changes include updating RaftHAServer to resolve the storage directory dynamically, updating test cleanup logic, and adding a new integration test. The review feedback suggests replacing manual string concatenation using File.separator with the safer and more idiomatic File(String parent, String child) constructor across the implementation and test files to prevent potential issues with null paths resolving to a literal "null" directory name.
| private File getRaftStorageDir() { | ||
| String raftDir = configuration.getValueAsString(GlobalConfiguration.HA_RAFT_STORAGE_DIRECTORY); | ||
| if (raftDir == null || raftDir.isEmpty()) | ||
| raftDir = arcadeServer.getRootPath(); | ||
| return new File(raftDir + File.separator + "raft-storage-" + localPeerId); | ||
| } |
There was a problem hiding this comment.
Using string concatenation with File.separator when raftDir can be null (e.g., if arcadeServer.getRootPath() returns null) will result in a path starting with the literal string "null" (e.g., "null/raft-storage-..."). This can lead to the accidental creation of a directory named null in the current working directory.
Using the File(String parent, String child) constructor is safer and more idiomatic, as it gracefully handles a null parent by treating the child as a relative path.
| private File getRaftStorageDir() { | |
| String raftDir = configuration.getValueAsString(GlobalConfiguration.HA_RAFT_STORAGE_DIRECTORY); | |
| if (raftDir == null || raftDir.isEmpty()) | |
| raftDir = arcadeServer.getRootPath(); | |
| return new File(raftDir + File.separator + "raft-storage-" + localPeerId); | |
| } | |
| private File getRaftStorageDir() { | |
| String raftDir = configuration.getValueAsString(GlobalConfiguration.HA_RAFT_STORAGE_DIRECTORY); | |
| if (raftDir == null || raftDir.isEmpty()) | |
| raftDir = arcadeServer.getRootPath(); | |
| return new File(raftDir, "raft-storage-" + localPeerId); | |
| } |
| String raftDir = GlobalConfiguration.HA_RAFT_STORAGE_DIRECTORY.getValueAsString(); | ||
| if (raftDir == null || raftDir.isEmpty()) | ||
| raftDir = GlobalConfiguration.SERVER_ROOT_PATH.getValueAsString(); | ||
| if (raftDir == null) | ||
| return; | ||
| for (int i = 0; i < getServerCount(); i++) | ||
| FileUtils.deleteRecursively(new File(rootPath + File.separator + "raft-storage-" + peerIdForIndex(i))); | ||
| FileUtils.deleteRecursively(new File(raftDir + File.separator + "raft-storage-" + peerIdForIndex(i))); | ||
| } |
There was a problem hiding this comment.
For better readability and consistency, use the File(String parent, String child) constructor instead of manual string concatenation with File.separator.
| String raftDir = GlobalConfiguration.HA_RAFT_STORAGE_DIRECTORY.getValueAsString(); | |
| if (raftDir == null || raftDir.isEmpty()) | |
| raftDir = GlobalConfiguration.SERVER_ROOT_PATH.getValueAsString(); | |
| if (raftDir == null) | |
| return; | |
| for (int i = 0; i < getServerCount(); i++) | |
| FileUtils.deleteRecursively(new File(rootPath + File.separator + "raft-storage-" + peerIdForIndex(i))); | |
| FileUtils.deleteRecursively(new File(raftDir + File.separator + "raft-storage-" + peerIdForIndex(i))); | |
| } | |
| String raftDir = GlobalConfiguration.HA_RAFT_STORAGE_DIRECTORY.getValueAsString(); | |
| if (raftDir == null || raftDir.isEmpty()) | |
| raftDir = GlobalConfiguration.SERVER_ROOT_PATH.getValueAsString(); | |
| if (raftDir == null) | |
| return; | |
| for (int i = 0; i < getServerCount(); i++) | |
| FileUtils.deleteRecursively(new File(raftDir, "raft-storage-" + peerIdForIndex(i))); | |
| } |
| for (int i = 0; i < getServerCount(); i++) { | ||
| final File expectedDir = new File(CUSTOM_RAFT_DIR + File.separator + "raft-storage-" + peerIdForIndex(i)); | ||
| assertThat(expectedDir) | ||
| .as("raft-storage for server %d must exist under configured raftStorageDirectory", i) | ||
| .exists(); | ||
| } |
There was a problem hiding this comment.
Use the File(String parent, String child) constructor instead of manual string concatenation with File.separator for cleaner and more idiomatic path construction.
| for (int i = 0; i < getServerCount(); i++) { | |
| final File expectedDir = new File(CUSTOM_RAFT_DIR + File.separator + "raft-storage-" + peerIdForIndex(i)); | |
| assertThat(expectedDir) | |
| .as("raft-storage for server %d must exist under configured raftStorageDirectory", i) | |
| .exists(); | |
| } | |
| for (int i = 0; i < getServerCount(); i++) { | |
| final File expectedDir = new File(CUSTOM_RAFT_DIR, "raft-storage-" + peerIdForIndex(i)); | |
| assertThat(expectedDir) | |
| .as("raft-storage for server %d must exist under configured raftStorageDirectory", i) | |
| .exists(); | |
| } |
Code ReviewOverall: This is a clean, well-scoped feature PR. The implementation is minimal, the refactoring is correct, and the test coverage is adequate. A few things worth addressing. What the PR DoesAdds Issues1. GlobalConfiguration vs ContextConfiguration split in test cleanup (potential maintenance trap)
// BaseRaftHATest.java - reads GLOBAL config
String raftDir = GlobalConfiguration.HA_RAFT_STORAGE_DIRECTORY.getValueAsString();// RaftHAServer.java - reads per-server ContextConfiguration
String raftDir = configuration.getValueAsString(GlobalConfiguration.HA_RAFT_STORAGE_DIRECTORY);This works now only because 2. Unnecessary null check in String raftDir = configuration.getValueAsString(GlobalConfiguration.HA_RAFT_STORAGE_DIRECTORY);
if (raftDir == null || raftDir.isEmpty()) // null branch is dead - default is ""The config entry has a 3. Missing Several peer tests in the same package carry Minor Suggestions
What's Good
|
…eanup warning comment
Code Review - PR #4446: Configurable Raft Storage DirectoryOverall this is a well-scoped, clean feature. The use case (Kubernetes Bug: Duplicate Javadoc block in
|
| Severity | Item |
|---|---|
| Must fix | Duplicate Javadoc block in BaseRaftHATest.java |
| Must fix | Remove docs/review-deferred-75cb6b3.md |
| Should fix | Add negative assertion in RaftStorageDirectoryIT |
| Minor | Add @Tag("IntegrationTest") to RaftStorageDirectoryIT |
| Note | Document deleteDatabaseFolders() footgun is noted and acceptable for now |
…ove deferred review doc
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request introduces a new configuration option arcadedb.ha.raftStorageDirectory (HA_RAFT_STORAGE_DIRECTORY) to make the parent directory of Raft storage folders configurable, which is particularly useful for Kubernetes deployments with a read-only root filesystem. The changes update RaftHAServer to use this configured path, adjust test cleanup in BaseRaftHATest, and add a new integration test RaftStorageDirectoryIT along with documentation. There are no review comments on this pull request, so we have no feedback to provide.
|
test comment - please ignore |
|
Code Review Overview This PR adds a configurable arcadedb.ha.raftStorageDirectory setting so Raft storage sub-folders can be placed outside the server root, primarily enabling Kubernetes deployments with readOnlyRootFilesystem: true. The implementation is clean and minimal, the default is backward-compatible, and a regression test is included. Strengths
Issues and Suggestions Issue 1 - Missing @tag(slow) on RaftStorageDirectoryIT The new test starts a 2-node Raft cluster, which takes multiple seconds. Similar tests in the same package carry this annotation: RaftLeaderCrashWithExternalPropertyIT, RaftPriorityRejoinIT, RaftLargeSchemaReplicationIT all use @tag(slow). RaftStorageDirectoryIT should follow the same convention. Note: RaftBootstrapDoesNotEngageOnRestartIT also appears to be missing this tag and is similarly slow - worth tagging while in the area. Issue 2 - Fragile base-class cleanup for per-server config overrides BaseRaftHATest.deleteDatabaseFolders() reads GlobalConfiguration.HA_RAFT_STORAGE_DIRECTORY from the global static config, but RaftStorageDirectoryIT sets the value via per-server ContextConfiguration in onServerConfiguration(). The two do not share state, so the base cleanup silently becomes a no-op for the custom directory - which is why the override in RaftStorageDirectoryIT is necessary. The Javadoc comment calls this out, which is the right mitigation. A lighter alternative would be to store the resolved raft dir in a protected field on BaseRaftHATest that subclasses can set, so the base cleanup can always do the right thing without requiring a full override. The current approach is correct for this PR - this is a suggestion, not a blocker. Issue 3 - No warning for relative paths in production When raftDir is a relative path (e.g. ./target/custom-raft-storage as used in the test), it resolves relative to the JVM working directory rather than the server root. This is fine for tests but could surprise operators in production. A log warning when the configured path is not absolute would help with diagnosability. Not blocking. Minor nits
Summary The implementation is solid and solves a real operational pain point cleanly. The main actionable item before merge is adding @tag(slow) to RaftStorageDirectoryIT to keep CI build times consistent with the rest of the HA test suite. The other points are suggestions. |
Code ReviewOverviewThis PR adds Positives
IssuesMinor -
|
Code ReviewThis is a clean, well-scoped feature. The use case (Kubernetes Must Fix
This file reads like an AI-assisted review scaffold rather than user-facing or developer-facing documentation. It includes internal review cycle metadata such as: This is not suitable for the repository. It exposes internal tooling artifacts. If the intent is to document the feature for operators, the right home is the main docs site or the existing Should FixGlobalConfiguration vs ContextConfiguration split in
// RaftHAServer.java - reads per-server config
String raftDir = configuration.getValueAsString(GlobalConfiguration.HA_RAFT_STORAGE_DIRECTORY);But // BaseRaftHATest.java - reads GLOBAL config, not per-server
String raftDir = GlobalConfiguration.HA_RAFT_STORAGE_DIRECTORY.getValueAsString();This works now only because A safer approach: have Minor
if (raftDir == null || raftDir.isBlank())This already uses Test uses a relative path for private static final String CUSTOM_RAFT_DIR = "./target/custom-raft-storage";This works when Maven runs tests from the module root, but is fragile if someone runs the test from the IDE from a different working directory. Using Consider a startup log/warning for relative paths The config description recommends an absolute path ( What's Good
|
Code ReviewOverviewThis PR cleanly solves a real operational pain point: Kubernetes StatefulSets with What works well
Issues1. Minor - Per the project coding standards, private File getRaftStorageDir() {
final String configured = configuration.getValueAsString(GlobalConfiguration.HA_RAFT_STORAGE_DIRECTORY);
final String parent = (configured == null || configured.isBlank()) ? arcadeServer.getRootPath() : configured;
return new File(parent, "raft-storage-" + localPeerId);
}2. Medium - Global vs per-server config mismatch in test cleanup
In practice: when The Javadoc warning mitigates this, but the design is fragile: future subclasses that forget to override cleanup will silently leave stale directories on disk. This would be cleaner if the base cleanup tracked the configured paths per server rather than re-reading the global config at teardown, though I understand this may be difficult given the test framework architecture. 3. Suggestion - Warn on relative paths The config doc says "Set to an absolute path (e.g. /var/lib/arcadedb/raft)", but the code accepts relative paths silently (the test even uses SummaryThe core implementation is correct and well-tested. The |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #4447 +/- ##
==========================================
+ Coverage 65.23% 65.25% +0.02%
==========================================
Files 1612 1612
Lines 124319 124320 +1
Branches 26908 26908
==========================================
+ Hits 81100 81126 +26
+ Misses 31814 31796 -18
+ Partials 11405 11398 -7 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
Closes #4446
Adds a new
arcadedb.ha.raftStorageDirectoryconfiguration setting that controls the parent directory where Raft storage sub-folders (raft-storage-<nodeName>) are created. When empty (the default), the server root path is used, preserving existing behaviour with zero migration cost.The primary use case is Kubernetes StatefulSets with
readOnlyRootFilesystem: true: operators can setarcadedb.ha.raftStorageDirectory=/var/lib/arcadedb/raft, mount that path on a PVC, and avoid the previous requirement to make the entire server root writable just to accommodate the dynamically-namedraft-storage-*directory.Changes
GlobalConfiguration.java- newHA_RAFT_STORAGE_DIRECTORYentry (arcadedb.ha.raftStorageDirectory,SCOPE.SERVER, default"")RaftHAServer.java- privategetRaftStorageDir()helper encapsulates the path logic; replaces 3 hardcodedarcadeServer.getRootPath() + "/raft-storage-" + localPeerIdconstructionsBaseRaftHATest.java-deleteDatabaseFolders()updated to use the configured directory with a null guard for the globalSERVER_ROOT_PATHcaseRaftStorageDirectoryIT.java- new regression test that starts a 2-node cluster with a customHA_RAFT_STORAGE_DIRECTORYand assertsraft-storage-<peerId>directories are created under the configured pathTest plan
RaftStorageDirectoryIT- new test verifying raft-storage dirs land in the configured parent directory (1 test, 0 failures)RaftBootstrapDoesNotEngageOnRestartIT- no regression (1 test, 0 failures)RaftBootstrapLateNewerJoinerIT- no regression (1 test, 0 failures)RaftReplicaCrashAndRecoverIT- no regression (1 test, 0 failures)mvn clean install -DskipTests -pl engine,ha-raft -am- clean build