Skip to content

Commit

Permalink
Merge branch 'multi-loop' into v0.8.x
Browse files Browse the repository at this point in the history
  • Loading branch information
trowski committed Aug 13, 2015
2 parents 1ee3e9b + fb60c2b commit c8429e3
Show file tree
Hide file tree
Showing 73 changed files with 2,161 additions and 1,863 deletions.
30 changes: 20 additions & 10 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,28 +1,36 @@
# Changelog

### v0.8.0 / v1.0.0-beta3

- New Features
- The default event loop can be swapped during execution. Normally this is not recommended and will break a program, but it can be useful in certain circumstances (forking, threading).
- Added the function `Icicle\Loop\with()` that accepts a function that is run in a separate loop from the default event loop (a specific loop instance can be provided to the function). The default loop is blocked while running the loop.
### v0.8.0

- Changes
- **There is no longer a default event loop.** Most of the `Icicle\Loop\*` functions have been removed. The event loop must be passed as the first argument of the constructor to create promises and coroutines.
- The coroutine instance can now be accessed from within the coroutine using the `yield` keyword without any expression after `yield`. For example, `$coroutine = yield;` will set `$coroutine` to the coroutine instance. Useful for retrieving the event loop used to run the coroutine.
- The promise instance is now passed as a third argument to the promise resolver function.
- The cancellation callable is no longer passed to the `Icicle\Promise\Promise` constructor, it should be returned from the resolver function passed to the constructor. This change was made to avoid the need to create reference variables to share values between functions. Instead values can just be used in the cancellation function returned from the resolver. The resolver function must return a `callable` or `null`.
- Cancelling a promise is now an asynchronous task. Calling `Icicle\Promise\Promise::cancel()` does not immediately call the cancellation method (if given), it is called later (like a function registered with `then()`).
- `Icicle/Promise/PromiseInterface` now includes an `isCancelled()` method. When a promise is cancelled, this method will return true once the promise has been cancelled. Note that if a child promise is rejected due to an `$onRejected` callable throwing after cancelling the parent promise, `isCancelled()` of the child promise will return false because the promise was not cancelled, it was rejected from the `$onRejected` callback.
- Modifications to `Icicle\Promise\PromiseInterface`:
- Added an `isCancelled()` method. When a promise is cancelled, this method will return true. Note that if a child promise is rejected due to an `$onRejected` callable throwing after cancelling the parent promise, `isCancelled()` of the child promise will return false because the promise was not cancelled, it was rejected from the `$onRejected` callback.
- Added a `wait()` method that synchronously waits for the promise to be resolved. This method generally should not be used in a running event loop, but rather is designed for integrating Icicle into a synchronous environment.
- Added a `getLoop()` method to fetch the `Icicle\Loop\LoopInterface` associated with the promise.
- Removed the `getResult()` method. Use `wait()` instead to synchronously get the result of a promise.
- The function `Icicle\Promise\wait()` function has been removed in favor of the `wait()` method on `PromiseInterface` above.
- Modifications to `Icicle\Loop\LoopInterface`:
- The methods `queue()`, `timer()`, and `immediate()` have been modified to be variadic functions so arguments do not have to be passed as an array, but rather as additional arguments.
- `timer()` was changed to remove the boolean periodic argument. Periodic timers can be created using the new `periodic()` method.
- `Icicle\Loop\Events\SocketEventInterface` and `Icicle\Loop\Events\SignalInterface` now have a `setCallback()` method to set the callback executed when the event is triggered.
- Added a `getLoop()` method to all loop event interfaces.

- Bug Fixes
- Fixed issue where `Icicle\Loop\SelectLoop` would not dispatch a signal while blocking. The issue was fixed by adding a periodic timer that checks for signals that may have arrived. The interval of this timer can be set with `Icicle\Loop\SelectLoop::signalInterval()`.

---

### v0.7.1 / v1.0.0-beta2
### v0.7.1

- Modified `Icicle\Promise\Promise` for better performance. The modified implementation eliminates the creation of one closure and only creates a queue of callbacks if more than one callback is registered to be invoked on fulfillment or rejection. No changes were made to functionality.

---

### v0.7.0 / v1.0.0-beta1
### v0.7.0

- Changes
- Moved Stream and Socket components to separate repositories: [icicleio/stream](https://github.com/icicleio/stream) and [icicleio/socket](https://github.com/icicleio/socket). No API changes were made in these components from v0.6.0. If your project depends on these components, just add them as a requirement with composer.
Expand All @@ -48,8 +56,10 @@

- New Features
- Added `Icicle\Promise\wait()` function that can be used to synchronously wait for a promise to be resolved. The fulfillment value is returned or the rejection reason is thrown from the function. This function can be used to integrate Icicle into a synchronous environment, but generally should not be used in an active event loop.

- Changes
- Various performance improvements when executing scheduled callbacks, executing promise callbacks, and checking for coroutine completion or cancellation.

- Bug Fixes
- Added check in `Icicle\Socket\Datagram\Datagram::send()` on `stream_socket_sendto()` sending 0 bytes if the data was not immediately sent to prevent an infinite loop if the datagram is unexpectedly closed while waiting to send data.
- Changed timer execution in `Icicle\Loop\SelectLoop` to avoid timer drift.
Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@
- Please submit any [issues](issues) or [pull requests](pulls) through GitHub.
- All code must be [PSR-1](http://www.php-fig.org/psr/psr-1/) and [PSR-2](http://www.php-fig.org/psr/psr-2/) compliant.
- Please see the [git-flow cheatsheet](http://danielkummer.github.com/git-flow-cheatsheet/) for a guide on how pull requests should be structured.
- All pull requests for features or bugfixes should be made to the branch with the lowest version number possible. Changes will then be merged up to higher versions.
- Pull requests for features and bug fixes should be made to the lowest possible version branch that contains the bug or can be modified without backwards compatibility concerns. Changes will then be merged up to higher version branches.
- Please ensure that all tests pass before submitting a pull request. If you make changes to the code, please add, update, or remove unit tests as needed.
- If you'd like to implement a significant new feature, please contact us beforehand to avoid any duplication of effort.
49 changes: 30 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@ Icicle uses [Coroutines](#coroutines) built with [Promises](#promises) to facili
[![Apache 2 License](https://img.shields.io/packagist/l/icicleio/icicle.svg?style=flat-square)](LICENSE)
[![@icicleio on Twitter](https://img.shields.io/badge/twitter-%40icicleio-5189c7.svg?style=flat-square)](https://twitter.com/icicleio)

**Note to php[architect] readers:** Some changes have been made since the article was written. The most significant is that the `Icicle\Loop\Loop` facade class no longer exists and has been replaced by a set of functions in the `Icicle\Loop` namespace. For example, `Icicle\Loop\Loop::run()` has become `Icicle\Loop\run()`. Some methods that returned promises have been changed to be coroutines, returning `Generator` instances that can made into a promise by simply wrapping the method call with `new Icicle\Coroutine\Coroutine(/* ... */)`. This change was made to support `yield from` in PHP 7. See the [changelog](CHANGELOG.md) for more details on the recent changes.

#### Library Components

- [Coroutines](#coroutines): Interruptible functions for building asynchronous code using synchronous coding patterns and error handling.
Expand Down Expand Up @@ -73,17 +71,24 @@ use Icicle\Http\Message\Response;
use Icicle\Http\Server\Server;
use Icicle\Loop;

$server = new Server(function (RequestInterface $request) {
$response = new Response(200);
$loop = Loop\create();

$server = new Server($loop, function (RequestInterface $request) {
// Create a plain-text response.
$response = new Response(200, ['Content-Type', 'text/plain']);

// Write response text to message stream and end the stream.
yield $response->getBody()->end('Hello, world!');
yield $response->withHeader('Content-Type', 'text/plain');

// The final yield is like `return` in a coroutine.
yield $response;
});

$server->listen(8080);

echo "Server running at http://127.0.0.1:8080\n";

Loop\run();
$loop->run();
```

#### Documentation and Support
Expand Down Expand Up @@ -118,15 +123,17 @@ use Icicle\Loop;
use Icicle\Socket\Client\ClientInterface;
use Icicle\Socket\Client\Connector;

$loop = Loop\create();

$resolver = new Resolver(new Executor('8.8.8.8'));

// Method returning a Generator used to create a Coroutine (a type of promise)
$promise1 = new Coroutine($resolver->resolve('example.com'));
$promise1 = new Coroutine($loop, $resolver->resolve('example.com'));

$promise2 = $promise1->then(
function (array $ips) { // Called if $promise1 is fulfilled.
function (array $ips) use ($loop) { // Called if $promise1 is fulfilled.
$connector = new Connector();
return new Coroutine($connector->connect($ips[0], 80)); // Return another promise.
return new Coroutine($loop, $connector->connect($ips[0], 80)); // Return another promise.
// $promise2 will adopt the state of the promise returned above.
}
);
Expand All @@ -140,7 +147,7 @@ $promise2->done(
}
);

Loop\run();
$loop->run();
```

The example above uses the [DNS component](https://github.com/icicleio/Dns) to resolve the IP address for a domain, then connect to the resolved IP address. The `resolve()` method of `$resolver` and the `connect()` method of `$connector` both return promises. `$promise1` created by `resolve()` will either be fulfilled or rejected:
Expand Down Expand Up @@ -178,6 +185,8 @@ use Icicle\Dns\Resolver\Resolver;
use Icicle\Loop;
use Icicle\Socket\Client\Connector;

$loop = Loop\create();

$generator = function () {
try {
$resolver = new Resolver(new Executor('8.8.8.8'));
Expand All @@ -196,9 +205,9 @@ $generator = function () {
}
};

$coroutine = new Coroutine($generator());
$coroutine = new Coroutine($loop, $generator());

Loop\run();
$loop->run();
```

The example above does the same thing as the example in the section on [promises](#promises) above, but instead uses a coroutine to **structure asynchronous code like synchronous code**. Fulfillment values of promises are accessed through simple variable assignments and exceptions used to reject promises are caught using a try/catch block, rather than creating and registering callback functions to each promise.
Expand All @@ -211,29 +220,31 @@ An `Icicle\Coroutine\Coroutine` object is also a [promise](#promises), implement

The event loop schedules functions, runs timers, handles signals, and polls sockets for pending reads and available writes. There are several event loop implementations available depending on what PHP extensions are available. The `Icicle\Loop\SelectLoop` class uses only core PHP functions, so it will work on any PHP installation, but is not as performant as some of the other available implementations. All event loops implement `Icicle\Loop\LoopInterface` and provide the same features.

The event loop should be accessed via functions defined in the `Icicle\Loop` namespace. If a program requires a specific or custom event loop implementation, `Icicle\Loop\loop()` can be called with an instance of `Icicle\Loop\LoopInterface` before any other loop functions to use that instance as the event loop.
An event loop may be created using the `Icicle\Loop\create()` method. An event loop will be created based on the extensions installed.

The `Icicle\Loop\run()` function runs the event loop and will not return until the event loop is stopped or no events are pending in the loop.
The `Icicle\Loop\LoopInterface::run()` method runs the event loop and will not return until the event loop is stopped or no events are pending in the loop.

The following code demonstrates how timers can be created to execute functions after a number of seconds elapses using the `Icicle\Loop\timer()` function.
The following code demonstrates how timers can be created to execute functions after a number of seconds elapses using the `Icicle\Loop\LoopInterface::timer()` method.

```php
use Icicle\Loop;

Loop\timer(1, function () { // Executed after 1 second.
$loop = Loop\create();

$loop->timer(1, function () use ($loop) { // Executed after 1 second.
echo "First.\n";
Loop\timer(1.5, function () { // Executed after 1.5 seconds.
$loop->timer(1.5, function () { // Executed after 1.5 seconds.
echo "Second.\n";
});
echo "Third.\n";
Loop\timer(0.5, function () { // Executed after 0.5 seconds.
$loop->timer(0.5, function () { // Executed after 0.5 seconds.
echo "Fourth.\n";
});
echo "Fifth.\n";
});

echo "Starting event loop.\n";
Loop\run();
$loop->run();
```

The above code will output:
Expand Down
14 changes: 8 additions & 6 deletions examples/coroutine.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,26 +7,28 @@
use Icicle\Loop;
use Icicle\Promise;

$generator = function () {
$loop = Loop\create(); // Create an event loop.

$generator = function () use ($loop) {
try {
// Sets $start to the value returned by microtime() after approx. 1 second.
$start = (yield Promise\resolve(microtime(true))->delay(1));
$start = (yield Promise\resolve($loop, microtime(true))->delay(1));

echo "Sleep time: ", microtime(true) - $start, "\n";

// Throws the exception from the rejected promise into the coroutine.
yield Promise\reject(new Exception('Rejected promise'));
yield Promise\reject($loop, new Exception('Rejected promise'));
} catch (Exception $e) { // Catches promise rejection reason.
echo "Caught exception: ", $e->getMessage(), "\n";
}

yield Promise\resolve('Coroutine completed');
yield Promise\resolve($loop, 'Coroutine completed');
};

$coroutine = new Coroutine($generator());
$coroutine = new Coroutine($loop, $generator());

$coroutine->done(function ($data) {
echo $data, "\n";
});

Loop\run();
$loop->run();
9 changes: 6 additions & 3 deletions examples/promise.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@
require dirname(__DIR__) . '/vendor/autoload.php';

use Icicle\Loop;
use Icicle\Loop\LoopInterface;
use Icicle\Promise\Promise;

$promise = new Promise(function ($resolve, $reject) {
Loop\timer(1, function () use ($resolve) {
$loop = Loop\create(); // Create an event loop.

$promise = new Promise($loop, function (callable $resolve, callable $reject, Promise $promise) {
$promise->getLoop()->timer(1, function () use ($resolve) {
$resolve("Promise resolved");
});
});
Expand All @@ -22,4 +25,4 @@
echo $data;
});

Loop\run();
$loop->run();
22 changes: 14 additions & 8 deletions src/Coroutine/Coroutine.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

use Exception;
use Generator;
use Icicle\Loop;
use Icicle\Loop\LoopInterface;
use Icicle\Promise\Promise;
use Icicle\Promise\PromiseInterface;

Expand Down Expand Up @@ -58,19 +58,21 @@ class Coroutine extends Promise implements CoroutineInterface
private $initial = true;

/**
* @param \Icicle\Loop\LoopInterface $loop
* @param \Generator $generator
*/
public function __construct(Generator $generator)
public function __construct(LoopInterface $loop, Generator $generator)
{
$this->generator = $generator;

parent::__construct(
function (callable $resolve, callable $reject) {
$loop,
function (callable $resolve, callable $reject) use ($loop) {
/**
* @param mixed $value The value to send to the generator.
* @param \Exception|null $exception Exception object to be thrown into the generator if not null.
*/
$this->worker = function ($value = null, Exception $exception = null) use ($resolve, $reject) {
$this->worker = function ($value = null, Exception $exception = null) use ($resolve, $reject, $loop) {
if ($this->paused) { // If paused, mark coroutine as ready to resume.
$this->ready = true;
return;
Expand All @@ -93,13 +95,15 @@ function (callable $resolve, callable $reject) {
}

if ($this->current instanceof Generator) {
$this->current = new self($this->current);
$this->current = new self($loop, $this->current);
}

if ($this->current instanceof PromiseInterface) {
$this->current->done($this->worker, $this->capture);
} elseif (null === $this->current) {
$loop->queue($this->worker, $this); // Send $this if no value is yielded.
} else {
Loop\queue($this->worker, $this->current);
$loop->queue($this->worker, $this->current);
}
} catch (Exception $exception) {
$reject($exception);
Expand All @@ -116,7 +120,7 @@ function (callable $resolve, callable $reject) {
}
};

Loop\queue($this->worker);
$loop->queue($this->worker);

return function (Exception $exception) {
try {
Expand Down Expand Up @@ -168,8 +172,10 @@ public function resume()
if ($this->ready) {
if ($this->current instanceof PromiseInterface) {
$this->current->done($this->worker, $this->capture);
} elseif (null === $this->current) {
$this->getLoop()->queue($this->worker, $this->initial ? null : $this);
} else {
Loop\queue($this->worker, $this->current);
$this->getLoop()->queue($this->worker, $this->current);
}

$this->ready = false;
Expand Down
Loading

0 comments on commit c8429e3

Please sign in to comment.