Skip to content
Merged
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
21 changes: 11 additions & 10 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion crates/ai_hash/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "ai_hash"
version = "0.4.0"
version = "0.4.1"
authors = ["Apilium Technologies <hello@apilium.com>"]
keywords = [ "aingle", "ai", "hash", "blake", "blake2b" ]
categories = [ "cryptography" ]
Expand Down
2 changes: 1 addition & 1 deletion crates/aingle/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "aingle"
version = "0.4.0"
version = "0.4.1"
description = "AIngle, a framework for distributed applications"
license = "Apache-2.0 OR LicenseRef-Commercial"
homepage = "https://apilium.com"
Expand Down
2 changes: 1 addition & 1 deletion crates/aingle_ai/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "aingle_ai"
version = "0.4.0"
version = "0.4.1"
description = "AI integration layer for AIngle - Ineru, Nested Learning, Kaneru"
license = "Apache-2.0 OR LicenseRef-Commercial"
repository = "https://github.com/ApiliumCode/aingle"
Expand Down
2 changes: 1 addition & 1 deletion crates/aingle_contracts/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "aingle_contracts"
version = "0.4.0"
version = "0.4.1"
description = "Smart Contracts DSL and WASM Runtime for AIngle"
license = "Apache-2.0 OR LicenseRef-Commercial"
repository = "https://github.com/ApiliumCode/aingle"
Expand Down
8 changes: 4 additions & 4 deletions crates/aingle_cortex/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "aingle_cortex"
version = "0.4.0"
version = "0.4.1"
description = "Córtex API - REST/GraphQL/SPARQL interface for AIngle semantic graphs"
license = "Apache-2.0 OR LicenseRef-Commercial"
repository = "https://github.com/ApiliumCode/aingle"
Expand All @@ -18,7 +18,7 @@ rest = []
graphql = ["dep:async-graphql", "dep:async-graphql-axum"]
sparql = ["dep:spargebra"]
auth = ["dep:jsonwebtoken", "dep:argon2"]
p2p = ["dep:quinn", "dep:rustls", "dep:rcgen", "dep:ed25519-dalek", "dep:hex", "dep:dirs"]
p2p = ["dep:quinn", "dep:rustls", "dep:rcgen", "dep:ed25519-dalek", "dep:hex"]
p2p-mdns = ["p2p", "dep:mdns-sd", "dep:if-addrs"]
full = ["rest", "graphql", "sparql", "auth"]

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

[dependencies]
# Core AIngle crates
aingle_graph = { version = "0.4", path = "../aingle_graph" }
aingle_graph = { version = "0.4", path = "../aingle_graph", features = ["sled-backend"] }
aingle_logic = { version = "0.4", path = "../aingle_logic" }
aingle_zk = { version = "0.4", path = "../aingle_zk" }
ineru = { version = "0.4", path = "../ineru" }
Expand Down Expand Up @@ -92,7 +92,7 @@ rustls = { version = "0.23", default-features = false, features = ["ring", "std"
rcgen = { version = "0.13", optional = true }
ed25519-dalek = { version = "2", features = ["rand_core"], optional = true }
hex = { version = "0.4", optional = true }
dirs = { version = "6", optional = true }
dirs = "6"
mdns-sd = { version = "0.18", optional = true }
if-addrs = { version = "0.13", optional = true }

Expand Down
62 changes: 57 additions & 5 deletions crates/aingle_cortex/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,15 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
"--public" => {
config.host = "0.0.0.0".to_string();
}
"--db" => {
if i + 1 < args.len() {
config.db_path = Some(args[i + 1].clone());
i += 1;
}
}
"--memory" => {
config.db_path = Some(":memory:".to_string());
}
"--help" => {
print_help();
return Ok(());
Expand All @@ -72,10 +81,24 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
p2p
};

// Resolve the snapshot directory for Ineru persistence
let snapshot_dir = match &config.db_path {
Some(p) if p == ":memory:" => None,
Some(p) => std::path::Path::new(p).parent().map(|p| p.to_path_buf()),
None => {
let home = dirs::home_dir().unwrap_or_else(|| std::path::PathBuf::from("."));
Some(home.join(".aingle").join("cortex"))
}
};

// Create and run server
#[allow(unused_mut)]
let mut server = CortexServer::new(config)?;

// Keep a reference to the state for shutdown flush
let state_for_shutdown = server.state().clone();
let snapshot_dir_for_shutdown = snapshot_dir.clone();

// Start P2P manager if enabled.
#[cfg(feature = "p2p")]
if p2p_config.enabled {
Expand All @@ -96,12 +119,39 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
}
}

// Set up graceful shutdown
let shutdown_signal = async {
tokio::signal::ctrl_c()
// Set up graceful shutdown with data flush (handles both SIGINT and SIGTERM)
let shutdown_signal = async move {
let ctrl_c = tokio::signal::ctrl_c();

#[cfg(unix)]
let terminate = async {
tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
.expect("Failed to install SIGTERM handler")
.recv()
.await;
};

#[cfg(not(unix))]
let terminate = std::future::pending::<()>();

tokio::select! {
_ = ctrl_c => {
tracing::info!("SIGINT received — flushing data...");
}
_ = terminate => {
tracing::info!("SIGTERM received — flushing data...");
}
}

// Flush graph database and save Ineru snapshot
if let Err(e) = state_for_shutdown
.flush(snapshot_dir_for_shutdown.as_deref())
.await
.expect("Failed to install CTRL+C handler");
tracing::info!("Shutdown signal received");
{
tracing::error!("Failed to flush data on shutdown: {}", e);
} else {
tracing::info!("Data flushed successfully");
}
};

server.run_with_shutdown(shutdown_signal).await?;
Expand All @@ -119,6 +169,8 @@ fn print_help() {
println!(" -h, --host <HOST> Host to bind to (default: 127.0.0.1)");
println!(" -p, --port <PORT> Port to listen on (default: 8080)");
println!(" --public Bind to all interfaces (0.0.0.0)");
println!(" --db <PATH> Path to graph database (default: ~/.aingle/cortex/graph.sled)");
println!(" --memory Use volatile in-memory storage (no persistence)");
println!(" -V, --version Print version and exit");
println!(" --help Print this help message");
println!();
Expand Down
3 changes: 2 additions & 1 deletion crates/aingle_cortex/src/rest/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,10 @@ pub fn router() -> Router<AppState> {
.route("/api/v1/query", post(query::query_pattern))
.route("/api/v1/query/subjects", get(query::list_subjects))
.route("/api/v1/query/predicates", get(query::list_predicates))
// Stats
// Stats & Management
.route("/api/v1/stats", get(stats::get_stats))
.route("/api/v1/health", get(stats::health_check))
.route("/api/v1/flush", post(stats::flush_data))
// Validation/Proofs (legacy)
.route("/api/v1/validate", post(proof::validate_triples))
.route("/api/v1/proof/{hash}", get(proof::get_proof))
Expand Down
20 changes: 20 additions & 0 deletions crates/aingle_cortex/src/rest/stats.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,26 @@ pub struct ComponentStatus {
pub message: Option<String>,
}

/// Flush response
#[derive(Debug, Serialize)]
pub struct FlushResponse {
/// Whether the flush was successful
pub ok: bool,
}

/// Flush graph database and Ineru memory to disk.
///
/// POST /api/v1/flush
pub async fn flush_data(State(state): State<AppState>) -> Result<Json<FlushResponse>> {
// Flush graph
{
let graph = state.graph.read().await;
graph.flush()?;
}

Ok(Json(FlushResponse { ok: true }))
}

/// Health check endpoint
///
/// GET /api/v1/health
Expand Down
40 changes: 34 additions & 6 deletions crates/aingle_cortex/src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ pub struct CortexConfig {
pub audit_log_path: Option<PathBuf>,
/// Maximum request body size in bytes (default: 1MB).
pub max_body_size: usize,
/// Path to the graph database directory.
///
/// - `Some(":memory:")` — volatile in-memory storage (no persistence).
/// - `Some(path)` — persist to the given directory.
/// - `None` — persist to the default `~/.aingle/cortex/graph.sled`.
pub db_path: Option<String>,
}

impl Default for CortexConfig {
Expand All @@ -52,6 +58,7 @@ impl Default for CortexConfig {
rate_limit_rpm: 100,
audit_log_path: None,
max_body_size: 1024 * 1024, // 1MB
db_path: None,
}
}
}
Expand Down Expand Up @@ -88,13 +95,16 @@ pub struct CortexServer {
}

impl CortexServer {
/// Creates a new `CortexServer` with a given configuration and a default, in-memory `AppState`.
/// Creates a new `CortexServer` with a given configuration.
///
/// The graph database backend is selected based on `config.db_path`:
/// - `Some(":memory:")` — volatile in-memory storage.
/// - `Some(path)` — Sled-backed persistent storage at the given path.
/// - `None` — Sled-backed persistent storage at `~/.aingle/cortex/graph.sled`.
pub fn new(config: CortexConfig) -> Result<Self> {
let state = if let Some(ref path) = config.audit_log_path {
AppState::with_audit_path(path.clone())
} else {
AppState::new()
};
let db_path = resolve_db_path(&config.db_path);
let state = AppState::with_db_path(&db_path, config.audit_log_path.clone())?;
info!("Graph database: {}", db_path);
Ok(Self { config, state })
}

Expand Down Expand Up @@ -248,6 +258,24 @@ impl CortexServer {
}
}

/// Resolves the graph database path from the configuration.
///
/// - `":memory:"` → returns `":memory:"` (volatile in-memory storage).
/// - An explicit path → returns it as-is.
/// - `None` → returns the default `~/.aingle/cortex/graph.sled`.
fn resolve_db_path(db_path: &Option<String>) -> String {
match db_path {
Some(p) if p == ":memory:" => ":memory:".to_string(),
Some(p) => p.clone(),
None => {
let home = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
let default_dir = home.join(".aingle").join("cortex");
std::fs::create_dir_all(&default_dir).ok();
default_dir.join("graph.sled").to_string_lossy().to_string()
}
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
Loading
Loading