ADR-0033: May Runtime Ownership and Dependency Compatibility
Status
Accepted
Date
2026-08-23
Owners
May and Mayfly runtime maintainers
Related work
- Postmortem: May Runtime Substrate Violation
- ADR-0003: Task Scheduler Model
- ADR-0032: Durable Workflow Runtime
Context
Mayfly is built on May, a stackful coroutine runtime. May is not merely a coroutine syntax or context-switching library. It already owns:
- coroutine creation, scheduling, queues, and work stealing;
- parking, resumption, cancellation, joining, and sleep;
- timers and worker wakeups;
- native I/O readiness through epoll/eventfd, kqueue, and IOCP;
- coroutine-aware raw descriptor I/O through
CoIoandWaitIo; - stackful context switching through architecture-specific assembly supplied by
generator.
Mayfly added MIO polling and a separate runnable queue, while its daemon added
Tokio and Hyper. These facilities impose competing schedulers, reactors, timer
models, cancellation semantics, and resource lifecycles. Mayfly cannot make a
May coroutine runnable by manipulating a JoinHandle; only May owns the
concrete parked coroutine and can schedule it.
ADR-0003 correctly rejects tokio::spawn, but its generic wording permitted an
implementation to rebuild facilities already provided by May. This ADR
clarifies and partially supersedes ADR-0003 wherever it implies that Mayfly
owns coroutine execution.
Decision
May is the exclusive execution substrate for Mayfly. No Mayfly crate may introduce a second coroutine scheduler, async executor, I/O reactor, timer wheel, or coroutine lifecycle owner.
The ownership boundary is:
flowchart TB
App["Application policy, including Cylon"]
Workflow["Mayfly durable workflow policy"]
Adapter["Narrow May execution adapter"]
May["May runtime"]
Native["epoll, kqueue, IOCP, timers, assembly context switch"]
App --> Workflow
Workflow --> Adapter
Adapter --> May
May --> Native
May owns execution mechanism
May exclusively owns:
- when and where a coroutine runs;
- the concrete suspended coroutine and its stack;
- park/wake and readiness registration;
- timer and cancellation wakeups;
- worker scheduling and work stealing;
- native I/O dispatch and context switching.
Mayfly owns policy
Mayfly owns:
- durable workflow identity and event history;
- dependency, priority, retry, budget, and transition policy;
- handler registration and dispatch intent;
- WAL/replay/recovery semantics;
- typed outcomes and application-neutral orchestration.
Mayfly may ask a narrow adapter to execute or cancel work. It must receive typed completion facts back from that adapter. It must not maintain a second queue that purports to resume May coroutines.
Dependency compatibility rule
Tokio and MIO are forbidden runtime dependencies in May and Mayfly.
Any library that directly or transitively depends on Tokio is incompatible with Mayfly. This includes libraries offering a synchronous or blocking facade while internally requiring Tokio. Any library that directly or transitively depends on MIO is likewise incompatible because May already owns native I/O readiness.
The prohibition applies to:
- normal, build, and development dependencies;
- default and optional features;
- every supported target;
- binaries, tests, examples, benchmarks, and tooling executed in the Mayfly process or used to validate its runtime behavior.
Moving Tokio/MIO to another native thread, hiding it behind an adapter, or making it optional does not make it compatible.
The names tokio and mio are enforceable minimum bans. Absence of those
names is not sufficient proof. A dependency is also incompatible when it owns
another executor/reactor, blocks May workers without an approved isolation
boundary, assumes async thread-local context, or imposes lifecycle semantics
that May cannot control.
Dependency admission procedure
Before adding or upgrading a dependency, the author must:
- inspect its complete Cargo feature and target graph;
- run the repository runtime-dependency audit with all features and targets;
- identify any executor, reactor, timer, signal, thread, or cancellation ownership in its documentation and source;
- demonstrate how blocking or readiness integrates through May primitives;
- add contract tests for cancellation and shutdown where the dependency owns external resources;
- obtain an ADR amendment when ownership cannot be proven compatible.
Version upgrades are architecture changes when they alter a forbidden-runtime edge.
Enforcement and migration
tools/check_runtime_dependencies.py reads Cargo metadata and compares every
package with transitive reachability to a forbidden runtime against the
versioned ledger in
.config/runtime-dependency-policy.toml.
- A new or changed dependency path fails locally and in CI, even when it reaches Tokio or MIO through an already-known incompatible library.
- A stale ledger entry also fails, forcing the exception to be removed when remediation lands.
- Existing entries are temporary incompatibilities, not permissions.
- The migration is complete only when the ledger is empty and Cargo metadata contains neither Tokio nor MIO.
The initial ledger recorded 18 versioned reachability pairs arising from ten direct violations discovered on 2026-08-23. Remediation removed those paths; the ledger is now empty and any Tokio or MIO reachability is a CI failure.
Required implementation direction
The completed scheduler cleanup:
- pin Mayfly to an intentional, tested May revision;
- remove the direct MIO dependency and
async-iofeature; - delete
IoSource, MIO poll/token state, and the duplicate scheduler loop; - removed synthetic I/O readiness and uses May coroutine-aware park/wake
channels for policy dispatch; future descriptor work must use May
CoIo/WaitIo; - reduce the Mayfly scheduler to policy and accounting that does not claim execution ownership;
- replace Tokio/Hyper/Reqwest and MIO-backed Notify paths in the daemon;
- prove single execution, parking, readiness, timers, cancellation, shutdown, and recovery through substrate contract tests.
Consequences
Positive
- Coroutine execution has one owner and one lifecycle model.
- Native I/O, cancellation, and timers use the system-level implementation already maintained in May.
- Mayfly’s durable workflow policy can reason from typed execution facts rather than duplicate scheduling state.
- Dependency drift becomes visible during review and CI.
Negative
- Common Tokio-first Rust libraries cannot be used, even through blocking facades.
- HTTP, filesystem notification, gRPC, and client integrations may require May-native adapters or different libraries.
- The current daemon and scheduler require substantial remediation.
- The local May fork must be versioned and consumed deliberately, increasing coordination between the May and Mayfly repositories.
Alternatives rejected
Keep MIO as an optional scheduler feature
Rejected because optional compilation does not resolve duplicate reactor and coroutine ownership. It also permits two implementations to drift.
Run Tokio on a dedicated thread
Rejected because thread separation does not unify timers, cancellation, shutdown, resource ownership, or coroutine wake semantics.
Let Mayfly own scheduling while May only switches stacks
Rejected because that is not May’s implemented contract. May stores and reschedules the concrete coroutine through its own scheduler, parkers, timers, and native selectors.
Permit Tokio only in tests
Rejected because tests must exercise the same runtime assumptions as production. A Tokio-based test client can also conceal shutdown and timing defects at the system boundary.
Compliance test
The following must pass before completion is claimed:
python3 tools/check_runtime_dependencies.py --all-targets
The expected result is zero forbidden edges and an empty exception ledger.