# Core Reactor Guide

This document describes the low-level Linux::Event XS reactor. Applications
that want resource ownership should normally choose a concrete public leaf
below `Linux::Event::IO` or `Linux::Event::Kernel` instead of implementing fd
lifecycle themselves.

## Mental model

Linux::Event separates readiness from I/O policy:

```text
kernel epoll
    |
    v
Linux::Event::Loop
    |
    v
opaque native registration
    |
    +-- error callback   EPOLLERR / EPOLLHUP / EPOLLRDHUP
    +-- read callback    EPOLLIN
    `-- write callback   EPOLLOUT
```

The reactor tells you what can be attempted. A raw callback decides whether to
`sysread`, `syswrite`, accept, drain a Linux fd, or perform another operation
appropriate to that descriptor.

Higher-level public leaves own those policies for common resources:

```text
Linux::Event::IO::Pipe
Linux::Event::IO::TTY
Linux::Event::IO::Sock::Stream
Linux::Event::IO::Sock::Listener
Linux::Event::IO::Sock::Dgram
Linux::Event::Kernel::Timer
Linux::Event::Kernel::Signal
Linux::Event::Kernel::Event
Linux::Event::Kernel::Process
```

`IO` and `Kernel` are namespace categories, not public base objects.

## Constructing a loop

```perl
use Linux::Event::Loop;
my $loop = Linux::Event::Loop->new;
```

Each Loop owns one epoll instance, its native fd registry, scheduled timer
state, and the private registrations used by attached higher-level resources.
Concrete public objects can accept `loop => $loop` or attach later through:

```perl
$loop->add($object);
```

`add()` returns the same object.

## Registering a handle directly

Normal raw application code should label the watched resource explicitly:

```perl
my $registration = $loop->watch(
    fh   => $fh,
    data => { connection_id => 42 },
    read => sub ($registration) {
        my $count = sysread($registration->fh, my $bytes, 8192);
        $registration->cancel if defined($count) && $count == 0;
    },
    write => sub ($registration) {
        $registration->disable_write;
    },
    error => sub ($registration) {
        $registration->cancel;
    },
);
```

The returned value is an opaque native registration handle. Its internal class
name is not public and it must not be subclassed.

Supplying `fh` lets Linux::Event retain the handle and resolve its integer fd
once at registration. If only an integer descriptor is available:

```perl
my $registration = $loop->watch(
    fd   => $fd,
    read => sub ($registration) { $registration->cancel },
);
```

Exactly one of `fh` or `fd` is required. A registration created from `fd`
alone has no retained filehandle, so `$registration->fh` is `undef`.

The lower-level positional form remains available:

```perl
$loop->watch_fd($fd, read => sub ($registration) {
    $registration->cancel;
});
```

`watch_fd` is useful for internal code and measured registration-heavy
workloads. `watch()` and `watch_fd()` create the same native readiness record
and have the same steady-state dispatch path.

## Registration replacement and lifetime

One active registration is allowed per integer fd. Registering the fd again
replaces the old native record. The old opaque handle becomes inert and cannot
cancel or alter the replacement even if the kernel later reuses the fd number.

Cancellation and Loop destruction release retained Perl state as soon as no
active callback is dispatching through the registration.

## Callback order

For one returned epoll event the dispatch order is:

1. terminal/error callback
2. read callback
3. write callback

The registration is rechecked after every callback. Self-cancellation can
therefore prevent later readiness types for the same returned event.

## Reading until EAGAIN

A level-triggered raw callback normally drains the fd:

```perl
read => sub ($registration) {
    my $fh = $registration->fh;

    while (1) {
        my $buf = '';
        my $n = sysread($fh, $buf, 8192);

        if (!defined $n) {
            last if $!{EAGAIN} || $!{EWOULDBLOCK};
            $registration->cancel;
            last;
        }

        if ($n == 0) {
            $registration->cancel;
            last;
        }

        handle_bytes($buf);
    }
},
```

The repeated Perl syscall, buffering, queue, and lifecycle work is intentionally
visible in this low-level API. The ordered-byte engine used by
`IO::Pipe`, `IO::TTY`, and `IO::Sock::Stream` moves those mechanics into native
code when Linux::Event should own them.

## Writable interest

Raw code should generally enable writable readiness only while output is
blocked:

```perl
$registration->enable_write;
$registration->disable_write;
```

The ordered-byte public leaves manage this transition automatically through
their native write queue.

## Driving the loop

Persistent drive:

```perl
$loop->run;
```

Stop from a callback:

```perl
$loop->stop;
```

Single iteration:

```perl
my $events = $loop->run_once(-1);  # block indefinitely
my $events = $loop->run_once(0);   # poll
my $events = $loop->run_once(50);  # wait at most 50 ms
```

Bounded drive:

```perl
$loop->run_for(0.250);
```

`run_for` uses a monotonic deadline. Only one driver method may be active on a
particular Loop. Recursive drive of the same Loop throws; a callback may drive
a different Loop. Callback exceptions propagate after Loop driver and dispatch
state has been restored.

## No-argument fast callbacks

A closure that already captures everything it needs can omit the registration
argument:

```perl
my $registration = $loop->watch(
    fh      => $fh,
    read    => sub { drain_socket($fh) },
    no_args => 1,
    lean    => 1,
);
```

`lean` is available only on this no-argument path. It reduces retained Perl
state and is intended for expert, measured hot paths.

## Edge-triggered and oneshot modes

`watch()` accepts:

```perl
edge_triggered => 1
oneshot        => 1
```

Use them only when the application implements their Linux semantics. An
edge-triggered reader must drain until EAGAIN. An oneshot registration must be
rearmed before more readiness can be delivered.

## Introspection

The Loop can query managed objects, native resources, liveness reasons, and
conservative capacity pressure without maintaining duplicate hot-path
bookkeeping:

```perl
my $objects   = $loop->objects;
my $snapshot  = $loop->inspect($objects->[0]);
my $census    = $loop->census;
my $resources = $loop->resources;
my $reasons   = $loop->why_alive;
my $pressure  = $loop->pressure;
```

`running` is an O(1) native driver-state query. Other object/resource views are
assembled from authoritative registries when requested. See
[Loop Introspection](INTROSPECTION.md) for exact shapes and complexity.

## Statistics and profiling

```perl
my $stats = $loop->stats;
$loop->reset_stats;
```

Cheap counters cover epoll waits, readiness classes, callback calls,
`epoll_ctl`, batching, watcher lifecycle, timers, and drive methods.

Optional nanosecond profiling is explicit:

```perl
$loop->profile(1);
# measured workload
my $stats = $loop->stats;
$loop->profile(0);
```

Profiling changes the measured workload and should remain disabled for normal
throughput comparisons.

## Measured reactor defaults

The current measured defaults are:

- event capacity: 8192
- callback scope limit: 128
- aggressive watcher reclaim: disabled

Setters remain available for controlled experiments. They are not general
application tuning requirements.

## What the raw reactor intentionally does not own

The low-level reactor does not provide application-level:

- input buffering;
- output queues;
- partial-write management;
- framing or codecs;
- backpressure policy;
- socket connection semantics;
- packet semantics;
- process lifecycle semantics;
- Future, Promise, or coroutine scheduling.

Those responsibilities belong to the appropriate semantic layer. Ordered bytes
belong to the private engine behind `IO::Pipe`, `IO::TTY`, and
`IO::Sock::Stream`; packet boundaries belong to `IO::Sock::Dgram`; listening
ownership belongs to `IO::Sock::Listener`; kernel scheduling/notification state
belongs to the concrete `Kernel::*` leaves.

Third-party async/await or Future layers may compose Linux::Event without
changing that division. A new core primitive should be added only when an
external implementation demonstrates that the existing reactor cannot express
the required behavior safely or efficiently.
