Description
PPE is async all the way down and tokio is an unconditional dependency of every core crate. That forces a tokio runtime on anyone embedding the engine, including callers that already have their own async dispatcher in the filter and callers that are plain sync. We want a version of PPE that does not use tokio at all, with tokio available as an opt-in feature for the callers that want the concurrent paths.
Where tokio is today:
ppe-orchestration is entirely tokio: JoinSet, timeout, sleep. It is the shared fan-out primitive for concurrent plugin phases and APL parallel: blocks.
ppe-core/src/executor.rs uses tokio::spawn, JoinHandle, TaskTracker for fire-and-forget plugins, and timeout for per-plugin deadlines.
ppe-core/src/engine.rs uses tokio::spawn, sleep, yield_now.
ppe-apl-runtime uses tokio::sync::Mutex in the cmf, delegation, and elicitation invokers.
- Unconditional dep in
ppe-core, ppe-apl-core, ppe-apl-cmf, ppe-apl-runtime, ppe-orchestration, ppe.
The bigger cost is the trait surface. Every extension point is #[async_trait]: plugin handlers, PDP resolvers, session stores, route handlers, the invokers. A sync API is not a wrapper over the async one, it is a second shape for those traits. Decide that shape before writing code.
Decisions to make first
- Do the traits go sync with async implementations adapting at the boundary, or do we carry two trait sets behind cfg? Two trait sets means every builtin gets written twice.
- What does sync mode do with concurrent phases and
parallel: blocks? Options are run them serially, or drive them on a thread pool. Serial is simpler and changes observable behavior, so it needs to be documented, not silently different.
- Fire and forget plugins have no sync equivalent without spawning a thread. Decide whether sync mode drops them, runs them inline, or requires a caller supplied executor.
- Per-plugin timeouts currently come from
tokio::time::timeout. Sync needs a different mechanism, and cancelling a blocked sync call is not generally possible. Decide what a timeout means in sync mode.
Acceptance Criteria
Description
PPE is async all the way down and tokio is an unconditional dependency of every core crate. That forces a tokio runtime on anyone embedding the engine, including callers that already have their own async dispatcher in the filter and callers that are plain sync. We want a version of PPE that does not use tokio at all, with tokio available as an opt-in feature for the callers that want the concurrent paths.
Where tokio is today:
ppe-orchestrationis entirely tokio:JoinSet,timeout,sleep. It is the shared fan-out primitive for concurrent plugin phases and APLparallel:blocks.ppe-core/src/executor.rsusestokio::spawn,JoinHandle,TaskTrackerfor fire-and-forget plugins, andtimeoutfor per-plugin deadlines.ppe-core/src/engine.rsusestokio::spawn,sleep,yield_now.ppe-apl-runtimeusestokio::sync::Mutexin the cmf, delegation, and elicitation invokers.ppe-core,ppe-apl-core,ppe-apl-cmf,ppe-apl-runtime,ppe-orchestration,ppe.The bigger cost is the trait surface. Every extension point is
#[async_trait]: plugin handlers, PDP resolvers, session stores, route handlers, the invokers. A sync API is not a wrapper over the async one, it is a second shape for those traits. Decide that shape before writing code.Decisions to make first
parallel:blocks? Options are run them serially, or drive them on a thread pool. Serial is simpler and changes observable behavior, so it needs to be documented, not silently different.tokio::time::timeout. Sync needs a different mechanism, and cancelling a blocked sync call is not generally possible. Decide what a timeout means in sync mode.Acceptance Criteria
cargo build --no-default-featureson the workspace produces no tokio in the dep tree, verified withcargo tree -i tokioPolicyEnginecovering config load, initialize, invoke, and shutdownppe-orchestrationis either feature gated whole or split so the sync path does not pull ittokio::sync::Mutexin the apl-runtime invokers replaced with a std or parking_lot lock where the critical section does not await