Skip to content

Commit edeefcd

Browse files
Merge pull request #60 from ApiliumCode/feat/persistent-cortex-storage
Persistent Cortex storage with Sled backend and Ineru snapshots
2 parents 5d30d1f + 89e8a43 commit edeefcd

25 files changed

Lines changed: 348 additions & 38 deletions

File tree

Cargo.lock

Lines changed: 11 additions & 10 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/ai_hash/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "ai_hash"
3-
version = "0.4.0"
3+
version = "0.4.1"
44
authors = ["Apilium Technologies <hello@apilium.com>"]
55
keywords = [ "aingle", "ai", "hash", "blake", "blake2b" ]
66
categories = [ "cryptography" ]

crates/aingle/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "aingle"
3-
version = "0.4.0"
3+
version = "0.4.1"
44
description = "AIngle, a framework for distributed applications"
55
license = "Apache-2.0 OR LicenseRef-Commercial"
66
homepage = "https://apilium.com"

crates/aingle_ai/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "aingle_ai"
3-
version = "0.4.0"
3+
version = "0.4.1"
44
description = "AI integration layer for AIngle - Ineru, Nested Learning, Kaneru"
55
license = "Apache-2.0 OR LicenseRef-Commercial"
66
repository = "https://github.com/ApiliumCode/aingle"

crates/aingle_contracts/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "aingle_contracts"
3-
version = "0.4.0"
3+
version = "0.4.1"
44
description = "Smart Contracts DSL and WASM Runtime for AIngle"
55
license = "Apache-2.0 OR LicenseRef-Commercial"
66
repository = "https://github.com/ApiliumCode/aingle"

crates/aingle_cortex/Cargo.toml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "aingle_cortex"
3-
version = "0.4.0"
3+
version = "0.4.1"
44
description = "Córtex API - REST/GraphQL/SPARQL interface for AIngle semantic graphs"
55
license = "Apache-2.0 OR LicenseRef-Commercial"
66
repository = "https://github.com/ApiliumCode/aingle"
@@ -18,7 +18,7 @@ rest = []
1818
graphql = ["dep:async-graphql", "dep:async-graphql-axum"]
1919
sparql = ["dep:spargebra"]
2020
auth = ["dep:jsonwebtoken", "dep:argon2"]
21-
p2p = ["dep:quinn", "dep:rustls", "dep:rcgen", "dep:ed25519-dalek", "dep:hex", "dep:dirs"]
21+
p2p = ["dep:quinn", "dep:rustls", "dep:rcgen", "dep:ed25519-dalek", "dep:hex"]
2222
p2p-mdns = ["p2p", "dep:mdns-sd", "dep:if-addrs"]
2323
full = ["rest", "graphql", "sparql", "auth"]
2424

@@ -28,7 +28,7 @@ path = "src/main.rs"
2828

2929
[dependencies]
3030
# Core AIngle crates
31-
aingle_graph = { version = "0.4", path = "../aingle_graph" }
31+
aingle_graph = { version = "0.4", path = "../aingle_graph", features = ["sled-backend"] }
3232
aingle_logic = { version = "0.4", path = "../aingle_logic" }
3333
aingle_zk = { version = "0.4", path = "../aingle_zk" }
3434
ineru = { version = "0.4", path = "../ineru" }
@@ -92,7 +92,7 @@ rustls = { version = "0.23", default-features = false, features = ["ring", "std"
9292
rcgen = { version = "0.13", optional = true }
9393
ed25519-dalek = { version = "2", features = ["rand_core"], optional = true }
9494
hex = { version = "0.4", optional = true }
95-
dirs = { version = "6", optional = true }
95+
dirs = "6"
9696
mdns-sd = { version = "0.18", optional = true }
9797
if-addrs = { version = "0.13", optional = true }
9898

crates/aingle_cortex/src/main.rs

Lines changed: 57 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,15 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
5252
"--public" => {
5353
config.host = "0.0.0.0".to_string();
5454
}
55+
"--db" => {
56+
if i + 1 < args.len() {
57+
config.db_path = Some(args[i + 1].clone());
58+
i += 1;
59+
}
60+
}
61+
"--memory" => {
62+
config.db_path = Some(":memory:".to_string());
63+
}
5564
"--help" => {
5665
print_help();
5766
return Ok(());
@@ -72,10 +81,24 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
7281
p2p
7382
};
7483

84+
// Resolve the snapshot directory for Ineru persistence
85+
let snapshot_dir = match &config.db_path {
86+
Some(p) if p == ":memory:" => None,
87+
Some(p) => std::path::Path::new(p).parent().map(|p| p.to_path_buf()),
88+
None => {
89+
let home = dirs::home_dir().unwrap_or_else(|| std::path::PathBuf::from("."));
90+
Some(home.join(".aingle").join("cortex"))
91+
}
92+
};
93+
7594
// Create and run server
7695
#[allow(unused_mut)]
7796
let mut server = CortexServer::new(config)?;
7897

98+
// Keep a reference to the state for shutdown flush
99+
let state_for_shutdown = server.state().clone();
100+
let snapshot_dir_for_shutdown = snapshot_dir.clone();
101+
79102
// Start P2P manager if enabled.
80103
#[cfg(feature = "p2p")]
81104
if p2p_config.enabled {
@@ -96,12 +119,39 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
96119
}
97120
}
98121

99-
// Set up graceful shutdown
100-
let shutdown_signal = async {
101-
tokio::signal::ctrl_c()
122+
// Set up graceful shutdown with data flush (handles both SIGINT and SIGTERM)
123+
let shutdown_signal = async move {
124+
let ctrl_c = tokio::signal::ctrl_c();
125+
126+
#[cfg(unix)]
127+
let terminate = async {
128+
tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
129+
.expect("Failed to install SIGTERM handler")
130+
.recv()
131+
.await;
132+
};
133+
134+
#[cfg(not(unix))]
135+
let terminate = std::future::pending::<()>();
136+
137+
tokio::select! {
138+
_ = ctrl_c => {
139+
tracing::info!("SIGINT received — flushing data...");
140+
}
141+
_ = terminate => {
142+
tracing::info!("SIGTERM received — flushing data...");
143+
}
144+
}
145+
146+
// Flush graph database and save Ineru snapshot
147+
if let Err(e) = state_for_shutdown
148+
.flush(snapshot_dir_for_shutdown.as_deref())
102149
.await
103-
.expect("Failed to install CTRL+C handler");
104-
tracing::info!("Shutdown signal received");
150+
{
151+
tracing::error!("Failed to flush data on shutdown: {}", e);
152+
} else {
153+
tracing::info!("Data flushed successfully");
154+
}
105155
};
106156

107157
server.run_with_shutdown(shutdown_signal).await?;
@@ -119,6 +169,8 @@ fn print_help() {
119169
println!(" -h, --host <HOST> Host to bind to (default: 127.0.0.1)");
120170
println!(" -p, --port <PORT> Port to listen on (default: 8080)");
121171
println!(" --public Bind to all interfaces (0.0.0.0)");
172+
println!(" --db <PATH> Path to graph database (default: ~/.aingle/cortex/graph.sled)");
173+
println!(" --memory Use volatile in-memory storage (no persistence)");
122174
println!(" -V, --version Print version and exit");
123175
println!(" --help Print this help message");
124176
println!();

crates/aingle_cortex/src/rest/mod.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,9 +78,10 @@ pub fn router() -> Router<AppState> {
7878
.route("/api/v1/query", post(query::query_pattern))
7979
.route("/api/v1/query/subjects", get(query::list_subjects))
8080
.route("/api/v1/query/predicates", get(query::list_predicates))
81-
// Stats
81+
// Stats & Management
8282
.route("/api/v1/stats", get(stats::get_stats))
8383
.route("/api/v1/health", get(stats::health_check))
84+
.route("/api/v1/flush", post(stats::flush_data))
8485
// Validation/Proofs (legacy)
8586
.route("/api/v1/validate", post(proof::validate_triples))
8687
.route("/api/v1/proof/{hash}", get(proof::get_proof))

crates/aingle_cortex/src/rest/stats.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,26 @@ pub struct ComponentStatus {
9191
pub message: Option<String>,
9292
}
9393

94+
/// Flush response
95+
#[derive(Debug, Serialize)]
96+
pub struct FlushResponse {
97+
/// Whether the flush was successful
98+
pub ok: bool,
99+
}
100+
101+
/// Flush graph database and Ineru memory to disk.
102+
///
103+
/// POST /api/v1/flush
104+
pub async fn flush_data(State(state): State<AppState>) -> Result<Json<FlushResponse>> {
105+
// Flush graph
106+
{
107+
let graph = state.graph.read().await;
108+
graph.flush()?;
109+
}
110+
111+
Ok(Json(FlushResponse { ok: true }))
112+
}
113+
94114
/// Health check endpoint
95115
///
96116
/// GET /api/v1/health

crates/aingle_cortex/src/server.rs

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,12 @@ pub struct CortexConfig {
3737
pub audit_log_path: Option<PathBuf>,
3838
/// Maximum request body size in bytes (default: 1MB).
3939
pub max_body_size: usize,
40+
/// Path to the graph database directory.
41+
///
42+
/// - `Some(":memory:")` — volatile in-memory storage (no persistence).
43+
/// - `Some(path)` — persist to the given directory.
44+
/// - `None` — persist to the default `~/.aingle/cortex/graph.sled`.
45+
pub db_path: Option<String>,
4046
}
4147

4248
impl Default for CortexConfig {
@@ -52,6 +58,7 @@ impl Default for CortexConfig {
5258
rate_limit_rpm: 100,
5359
audit_log_path: None,
5460
max_body_size: 1024 * 1024, // 1MB
61+
db_path: None,
5562
}
5663
}
5764
}
@@ -88,13 +95,16 @@ pub struct CortexServer {
8895
}
8996

9097
impl CortexServer {
91-
/// Creates a new `CortexServer` with a given configuration and a default, in-memory `AppState`.
98+
/// Creates a new `CortexServer` with a given configuration.
99+
///
100+
/// The graph database backend is selected based on `config.db_path`:
101+
/// - `Some(":memory:")` — volatile in-memory storage.
102+
/// - `Some(path)` — Sled-backed persistent storage at the given path.
103+
/// - `None` — Sled-backed persistent storage at `~/.aingle/cortex/graph.sled`.
92104
pub fn new(config: CortexConfig) -> Result<Self> {
93-
let state = if let Some(ref path) = config.audit_log_path {
94-
AppState::with_audit_path(path.clone())
95-
} else {
96-
AppState::new()
97-
};
105+
let db_path = resolve_db_path(&config.db_path);
106+
let state = AppState::with_db_path(&db_path, config.audit_log_path.clone())?;
107+
info!("Graph database: {}", db_path);
98108
Ok(Self { config, state })
99109
}
100110

@@ -248,6 +258,24 @@ impl CortexServer {
248258
}
249259
}
250260

261+
/// Resolves the graph database path from the configuration.
262+
///
263+
/// - `":memory:"` → returns `":memory:"` (volatile in-memory storage).
264+
/// - An explicit path → returns it as-is.
265+
/// - `None` → returns the default `~/.aingle/cortex/graph.sled`.
266+
fn resolve_db_path(db_path: &Option<String>) -> String {
267+
match db_path {
268+
Some(p) if p == ":memory:" => ":memory:".to_string(),
269+
Some(p) => p.clone(),
270+
None => {
271+
let home = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
272+
let default_dir = home.join(".aingle").join("cortex");
273+
std::fs::create_dir_all(&default_dir).ok();
274+
default_dir.join("graph.sled").to_string_lossy().to_string()
275+
}
276+
}
277+
}
278+
251279
#[cfg(test)]
252280
mod tests {
253281
use super::*;

0 commit comments

Comments
 (0)