Skip to content

feat(core): sync API with tokio behind a feature gate #21

Description

@terylt

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

  1. 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.
  2. 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.
  3. 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.
  4. 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

  • cargo build --no-default-features on the workspace produces no tokio in the dep tree, verified with cargo tree -i tokio
  • A sync API on PolicyEngine covering config load, initialize, invoke, and shutdown
  • Async support lives behind a feature, off or on by default per the decision above, with the feature named consistently across all crates
  • ppe-orchestration is either feature gated whole or split so the sync path does not pull it
  • tokio::sync::Mutex in the apl-runtime invokers replaced with a std or parking_lot lock where the critical section does not await
  • Behavior differences between sync and async mode are documented, specifically concurrency, fire and forget, and timeouts
  • Existing async tests still pass with the feature on
  • A sync integration test covering a full route evaluation with a PDP step and a plugin step
  • CI builds both feature configurations

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions