Skip to content

Commit a367821

Browse files
aksOpsclaude
andcommitted
feat: add --read-only mode for serving on read-only filesystems (AKS/K8s)
New --read-only flag on serve command for deployments with read-only volumes (e.g., AKS, K8s with read-only root filesystem): code-iq serve /data --read-only What it does: - Neo4j: opens in read-only mode (GraphDatabaseSettings.read_only_database_default) — no lock files, no transaction logs, no store_lock created - H2: opens with ACCESS_MODE_DATA=r and FILE_LOCK=NO — no lock files, no writes - GraphBootstrapper: skipped entirely (no H2→Neo4j bootstrap writes) Configuration: - codeiq.read-only=true in application.yml or --read-only CLI flag - Only affects serving profile — indexing/enriching always need write access For AKS deployment: 1. index + enrich on writable storage (CI or local) 2. bundle the .code-iq/ directory into container image 3. serve --read-only on AKS with read-only filesystem Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent b417ee3 commit a367821

5 files changed

Lines changed: 63 additions & 8 deletions

File tree

src/main/java/io/github/randomcodespace/iq/cache/AnalysisCache.java

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -112,17 +112,37 @@ CREATE TABLE IF NOT EXISTS analysis_runs (
112112
* @param dbPath path to the H2 database file (without extension)
113113
*/
114114
public AnalysisCache(Path dbPath) {
115+
this(dbPath, false);
116+
}
117+
118+
/**
119+
* Open an analysis cache. In read-only mode, no lock files are created
120+
* and no writes are allowed — suitable for read-only filesystems (AKS/K8s).
121+
*
122+
* @param dbPath path to the H2 database file (without extension)
123+
* @param readOnly if true, opens DB in read-only mode (no lock files, no writes)
124+
*/
125+
public AnalysisCache(Path dbPath, boolean readOnly) {
115126
this.dbPath = dbPath;
116127
try {
117-
Files.createDirectories(dbPath.getParent());
128+
if (!readOnly) {
129+
Files.createDirectories(dbPath.getParent());
130+
}
118131
// Strip .db extension if present — H2 appends its own .mv.db
119132
String dbFile = dbPath.toString();
120133
if (dbFile.endsWith(".db")) {
121134
dbFile = dbFile.substring(0, dbFile.length() - 3);
122135
}
123-
this.conn = DriverManager.getConnection(
124-
"jdbc:h2:file:" + dbFile + ";AUTO_SERVER=FALSE;MODE=MySQL;DB_CLOSE_ON_EXIT=FALSE;WRITE_DELAY=0");
125-
initDb();
136+
String url = "jdbc:h2:file:" + dbFile + ";AUTO_SERVER=FALSE;MODE=MySQL;DB_CLOSE_ON_EXIT=FALSE";
137+
if (readOnly) {
138+
url += ";ACCESS_MODE_DATA=r;FILE_LOCK=NO";
139+
} else {
140+
url += ";WRITE_DELAY=0";
141+
}
142+
this.conn = DriverManager.getConnection(url);
143+
if (!readOnly) {
144+
initDb();
145+
}
126146
} catch (Exception e) {
127147
throw new RuntimeException("Failed to open analysis cache at " + dbPath, e);
128148
}

src/main/java/io/github/randomcodespace/iq/cli/ServeCommand.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@ public class ServeCommand implements Callable<Integer> {
4646
description = "Disable the web UI (React SPA). API and MCP endpoints remain active.")
4747
private boolean noUi;
4848

49+
@Option(names = {"--read-only"}, defaultValue = "false",
50+
description = "Read-only mode: no lock files, no writes. For read-only filesystems (AKS/K8s).")
51+
private boolean readOnly;
52+
4953
@Autowired
5054
private CodeIqConfig config;
5155

@@ -56,6 +60,9 @@ public class ServeCommand implements Callable<Integer> {
5660
public Integer call() {
5761
Path root = path.toAbsolutePath().normalize();
5862
config.setRootPath(root.toString());
63+
if (readOnly) {
64+
config.setReadOnly(true);
65+
}
5966
NumberFormat nf = NumberFormat.getIntegerInstance(Locale.US);
6067

6168
// Report Neo4j graph status

src/main/java/io/github/randomcodespace/iq/config/CodeIqConfig.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,9 @@ public static class Graph {
5151
public void setPath(String path) { this.path = path; }
5252
}
5353

54+
/** Read-only mode for serving — no lock files, no writes. For read-only filesystems (AKS). */
55+
private boolean readOnly = false;
56+
5457
/** Service name tag for multi-repo graph mode. */
5558
private String serviceName;
5659

@@ -104,6 +107,14 @@ public void setBatchSize(int batchSize) {
104107
this.batchSize = Math.max(1, batchSize);
105108
}
106109

110+
public boolean isReadOnly() {
111+
return readOnly;
112+
}
113+
114+
public void setReadOnly(boolean readOnly) {
115+
this.readOnly = readOnly;
116+
}
117+
107118
public String getServiceName() {
108119
return serviceName;
109120
}

src/main/java/io/github/randomcodespace/iq/config/GraphBootstrapper.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,12 @@ public GraphBootstrapper(GraphStore graphStore, CodeIqConfig config) {
4545

4646
@EventListener(ApplicationReadyEvent.class)
4747
public void bootstrapNeo4jFromCache() {
48+
// Skip bootstrap in read-only mode (no writes allowed)
49+
if (config.isReadOnly()) {
50+
log.info("Read-only mode -- skipping H2→Neo4j bootstrap");
51+
return;
52+
}
53+
4854
// Check if Neo4j already has data
4955
long existingCount = graphStore.count();
5056
if (existingCount > 0) {

src/main/java/io/github/randomcodespace/iq/config/Neo4jConfig.java

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package io.github.randomcodespace.iq.config;
22

3+
import org.neo4j.configuration.GraphDatabaseSettings;
34
import org.neo4j.configuration.connectors.BoltConnector;
45
import org.neo4j.configuration.helpers.SocketAddress;
56
import org.neo4j.dbms.api.DatabaseManagementService;
@@ -12,9 +13,11 @@
1213
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
1314
import org.springframework.context.annotation.Bean;
1415
import org.springframework.context.annotation.Configuration;
16+
import org.springframework.core.env.Environment;
1517
import org.springframework.data.neo4j.repository.config.EnableNeo4jRepositories;
1618

1719
import java.nio.file.Path;
20+
import java.util.Arrays;
1821

1922
/**
2023
* Neo4j Embedded configuration.
@@ -34,11 +37,19 @@ public class Neo4jConfig {
3437
private int boltPort;
3538

3639
@Bean(destroyMethod = "shutdown")
37-
DatabaseManagementService databaseManagementService(CodeIqConfig config) {
38-
return new DatabaseManagementServiceBuilder(Path.of(config.getGraph().getPath()))
40+
DatabaseManagementService databaseManagementService(CodeIqConfig config, Environment env) {
41+
var builder = new DatabaseManagementServiceBuilder(Path.of(config.getGraph().getPath()))
3942
.setConfig(BoltConnector.enabled, true)
40-
.setConfig(BoltConnector.listen_address, new SocketAddress("localhost", boltPort))
41-
.build();
43+
.setConfig(BoltConnector.listen_address, new SocketAddress("localhost", boltPort));
44+
45+
// Read-only mode for serving profile — no lock files, no transaction logs.
46+
// Required for read-only filesystems (e.g., AKS with read-only volumes).
47+
boolean isServing = Arrays.asList(env.getActiveProfiles()).contains("serving");
48+
if (isServing && config.isReadOnly()) {
49+
builder.setConfig(GraphDatabaseSettings.read_only_database_default, true);
50+
}
51+
52+
return builder.build();
4253
}
4354

4455
@Bean

0 commit comments

Comments
 (0)