Reactor Model
In otavia, each ActorThread owns an IoHandler (for NIO, this is a NioHandler wrapping a Java NIO Selector) and runs a three-phase loop: IO select/process, IO pipeline (ChannelsActors), and business logic (StateActors). The IO layer is transparent to most users — ChannelsActor encapsulates all IO interactions.
IoHandler
IoHandler is the per-thread IO engine. Each ActorThread creates its own instance via TransportFactory.openIoHandler(). All channel IO operations (register, bind, connect, read, flush, close, etc.) are submitted to the thread's own ioHandler.
ActorThread
└── ioHandler (IoHandler)
├── Selector (NIO Selector, one per thread)
├── run(ioCtx) ── select + processSelectedKeys
├── register(channel)
├── bind(channel, local)
├── connect(channel, remote, local, fastOpen)
├── read(channel, plan)
├── flush(channel, payload)
├── close(channel)
└── ...
NioHandler
NioHandler is the concrete IoHandler implementation for Java NIO, wrapping a java.nio.channels.Selector.
Selector Optimization
It uses Unsafe or reflection to replace the JDK's internal selectedKeys and publicSelectedKeys fields in SelectorImpl with SelectedSelectionKeySet — a custom Array[SelectionKey]-backed set that avoids iterator allocation overhead. This is the same optimization Netty uses.
Core Methods
run(context): Main entry point — calls select(context) then processSelectedKeys().
select(context): If context.canNotBlock, calls selector.selectNow() (non-blocking); otherwise calls selector.select() (blocking).
processSelectedKeys(): Iterates the optimized key array, calling processSelectedKey(key) for each.
processSelectedKey(key): Extracts NioUnsafeChannel from key.attachment() and calls processor.handle(key).
Epoll Bug Workaround
When the NIO selector returns empty results 512 consecutive times (configurable via io.otavia.selectorAutoRebuildThreshold), NioHandler rebuilds the Selector to work around the JDK epoll 100% CPU bug.
IO Within the ActorThread Loop
The IO phase is Phase 1 of the ActorThread three-phase loop. During this phase, ioHandler.run(ioCtx) performs NIO select and processes ready keys. Raw bytes are read from the socket and processed through the channel pipeline on the current thread. Decoded messages then enter the Actor mailbox (via the Inflight system), and the ChannelsActor processes them in Phase 2.
The ioRatio configuration (default 50) determines how much time is allocated to IO versus business logic within each loop iteration.
SPI Mechanism
The IO transport layer is implemented via SPI (Service Provider Interface). TransportFactory creates the concrete IoHandler, ChannelFactory, and Reactor implementations.
- Default:
NIOTransportFactory→ NIO-based transport - Pluggable: Replace with epoll/io_uring implementations by adding the JAR to the CLASSPATH
The native-transport project aims to provide epoll- and io_uring-based implementations.
NIO Transport Implementations
NioUnsafeSocketChannel (TCP)
- Read: Allocates page buffer, reads from
SocketChannel, callschannel.handleChannelReadBuffer()directly on the current thread - Write: Writes
RecyclablePageBufferchain toSocketChannel, enablesOP_WRITEon partial write - Connect: Non-blocking connect, sets
OP_CONNECTif not immediately ready
NioUnsafeServerSocketChannel (TCP Server)
- Accept: Calls
javaChannel.accept(), creates newNioSocketChannel, sendsAcceptedEventto the actor mailbox
NioUnsafeDatagramChannel (UDP)
- Read: Calls
ch.receive(byteBuffer), sendsReadBufferevent to actor mailbox - Write: Writes buffer directly to
DatagramChannel