it-pack / async-it
Portable, Capability-Oriented Asynchronous Execution and Scheduling for the ItPack Semantic Stack.
Requires
- php: ^8.5
- it-pack/run-it: @dev
- symfony/polyfill-io-poll: ^1.43
- symfony/polyfill-php86: ^1.43
Requires (Dev)
- amphp/amp: ^3.1
- codeception/module-asserts: ^3.3
- codeception/module-phpbrowser: ^4.1
- infection/codeception-adapter: ^0.4
- just-bases/php-obs-dev-base: ^0.1.0
- just-bases/php-qa-base: ^1.0
- just-bases/php-unit-test-base: ^1.0
- nyholm/psr7: ^1.8
- open-telemetry/api: ^1.9
- open-telemetry/sdk: ^1.0
- phparkitect/phparkitect: ^1.2
- psr/clock: ^1.0
- psr/event-dispatcher: ^1.0
- psr/http-client: ^1.0
- psr/http-server-handler: ^1.0
- psr/http-server-middleware: ^1.0
- psr/log: ^3.0
- react/react: ^1.0
- revolt/event-loop: ^1.0
- symfony/http-client: ^8.1
Suggests
- predis/predis: Required by ItPack\AsyncIt\Adapter\Redis\RedisTaskQueue for Redis-backed persistent task queue storage.
- prom/phprometheus: Required by ItPack\AsyncIt\Adapter\Prometheus\PrometheusTelemetryBridge for Prometheus metrics export.
Provides
None
Conflicts
None
Replaces
None
README
Portable, Capability-Oriented Asynchronous Execution and Scheduling for the ItPack Semantic Stack.
AsyncIt is a core component of the ItPack ecosystem. Rather than acting as a vendor-specific wrapper for existing asynchronous PHP engines, AsyncIt establishes an implementation-neutral, capability-driven execution platform. It defines the mechanical progression of asynchronous work strictly decoupled from underlying operating system multiplexers, polling engines, or telemetry frameworks.
1. The Five-Tier Semantic Stack
The ItPack architecture enforces the principle of "Consumption Without Responsibility". Higher-level packages consume complex asynchronous capabilities without inheriting the responsibility of managing low-level scheduling mechanics, Fiber suspension, or system polling:
| Tier | Layer | Package | The Core Question Answered | Focus |
|---|---|---|---|---|
| Tier 1 | Intent | Spindle | What is the business intent? | Pure business intent; oblivious to execution transport or timing. |
| Tier 2 | Orchestration | AExO | Why is it happening, and in what order? | Macro-coordination of agents, workflows, and retry/compensation logic. |
| Tier 3 | Execution | RunIt | What is an execution? (The Backbone) | Standardized identity, immutable context, occurrences, and measurements. |
| Tier 4 | Scheduling | AsyncIt | How does the work progress? | Schedulers, Fiber queue control, cooperative task execution, and timers. |
| Tier 5 | Platform | Engine/OS | How is I/O multiplexed? | Hardware triggers (e.g., native PHP 8.6 Io\Poll, kernel epoll/kqueue). |
AsyncIt is the canonical implementation of RunIt's ExecutionEngineInterface: it consumes RunIt's execution model (identity, immutable context, artifacts) and never redefines it. Higher tiers remain durable across engine evolution.
2. Core Architectural Pillars
A. Capability-First Architecture
AsyncIt avoids the "vendor-wrapping" trap by querying the runtime at the process boundary. The environment is analyzed once, and a queryable Capability Descriptor is generated:
- Instead of brittle checks like
extension_loaded('swoole'), AsyncIt asks functional questions: "Can you run Fibers natively? Do you support edge-triggered polling? Do you have native channel synchronization?" - It respects the asymmetry of the PHP async ecosystem—accepting that extension-based runtimes (Swoole, Swow) and pure-PHP loops (ReactPHP, Revolt) rely on incompatible process models. AsyncIt focuses on semantic portability instead of forcing incompatible loops to host one another.
- Each capability is categorized as
Supported,Emulated,Adapter-Only, orUnavailable. The descriptor is the single source of truth for selecting scheduling policy.
B. Fiber-Safe Context Propagation (The Sequence of Suspension)
Persistent-worker environments (Swoole, Swow, RoadRunner) carry severe risks of request leakage. AsyncIt enforces a strict architectural invariant when a Fiber yields control:
- Suspension: A Fiber hits an I/O wait boundary (e.g., awaiting stream readiness) and yields.
- Detachment: The RunIt Context Adapter immediately detaches the active context from the ambient Fiber scope. The context is locked away while the task is idle.
- Wait: The process safely blocks on low-level polling (e.g.,
Io\Poll\Context::wait()) with no context bound to the scope, preventing interleaved tasks from bleeding into one another. - Resumption & Re-attachment: Once signaled, AsyncIt resumes the Fiber, and the adapter safely re-attaches the matching execution context to the resumed Fiber scope.
C. Observed Cooperative Cancellation
AsyncIt rejects preemptive, violent task interruption (which can lead to file corruption and resource exhaustion). Cancellation is modeled as an observed state change:
- Schedulers transition tasks through a closed-state lifecycle:
created -> running -> terminal. - Running tasks cooperatively check their cancellation status. Upon observing a cancel signal, they perform a structured unwind and explicitly release OS-level file watchers (e.g.,
StreamPollHandleobjects) back to the polling context to prevent descriptor leaks. - Conflicting terminal transitions (e.g., succeeding an already
timed_outtask) raise aConflictingCompletionException. - In structured concurrency, cancellation cascades from parent to children unless explicitly detached.
3. The Concurrency Split: Polling vs. Scheduling
To future-proof the platform, AsyncIt enforces a strict separation of concerns:
$$\text{Polling (I/O Multiplexing)} \ \neq \ \text{Scheduling (Task Control)} \ \neq \ \text{Execution Semantics (Context & Telemetry)}$$
- Polling (Engine Layer): Managed natively in PHP 8.6 by the Polling API (
Io\Poll). It unifies low-level OS notifications (epoll, kqueue, WSAPoll) but does not manage fibers, task queues, or timers. ASelectPollerreference fallback provides the same behavioral contract within the legacystream_selectdescriptor ceiling. - Scheduling (AsyncIt Layer): Decides how work progresses. Schedulers coordinate task queues, cooperative cancellation cascades, and Fiber suspension/resumption.
- Execution Semantics (RunIt Layer): Defines what the execution is. It maps scheduler events directly to RunIt telemetry: task spawns are recorded as Occurrences, task durations (using monotonic clocks) as Measurements, and anomalies as Observations.
4. Conceptual Usage Example
declare(strict_types=1); use ItPack\AsyncIt\AsyncRuntime; use ItPack\AsyncIt\Application\Scope\ReferenceExecutionScope; // Initialize the portable AsyncIt runtime; capabilities are negotiated automatically. $runtime = AsyncRuntime::discover(); // Build an immutable execution scope from RunIt. $scope = new ReferenceExecutionScope('example'); // Run a single operation inside the reference reactor. $result = $runtime->run($scope, static function (): string { // The operation runs in a Fiber. Context is attached on resume // and detached under finally (conformance Assertion A). return 'Hello from AsyncIt'; }); echo $result; // Hello from AsyncIt
The
spawn()primitive is implemented onAsyncRuntime::spawn()and may be called from within a running execution to spawn a child task bound to the current execution scope. TheawaitReadable()/awaitWritable()primitives are declared onAwaitableIOInterfacebut are not yet exposed onAsyncRuntime; they are part of the next-generation roadmap (Phase 6+ indocs/ROADMAP.md) and will be added with conformance-gated semantics once the multi-task IO model lands. Seedocs/FEATURE_BACKLOG.mdfor details.
5. Public API Reference
SDK Entry Point
ItPack\AsyncIt\SDK\AsyncRuntime— The reference RunIt execution engine; discovered viaAsyncRuntime::discover(), thenrun(ExecutionScopeInterface, callable): mixed. Also exposeschannel(),telemetry(),capability().
Core Contracts
ItPack\AsyncIt\Contract\EngineSchedulerInterface— Scheduling port used by the RunItExecutionEngineInterface; extendsSchedulerInterfacewithdispatch().ItPack\AsyncIt\Contract\SchedulerInterface— Core scheduling contract.ItPack\AsyncIt\Contract\TaskInterface/TaskId— Task identity within an execution.ItPack\AsyncIt\Contract\PollerPortInterface— Port for readiness probing (register(),release(),await(),activeHandleCount()).ItPack\AsyncIt\Contract\TaskChannelInterface— Channel abstraction (native or emulated) for task coordination.ItPack\AsyncIt\Contract\RuntimeDiscoveryInterface— Runtime adapter discovery and capability negotiation.ItPack\AsyncIt\Domain\CapabilityDescriptor/CapabilityStatus/CapabilityKind— Negotiated capability model.ItPack\AsyncIt\Contract\CancellationScopeInterface— Observed cooperative cancellation boundary.ItPack\AsyncIt\Contract\TelemetryBridgeInterface— Telemetry integration port.
The
spawn()primitive is implemented onAsyncRuntime::spawn()(within a running execution).awaitReadable()/awaitWritable()are planned for Phase 6 (M11–M12). Seedocs/ROADMAP.mdfor sequencing.
6. Development and Conformance Roadmap
To ensure total behavioral compliance across vastly different runtime adapters, AsyncIt relies on a conformance-led QA pipeline. Adapters are continuously verified against the AsyncRuntimeVerifier test harness:
Contracts ──> Conformance (Verifiers) ──> Adapters
- M0 — Landscape Freeze: Finalize async landscape truths and
Io\Pollspecs. No code. - M1 — Semantic Boundary: Formalize boundaries (RunIt ↔ AsyncIt ↔ AExO) via Architecture Decision Records (ADRs).
- M2 — Capability Model: Taxonomize negotiable capability descriptors.
- M3 — Minimal Contracts: Establish the smallest contract set with negative tests and conformance verifiers.
- M4 — Reference Implementation: Build a pure-PHP deterministic test runtime to establish semantic baselines.
- M5 — RunIt Integration: Validate context isolation and detachment during suspension.
- M6 to M8 — Core Adapters: Develop Revolt (M6), Swoole (M7), and Swow (M8) drivers.
- M9 to M10 — Interoperability & Native: Implement userland interop layers and slide the native PHP 8.6
Io\PollAPI into its dedicated adapter slot.
AsyncRuntimeVerifier
An adapter is valid only when it passes the AsyncRuntimeVerifier suite. Mandatory assertions:
- Assertion A — Context Isolation: Interleave concurrent tasks and prove zero context bleed across suspension/resumption cycles.
- Assertion B — Temporal Precision: Verify monotonic-clock integration; durations must be free of wall-clock skew.
- Assertion C — Deterministic Cleanup: Prove no dangling watchers; every
StreamPollHandleand timer is released immediately after timeout or cancellation.
A single leaked descriptor or one context-bleed is a conformance failure.
6. Relationship to RunIt
AsyncIt does not own an execution model—it consumes RunIt's. RunIt owns identity, immutable context, and telemetry artifacts; AsyncIt owns the mechanics that move work forward. Publication into RunIt's artifact stream uses a port with a recursion-safe failure policy, and context propagation relies exclusively on RunIt's ContextStorageInterface. AsyncIt must never invent a parallel context system.
7. PSR posture
PSR packages (psr/clock, psr/log, psr/event-dispatcher, psr/http-*) are not
runtime dependencies of AsyncIt. They appear only in require-dev for adapter
test suites and in suggest where PSR-compatible bridges are optional. The core
scheduling and runtime-discovery contracts depend solely on RunIt's own port
interfaces.
License
AsyncIt is distributed under the MIT License. All core conceptual definitions align with the canonical ItPack OWL ontologies.