Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,23 @@ a package with no scheduler can offer: every primitive also exposes its non-bloc
(`trySend()`/`tryRecv()`/`tryLock()`/`readSlot()`) plus `notificationStream()`, so a
coroutine runtime can park a Fiber in its own event loop instead.

A capacity-0 channel needs one thing more, because its gate is "is a receiver waiting" and a
consumer with its own scheduler is never inside `recv()`:

```php
$token = $channel->registerReceiver(); // null => a record is already there, take it now
// ... park the Fiber on notificationStream() in the consumer's own event loop ...
$channel->cancelReceiver($token); // on unpark, whatever woke it

$ticket = $channel->trySendTicket($value); // deposits only while a receiver is waiting
$done = $channel->isTicketTaken($ticket); // the handshake completes when it is TAKEN
```

The registration is a claim about presence, never about storage — the record goes into the
one ring slot a capacity-0 channel allocates — so `cancelReceiver()` never has a value in its
hands and can always succeed. A registration can outlive its process, so each waiter entry
carries its owner pid and a rendezvous deposit reaps the dead ones before it reads the gate.

### Shared closures (registered before the fork)

A closure compiled **before the fork** is valid in every worker: the family inherited the
Expand Down
17 changes: 17 additions & 0 deletions docs/shared-memory-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,23 @@ sentence.
dedicated mutex (a structure locked on every operation does not belong on a shared stripe).
Head and tail are monotonic counters, so fill level is a subtraction; capacity 0 is a true
cross-process rendezvous; `close()` crosses processes;
- a rendezvous accepts a value only while a receiver is waiting, and a consumer with its own
scheduler is never inside `recv()` — so `registerReceiver()`/`cancelReceiver()` (and their
sender mirrors) let a receiver parked in someone else's event loop count as the partner.
The registration is a claim about **presence, never about storage**: the record still goes
into the single ring slot a capacity-0 channel allocates, so a cancellation can always
succeed — it never has a value in its hands — and a record deposited against a
registration that is withdrawn a moment later simply waits in the ring for the next
receiver while its sender stays parked. The whole handshake (register, re-check, deposit,
cancel) happens under the channel's own mutex, so the happens-before edge is the same
release/acquire pair it always was;
- a registration outlives the call that made it, and can therefore outlive its process. Each
waiter entry packs `owner pid << 32 | wake slot + 1` into one aligned word (two words would
be a 16-byte record, and those tear — §5), and a rendezvous deposit reaps the entries whose
owner is gone before it reads the gate, so a dead worker cannot go on standing in for a
partner. Liveness is `posix_kill(pid, 0)` **plus** the wake registry still naming that pid
as the slot's owner, because a dead owner's slot is recycled to the next process that
claims one;
- `SharedArray` — fixed-capacity vector of records, per-instance stripe: the container a
`zend_array` cannot be (§4);
- `ResultSlotTable` — futures. A slot settles exactly once **per generation**, carrying either
Expand Down
12 changes: 12 additions & 0 deletions src/Ipc/IpcException.php
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,18 @@ public static function slotTableFormat(int $found, int $expected): self
));
}

public static function waiterTableFull(string $role, int $capacity): self
{
return new self(sprintf(
'The %s waiter table of this channel is full: all %d entries hold a registration. Waiter ' .
'tables are pre-sized in the arena and never grow - a grown table would be reallocated ' .
'into one process\'s private heap - so create the channel with a larger waiterCapacity ' .
'before the workers fork.',
$role,
$capacity,
));
}

public static function invalidCapacity(string $structure, int $capacity): self
{
return new self(sprintf('%s capacity must be a positive number of records, got %d', $structure, $capacity));
Expand Down
Loading
Loading