Skip to content

Commit 7a24092

Browse files
committed
address P0/P1/P2 review findings (except #17)
Signed-off-by: Teryl Taylor <terylt@ibm.com>
1 parent 6a24649 commit 7a24092

8 files changed

Lines changed: 1197 additions & 160 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ serde_json = "1"
2828
async-trait = "0.1"
2929
thiserror = "2"
3030
tracing = "0.1"
31-
uuid = { version = "1", features = ["v4"] }
31+
uuid = { version = "1", features = ["v4", "serde"] }
3232
paste = "1"
3333
futures = "0.3"
3434
hashbrown = "0.15"

crates/cpex-core/src/config.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,18 @@ pub struct PluginSettings {
100100
/// Whether to halt the pipeline on any plugin error.
101101
#[serde(default)]
102102
pub fail_on_plugin_error: bool,
103+
104+
/// Maximum number of entries in the routing cache.
105+
///
106+
/// When the cache reaches this size, new resolutions are computed
107+
/// normally but not memoized — the cache rejects further inserts
108+
/// and emits a warning. This bounds memory growth from
109+
/// attacker-controlled entity names without the reasoning hazards
110+
/// of eviction (silently dropped entries, stale-vs-current
111+
/// confusion). Operators see the warning and tune the cap or
112+
/// investigate the entity-name growth.
113+
#[serde(default = "default_route_cache_max_entries")]
114+
pub route_cache_max_entries: usize,
103115
}
104116

105117
impl Default for PluginSettings {
@@ -110,10 +122,15 @@ impl Default for PluginSettings {
110122
short_circuit_on_deny: true,
111123
parallel_execution_within_band: false,
112124
fail_on_plugin_error: false,
125+
route_cache_max_entries: default_route_cache_max_entries(),
113126
}
114127
}
115128
}
116129

130+
fn default_route_cache_max_entries() -> usize {
131+
10_000
132+
}
133+
117134
fn default_timeout() -> u64 {
118135
30
119136
}

crates/cpex-core/src/context.rs

Lines changed: 80 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ use std::collections::HashMap;
2323

2424
use serde::{Deserialize, Serialize};
2525
use serde_json::Value;
26+
use uuid::Uuid;
2627

2728
// ---------------------------------------------------------------------------
2829
// Plugin Context
@@ -107,13 +108,84 @@ impl Default for PluginContext {
107108
// Plugin Context Table
108109
// ---------------------------------------------------------------------------
109110

110-
/// Lookup table of `PluginContext` instances indexed by plugin ID.
111+
/// Threaded execution state carried from one hook invocation to the next
112+
/// within a single request lifecycle (e.g., `pre_invoke` → `post_invoke`).
111113
///
112-
/// Threaded across hook invocations so that a plugin's `local_state`
113-
/// persists from one hook to the next within the same request lifecycle
114-
/// (e.g., `pre_invoke` → `post_invoke`).
114+
/// The table holds the canonical pipeline state in two parts:
115115
///
116-
/// The caller receives the table back in `PipelineResult` and passes
117-
/// it into the next hook invocation. On the first hook call, pass
118-
/// `None` — the executor creates fresh contexts for each plugin.
119-
pub type PluginContextTable = HashMap<String, PluginContext>;
116+
/// - `global_state` — a single shared map across all plugins. The executor
117+
/// clones this into each plugin's `PluginContext.global_state` at the
118+
/// start of a run, then commits the plugin's possibly-modified copy back
119+
/// when the run completes (last-writer-wins for serial phases).
120+
/// - `local_states` — per-plugin private state, indexed by plugin ID.
121+
/// Persists across hook invocations so a plugin's `pre_invoke` can stash
122+
/// data its `post_invoke` will read.
123+
///
124+
/// Storing `global_state` once (rather than copying it inside every per-plugin
125+
/// `PluginContext`) makes the canonical state explicit and removes the
126+
/// non-deterministic "pick an arbitrary plugin's snapshot" pattern that was
127+
/// previously needed to recover it.
128+
///
129+
/// Returned by the executor in `PipelineResult` and passed back into the
130+
/// next hook call. On the first hook call pass `None` — the executor
131+
/// creates a fresh table.
132+
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
133+
pub struct PluginContextTable {
134+
/// Authoritative shared state across all plugins in the pipeline.
135+
#[serde(default)]
136+
pub global_state: HashMap<String, Value>,
137+
138+
/// Per-plugin local state, indexed by plugin ID (`Uuid`).
139+
#[serde(default)]
140+
pub local_states: HashMap<Uuid, HashMap<String, Value>>,
141+
}
142+
143+
impl PluginContextTable {
144+
/// Create an empty context table.
145+
pub fn new() -> Self {
146+
Self::default()
147+
}
148+
149+
/// Build a `PluginContext` for the given plugin, *removing* its stored
150+
/// local_state from the table and seeding it with a fresh clone of the
151+
/// canonical global_state. Use in serial phases where the plugin will
152+
/// commit its local_state changes back via [`store_context`].
153+
///
154+
/// If the plugin has no stored local_state yet, its context starts
155+
/// empty (first invocation in the request lifecycle).
156+
pub fn take_context(&mut self, plugin_id: Uuid) -> PluginContext {
157+
PluginContext {
158+
local_state: self.local_states.remove(&plugin_id).unwrap_or_default(),
159+
global_state: self.global_state.clone(),
160+
}
161+
}
162+
163+
/// Build a `PluginContext` for the given plugin without mutating the
164+
/// table — the local_state is *cloned* and the global_state is cloned.
165+
/// Use in read-only phases (audit, concurrent, fire-and-forget) where
166+
/// per-plugin mutations should not influence subsequent plugins.
167+
pub fn snapshot_context(&self, plugin_id: Uuid) -> PluginContext {
168+
PluginContext {
169+
local_state: self.local_states.get(&plugin_id).cloned().unwrap_or_default(),
170+
global_state: self.global_state.clone(),
171+
}
172+
}
173+
174+
/// Commit a plugin's context back into the table after it ran. Replaces
175+
/// the canonical global_state with the plugin's possibly-modified copy
176+
/// (move, no clone) and stores the plugin's local_state for next time.
177+
pub fn store_context(&mut self, plugin_id: Uuid, ctx: PluginContext) {
178+
self.global_state = ctx.global_state;
179+
self.local_states.insert(plugin_id, ctx.local_state);
180+
}
181+
182+
/// Number of plugins with stored local_state in the table.
183+
pub fn len(&self) -> usize {
184+
self.local_states.len()
185+
}
186+
187+
/// Whether the table holds no per-plugin local_state.
188+
pub fn is_empty(&self) -> bool {
189+
self.local_states.is_empty()
190+
}
191+
}

0 commit comments

Comments
 (0)