Skip to content

Commit 3cb6526

Browse files
feat(rusty-red): add product query and graph cache surfaces
1 parent 1dfd008 commit 3cb6526

8 files changed

Lines changed: 3834 additions & 69 deletions

File tree

README.md

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,9 @@ This builds an `abi3-py312` wheel and installs it into the active Python environ
3232

3333
Rusty Red is the productized THG runtime profile: it keeps the THG command model
3434
for existing harness flows while adding first-class graph node, edge, adjacency,
35-
exact scalar property index, stats, verify, and MCP routes. By default, it runs
36-
in `RUSTY_RED_MODE=embedded` with RedCore RAM-first storage and local AOF/snapshot
37-
persistence.
35+
exact scalar property index, GraphCache, stats, verify, and MCP routes. By
36+
default, it runs in `RUSTY_RED_MODE=embedded` with RedCore RAM-first storage and
37+
local AOF/snapshot persistence.
3838

3939
It is not a raw Redis protocol, RedisGraph compatibility layer, FalkorDB
4040
replacement, or complete OpenCypher/GQL engine yet. `RUSTY_RED_MODE=redis` keeps
@@ -78,9 +78,21 @@ GET /ready
7878
GET /openapi.json
7979
GET /.well-known/mcp/thg.json
8080
POST /mcp
81+
POST /v1/command
82+
POST /v1/batch
83+
POST /v1/query
84+
POST /v1/cypher
85+
POST /v1/cypher/explain
86+
POST /v1/cache/put
87+
POST /v1/cache/get
88+
POST /v1/cache/check
89+
POST /v1/cache/explain
90+
POST /v1/cache/invalidate
91+
POST /v1/cache/stats
8192
POST /v1/tenants/{tenant_id}/command
8293
POST /v1/tenants/{tenant_id}/batch
8394
GET /v1/tenants/{tenant_id}/runs/{run_id}
95+
POST /v1/tenants/{tenant_id}/graph/query
8496
POST /v1/tenants/{tenant_id}/graph/nodes
8597
POST /v1/tenants/{tenant_id}/graph/nodes/query
8698
GET /v1/tenants/{tenant_id}/graph/nodes/{node_id}
@@ -102,6 +114,12 @@ records, exact scalar property indexes, adjacency traversal, and verification
102114
through the existing THG command surface instead of depending on a separate
103115
runtime name. In this slice, run/context state commands remain Redis-mode.
104116

117+
The public query surface is now split cleanly:
118+
119+
- `/v1/query` is the product-facing native subset for `node_match` and `neighbors`.
120+
- `/v1/cypher` and `/v1/cypher/explain` are the first read-only OpenCypher-compatible subset.
121+
- `/v1/tenants/{tenant_id}/graph/query` remains the older debug bridge and should not be treated as the product route.
122+
105123
The OpenAPI document is served at `/openapi.json`. It exists because Rusty Red
106124
is exposed through HTTP and MCP even though the underlying storage engine is a
107125
database-style service. The OpenAPI contract is for the HTTP API; MCP tool,
@@ -112,8 +130,10 @@ Railway template readiness follows the public template guidance: use a GitHub
112130
source repo, keep the service root minimal, set `/ready` as the health check,
113131
wire Redis only for explicit `RUSTY_RED_MODE=redis` deployments through private
114132
networking/reference variables, attach persistent storage to stateful dependencies,
115-
generate any public-ingress tokens with Railway template variable functions, and
116-
replace the badge placeholder above once Railway assigns the final template URL.
133+
set `RUSTY_RED_REQUIRE_VOLUME=true` for embedded Railway deployments so `/ready`
134+
fails when the mounted volume is absent, generate any public-ingress tokens with
135+
Railway template variable functions, and replace the badge placeholder above once
136+
Railway assigns the final template URL.
117137

118138
Railway can deploy this directory directly:
119139

crates/thg-product-server/src/config.rs

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ pub struct Config {
3434
pub port: u16,
3535
pub storage_mode: StorageMode,
3636
pub data_dir: String,
37+
pub require_volume: bool,
38+
pub volume_available: bool,
3739
pub durability: RedCoreDurability,
3840
pub snapshot_interval_writes: u64,
3941
pub strict_acid: bool,
@@ -71,15 +73,31 @@ impl Config {
7173
let storage_mode = env_first(&["RUSTY_RED_MODE", "THG_PRODUCT_STORE"])
7274
.map(|value| StorageMode::parse(&value))
7375
.unwrap_or(StorageMode::Embedded);
76+
let railway_volume_mount_path = env::var("RAILWAY_VOLUME_MOUNT_PATH")
77+
.ok()
78+
.filter(|value| !value.trim().is_empty());
7479
let data_dir = env_first(&["RUSTY_RED_DATA_DIR", "THG_PRODUCT_DATA_DIR"])
75-
.or_else(|_| env::var("RAILWAY_VOLUME_MOUNT_PATH"))
80+
.or_else(|_| {
81+
railway_volume_mount_path
82+
.clone()
83+
.ok_or(env::VarError::NotPresent)
84+
})
7685
.unwrap_or_else(|_| {
7786
if railway_port.is_some() {
7887
"/app/data/rusty-red".to_string()
7988
} else {
8089
"data/rusty-red".to_string()
8190
}
8291
});
92+
let require_volume = env_bool(
93+
&["RUSTY_RED_REQUIRE_VOLUME", "THG_PRODUCT_REQUIRE_VOLUME"],
94+
railway_port.is_some(),
95+
);
96+
let volume_available = railway_volume_mount_path.is_some()
97+
|| env_bool(
98+
&["RUSTY_RED_VOLUME_MOUNTED", "THG_PRODUCT_VOLUME_MOUNTED"],
99+
false,
100+
);
83101
let durability = env_first(&["RUSTY_RED_DURABILITY", "THG_PRODUCT_DURABILITY"])
84102
.map(|value| RedCoreDurability::parse(&value))
85103
.unwrap_or(RedCoreDurability::AofEverysec);
@@ -139,6 +157,8 @@ impl Config {
139157
port,
140158
storage_mode,
141159
data_dir,
160+
require_volume,
161+
volume_available,
142162
durability,
143163
snapshot_interval_writes,
144164
strict_acid,
@@ -164,6 +184,15 @@ impl Config {
164184
}
165185

166186
pub fn validate(&self) -> Result<(), String> {
187+
if self.storage_mode == StorageMode::Embedded
188+
&& self.require_volume
189+
&& !self.volume_available
190+
{
191+
return Err(
192+
"RUSTY_RED_REQUIRE_VOLUME=true requires RAILWAY_VOLUME_MOUNT_PATH or RUSTY_RED_VOLUME_MOUNTED=true"
193+
.to_string(),
194+
);
195+
}
167196
if !self.strict_acid {
168197
return Ok(());
169198
}
@@ -228,6 +257,8 @@ mod tests {
228257
port: 8380,
229258
storage_mode: StorageMode::Embedded,
230259
data_dir: "data/rusty-red".to_string(),
260+
require_volume: false,
261+
volume_available: false,
231262
durability: RedCoreDurability::AofAlways,
232263
snapshot_interval_writes: 1_000,
233264
strict_acid: true,
@@ -260,4 +291,22 @@ mod tests {
260291
fn strict_acid_config_accepts_single_writer_serializable_embedded() {
261292
assert_eq!(base_config().validate(), Ok(()));
262293
}
294+
295+
#[test]
296+
fn embedded_config_rejects_missing_required_volume() {
297+
let mut config = base_config();
298+
config.require_volume = true;
299+
config.volume_available = false;
300+
301+
assert!(config.validate().unwrap_err().contains("REQUIRE_VOLUME"));
302+
}
303+
304+
#[test]
305+
fn embedded_config_accepts_required_available_volume() {
306+
let mut config = base_config();
307+
config.require_volume = true;
308+
config.volume_available = true;
309+
310+
assert_eq!(config.validate(), Ok(()));
311+
}
263312
}

0 commit comments

Comments
 (0)