Skip to content

Commit 503bbd1

Browse files
committed
Add additional documentation
Most of it has been copied over from https://github.com/revoltphp/event-loop.
1 parent c990a98 commit 503bbd1

File tree

6 files changed

+407
-17
lines changed

6 files changed

+407
-17
lines changed

assets/styles/screen.css

+9
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,15 @@ pre::-webkit-scrollbar-thumb:hover {
253253
color: #666;
254254
}
255255

256+
.note {
257+
margin: 16px 0;
258+
padding: 0 16px;
259+
background: #fff;
260+
border: 2px solid var(--color-highlight);
261+
border-radius: 4px;
262+
font-size: 85%;
263+
}
264+
256265
.mt-1 {
257266
margin-top: 4px !important;
258267
}

fundamentals.md

+141-8
Original file line numberDiff line numberDiff line change
@@ -20,27 +20,27 @@ Cooperative multitasking works by telling the scheduler which events we're inter
2020
The scheduler will invoke a callback once the event happened.
2121
Revolt supports the following events:
2222

23-
- **Defer**
23+
- [**Defer**](/timers)
2424

2525
{:.small-hint .mt-n2 .mb-1}
2626
The callback is executed in the next iteration of the event loop. If there are defers scheduled, the event loop won't wait between iterations.
27-
- **Delay**
27+
- [**Delay**](/timers)
2828

2929
{:.small-hint .mt-n2 .mb-1}
3030
The callback is executed after the specified number of seconds. Fractions of a second may be expressed as floating point numbers.
31-
- **Repeat**
31+
- [**Repeat**](/timers)
3232

3333
{:.small-hint .mt-n2 .mb-1}
3434
The callback is executed after the specified number of seconds, repeatedly. Fractions of a second may be expressed as floating point numbers.
35-
- **Stream readable**
35+
- [**Stream readable**](/streams)
3636

3737
{:.small-hint .mt-n2 .mb-1}
3838
The callback is executed when there's data on the stream to be read, or the connection closed.
39-
- **Stream writable**
39+
- [**Stream writable**](/streams)
4040

4141
{:.small-hint .mt-n2 .mb-1}
4242
The callback is executed when there's enough space in the write buffer to accept new data to be written.
43-
- **Signal**
43+
- [**Signal**](/signals)
4444

4545
{:.small-hint .mt-n2 .mb-1}
4646
The callback is executed when the process received a specific signal from the OS.
@@ -50,11 +50,13 @@ The scheduler runs a loop that does a few things in each iteration:
5050

5151
- Check for any deferred callbacks
5252
- Check for actionable timer / stream / signal events
53-
- Wait until the next timer watcher expires (unless there are `defer` events to be executed)
53+
- Wait until the next timer callback expires (unless there are `defer` events to be executed)
5454

5555
The event loop controls the program flow as long as it runs.
5656
Once we tell the event loop to run it will maintain control until it is suspended, the application errors out, has nothing left to do, or is explicitly stopped.
5757

58+
## Examples
59+
5860
Consider this very simple example:
5961

6062
```php
@@ -142,4 +144,135 @@ EventLoop::cancel($timeoutId);
142144
```
143145

144146
Obviously we could have simply used `fgets(STDIN)` synchronously in this example.
145-
We're just demonstrating that it's possible to move in and out of the event loop to mix synchronous tasks with non-blocking tasks as needed.
147+
We're just demonstrating that it's possible to move in and out of the event loop to mix synchronous tasks with non-blocking tasks as needed.
148+
149+
## Events
150+
151+
Event callbacks are registered using the methods on `Revolt\EventLoop` and are invoked using the following standardized parameter order:
152+
153+
| Method | Callback Signature |
154+
| ------------------------------------- | --------------------------------------- |
155+
| [`EventLoop::defer()`](/timers) | `function(string $callbackId)` |
156+
| [`EventLoop::delay()`](/timers) | `function(string $callbackId)` |
157+
| [`EventLoop::repeat()`](/timers) | `function(string $callbackId)` |
158+
| [`EventLoop::onReadable()`](/streams) | `function(string $callbackId, $stream)` |
159+
| [`EventLoop::onWritable()`](/streams) | `function(string $callbackId, $stream)` |
160+
| [`EventLoop::onSignal()`](/signals) | `function(string $callbackId, $signal)` |
161+
162+
### Pausing, Resuming and Canceling Callbacks
163+
164+
All event callbacks, regardless of type, can be temporarily disabled and enabled in addition to being cleared via `EventLoop::cancel()`.
165+
This allows for advanced capabilities such as disabling the acceptance of new socket clients in server applications when simultaneity limits are reached.
166+
In general, the performance characteristics of event callback reuse via `enable()`/`disable()` are favorable by comparison to repeatedly canceling and re-registering callbacks.
167+
168+
#### Pausing Callbacks
169+
170+
A simple disable example:
171+
172+
```php
173+
<?php
174+
175+
use Revolt\EventLoop;
176+
177+
// Register a callback we'll disable
178+
$callbackIdToDisable = EventLoop::delay(1, function (): void {
179+
echo "I'll never execute in one second because: disable()\n";
180+
});
181+
182+
// Register a callback to perform the disable() operation
183+
EventLoop::delay(0.5, function () use ($callbackIdToDisable) {
184+
echo "Disabling callback: ", $callbackIdToDisable, "\n";
185+
EventLoop::disable($callbackIdToDisable);
186+
});
187+
188+
EventLoop::run();
189+
```
190+
191+
After our second event callback executes the event loop exits because there are no longer any enabled event callbacks registered to process.
192+
193+
#### Resuming Callbacks
194+
195+
`enable()` is the diametric analog of the `disable()` example demonstrated above:
196+
197+
```php
198+
<?php
199+
200+
use Revolt\EventLoop;
201+
202+
// Register a repeating timer callback
203+
$callbackId = EventLoop::repeat(1, function(): void {
204+
echo "tick\n";
205+
});
206+
207+
// Disable the callback
208+
EventLoop::disable($callbackId);
209+
210+
EventLoop::defer(function () use ($callbackId): void {
211+
// Immediately enable the callback when the event loop starts
212+
EventLoop::enable($callbackId);
213+
// Now that it's enabled we'll see tick output in our console every second.
214+
});
215+
216+
EventLoop::run();
217+
```
218+
219+
#### Cancelling Callbacks
220+
221+
It's important to *always* cancel persistent event callbacks once you're finished with them, or you'll create memory leaks in your application.
222+
This functionality works in exactly the same way as the above `enable` / `disable` examples:
223+
224+
```php
225+
<?php
226+
227+
use Revolt\EventLoop;
228+
229+
$callbackId = EventLoop::repeat(1, function (): void {
230+
echo "tick\n";
231+
});
232+
233+
// Cancel $callbackId in five seconds and exit the event loop
234+
EventLoop::delay(5, function () use ($callbackId): void {
235+
EventLoop::cancel($callbackId);
236+
});
237+
238+
EventLoop::run();
239+
```
240+
241+
#### Cancellation Safety
242+
243+
It is always safe to cancel a callback from within itself. For example:
244+
245+
```php
246+
<?php
247+
248+
use Revolt\EventLoop;
249+
250+
$increment = 0;
251+
252+
EventLoop::repeat(0.1, function ($callbackId) use (&$increment): void {
253+
echo "tick\n";
254+
if (++$increment >= 3) {
255+
EventLoop::cancel($callbackId); // <-- cancel myself!
256+
}
257+
});
258+
259+
EventLoop::run();
260+
```
261+
262+
It is also always safe to cancel a callback from multiple places. A double-cancel will simply be ignored.
263+
264+
### Referencing Callbacks
265+
266+
Callbacks can either be referenced or unreferenced. An unreferenced callback doesn't keep the event loop alive.
267+
All callbacks are referenced by default.
268+
269+
One example to use unreferenced callbacks is when using signal callbacks.
270+
Generally, if all callbacks are gone and only the signal callback still exists, you want to exit the event loop unless you're not actively waiting for that event to happen.
271+
272+
#### Referencing Callbacks
273+
274+
`reference()` marks a callback as referenced. Takes the `$callbackId` as first and only argument.
275+
276+
#### Unreferencing Callbacks
277+
278+
`unreference()` marks a callback as unreferenced. Takes the `$callbackId` as first and only argument.

layouts/base.html

+9-9
Original file line numberDiff line numberDiff line change
@@ -58,17 +58,17 @@
5858
<a href="/fundamentals">Fundamentals</a>
5959
</li>
6060

61-
<!-- <li>-->
62-
<!-- <a href="/#timers">Timers</a>-->
63-
<!-- </li>-->
61+
<li>
62+
<a href="/timers">Timers</a>
63+
</li>
6464

65-
<!-- <li>-->
66-
<!-- <a href="/#timers">Streams</a>-->
67-
<!-- </li>-->
65+
<li>
66+
<a href="/streams">Streams</a>
67+
</li>
6868

69-
<!-- <li>-->
70-
<!-- <a href="/#timers">Signals</a>-->
71-
<!-- </li>-->
69+
<li>
70+
<a href="/signals">Signals</a>
71+
</li>
7272

7373
<!-- <li>-->
7474
<!-- <a href="/#timers">Fibers</a>-->

signals.md

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
---
2+
title: Signals
3+
permalink: /signals
4+
layout: base
5+
---
6+
# Signals
7+
8+
Signals are [standardized messages in Unix-like operating systems](https://en.wikipedia.org/wiki/Signal_(IPC)).
9+
10+
## Signal Callbacks
11+
12+
`EventLoop::onSignal()` can be used to react to signals sent to the process.
13+
14+
```php
15+
<?php
16+
17+
use Revolt\EventLoop;
18+
19+
// Let's tick off output once per second, so we can see activity.
20+
EventLoop::repeat(1, function (): void {
21+
echo "tick: ", date('c'), "\n";
22+
});
23+
24+
// What to do when a SIGINT signal is received
25+
EventLoop::onSignal(SIGINT, function (): void {
26+
echo "Caught SIGINT! exiting ...\n";
27+
exit;
28+
});
29+
30+
EventLoop::run();
31+
```
32+
33+
As should be clear from the [fundamentals](/fundamentals), signal callbacks may be enabled, disabled and canceled like any other event callback.
34+
35+
Generally, if all callbacks are gone and only the signal callback still exists, you want to exit the event loop unless you're not actively waiting for that event to happen.
36+
37+
### Signal Number Availability
38+
39+
`ext-uv` exposes `UV::SIG*` constants for watchable signals. Applications using the `EventDriver` will need to manually
40+
specify the [appropriate integer signal numbers](https://en.wikipedia.org/wiki/Signal_(IPC)#Default_action) when registering signal callbacks or rely on `ext-pcntl`.

streams.md

+101
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
---
2+
title: Streams
3+
permalink: /streams
4+
layout: base
5+
---
6+
# Streams
7+
8+
Stream callbacks are how we know when we can read and write to sockets and other streams. These events are how we're able
9+
to actually create things like HTTP servers and asynchronous database libraries using the event loop. As such, stream IO
10+
callbacks form the backbone of any useful non-blocking, concurrent application.
11+
12+
There are two types of IO callbacks:
13+
14+
- Readability callbacks
15+
- Writability callbacks
16+
17+
### Readability Callbacks
18+
19+
{:.note}
20+
> This is an advanced low-level API. Most users should use a stream abstraction instead.
21+
22+
Callbacks registered via `EventLoop::onReadable()` are invoked in the following situations:
23+
24+
- When data is available to read on the stream under observation
25+
- When the stream is at EOF (for sockets, this means the connection is broken)
26+
27+
A common usage pattern for reacting to readable data looks something like this example:
28+
29+
```php
30+
<?php
31+
32+
use Revolt\EventLoop;
33+
34+
const IO_GRANULARITY = 32768;
35+
36+
function isStreamDead($socket): bool {
37+
return !is_resource($socket) || @feof($socket);
38+
}
39+
40+
EventLoop::onReadable($socket, function ($callbackId, $socket) {
41+
$socketId = (int) $socket;
42+
$newData = @fread($socket, IO_GRANULARITY);
43+
if ($newData != "") {
44+
// There was actually data and not an EOF notification. Let's consume it!
45+
parseIncrementalData($socketId, $newData);
46+
} elseif (isStreamDead($socket)) {
47+
EventLoop::cancel($callbackId);
48+
}
49+
});
50+
51+
EventLoop::run();
52+
```
53+
54+
In the above example we've done a few very simple things:
55+
56+
- Register a readability callback for a socket that will be triggered when there is data available to read.
57+
- When we read data from the stream in our callback we pass that to a stateful parser that does something
58+
domain-specific when certain conditions are met.
59+
- If the `fread()` call indicates that the socket connection is dead we clean up any resources we've allocated for the
60+
storage of this stream. This process should always include calling `EventLoop::cancel()` on any event loop callbacks we
61+
registered in relation to the stream.
62+
63+
> You should always read a multiple of the configured chunk size (default: 8192), otherwise your code might not work as expected with loop backends other than `stream_select()`, see [amphp/amp#65](https://github.com/amphp/amp/issues/65) for more information.
64+
65+
### Writabilty Callbacks
66+
67+
{:.note}
68+
> This is an advanced low-level API. Most users should use a stream abstraction instead.
69+
70+
Callbacks registered via `EventLoop::onWritable()` are invoked in the following situations:
71+
72+
- There's enough free space in the buffer to accept new data to be written.
73+
- Streams are essentially *"always"* writable. The only time they aren't is when their respective write buffers are
74+
full.
75+
76+
A common usage pattern for reacting to writability involves initializing a writability callback without enabling it when
77+
a client first connects to a server. Once incomplete writes occur we're then able to "unpause" the write callback
78+
using `EventLoop::enable()` until data is fully sent without having to create and cancel new callbacks on the same
79+
stream multiple times.
80+
81+
Because streams are essentially *"always"* writable you should only enable writability callbacks while you have data to
82+
send. If you leave these callbacks enabled when your application doesn't have anything to write the callback will trigger
83+
endlessly until disabled or canceled. This will max out your CPU. If you're seeing inexplicably high CPU usage in your
84+
application it's a good bet you've got a writability callback that you failed to disable or cancel after you were
85+
finished with it.
86+
87+
A standard pattern in this area is to initialize writability callbacks in a disabled state before subsequently enabling
88+
them at a later time as shown here:
89+
90+
```php
91+
<?php
92+
93+
use Revolt\EventLoop;
94+
95+
$callbackId = EventLoop::onWritable(STDOUT, function (): void {});
96+
EventLoop::disable($callbackId);
97+
// ...
98+
EventLoop::enable($callbackId);
99+
// ...
100+
EventLoop::disable($callbackId);
101+
```

0 commit comments

Comments
 (0)