Skip to content

Commit

Permalink
Merge branch 'master' into development
Browse files Browse the repository at this point in the history
  • Loading branch information
trowski committed Jun 27, 2015
2 parents 286010c + cdd6f89 commit a6446f8
Show file tree
Hide file tree
Showing 15 changed files with 452 additions and 310 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,13 @@

### v0.5.3

- New Features
- Added `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 `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 `SelectLoop` to avoid timer drift.

---

Expand Down
42 changes: 23 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,10 @@
Icicle uses [Coroutines](#coroutines) built with [Promises](#promises) to facilitate writing asynchronous code using techniques normally used to write synchronous code, such as returning values and throwing exceptions, instead of using nested callbacks typically found in asynchronous code.

[![@icicleio on Twitter](https://img.shields.io/badge/twitter-%40icicleio-5189c7.svg?style=flat-square)](https://twitter.com/icicleio)
[![Build Status](https://img.shields.io/travis/icicleio/Icicle/master.svg?style=flat-square)](https://travis-ci.org/icicleio/Icicle)
[![Coverage Status](https://img.shields.io/coveralls/icicleio/Icicle.svg?style=flat-square)](https://coveralls.io/r/icicleio/Icicle)
[![Semantic Version](https://img.shields.io/github/release/icicleio/Icicle.svg?style=flat-square)](http://semver.org)
[![Apache 2 License](https://img.shields.io/packagist/l/icicleio/Icicle.svg?style=flat-square)](LICENSE)

[![Join the chat at https://gitter.im/icicleio/Icicle](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/icicleio/Icicle)
[![Build Status](https://img.shields.io/travis/icicleio/icicle/master.svg?style=flat-square)](https://travis-ci.org/icicleio/icicle)
[![Coverage Status](https://img.shields.io/coveralls/icicleio/icicle.svg?style=flat-square)](https://coveralls.io/r/icicleio/icicle)
[![Semantic Version](https://img.shields.io/github/release/icicleio/icicle.svg?style=flat-square)](http://semver.org)
[![Apache 2 License](https://img.shields.io/packagist/l/icicleio/icicle.svg?style=flat-square)](LICENSE)

#### Library Components

Expand All @@ -22,9 +20,9 @@ Icicle uses [Coroutines](#coroutines) built with [Promises](#promises) to facili

#### Available Components

- [HTTP](https://github.com/icicleio/Http): Asynchronous HTTP server and client (under development).
- [DNS](https://github.com/icicleio/Dns): Asynchronous DNS resolver and connector.
- [React Adapter](https://github.com/icicleio/ReactAdapter): Adapts the event loop and promises of Icicle to interfaces compatible with components built for React.
- [HTTP](https://github.com/icicleio/http): Asynchronous HTTP server and client (under development).
- [DNS](https://github.com/icicleio/dns): Asynchronous DNS resolver and connector.
- [React Adapter](https://github.com/icicleio/react-adaptor): Adapts the event loop and promises of Icicle to interfaces compatible with components built for React.

##### Requirements

Expand Down Expand Up @@ -58,9 +56,9 @@ You can also manually edit `composer.json` to add Icicle as a project requiremen
- [event extension](http://pecl.php.net/package/event): Allows for the most performant event loop implementation.
- [libevent extension](http://pecl.php.net/package/libevent): Similar to the event extension, it allows for a more performant event loop implementation.

### Example
#### Example

The example below uses the [HTTP component](https://github.com/icicleio/Http) (under development) to create a simple HTTP server that responds with `Hello, world!` to every request.
The example below uses the [HTTP component](https://github.com/icicleio/http) (under development) to create a simple HTTP server that responds with `Hello, world!` to every request.

```php
#!/usr/bin/env php
Expand All @@ -86,9 +84,15 @@ echo "Server running at http://127.0.0.1:8080\n";
Loop\run();
```

#### Documentation and Support

- [Full API Documentation](https://github.com/icicleio/icicle/wiki)
- [Official Twitter](https://twitter.com/icicleio)
- [Gitter Chat](https://gitter.im/icicleio/icicle)

## Promises

**[Promise API documentation](https://github.com/icicleio/Icicle/wiki/Promises)**
**[Promise API documentation](https://github.com/icicleio/icicle/wiki/Promises)**

Icicle implements promises based on the [Promises/A+](http://promisesaplus.com) specification, adding support for cancellation.

Expand All @@ -102,7 +106,7 @@ The `Icicle\Promise\PromiseInterface::then(callable $onFulfilled = null, callabl

The `Icicle\Promise\PromiseInterface::done(callable $onFulfilled = null, callable $onRejected = null)` method registers callbacks that should either consume promised values or handle errors. No value is returned from `done()`. Values returned by callbacks registered using `done()` are ignored and exceptions thrown from callbacks are re-thrown in an uncatchable way.

*[More on using callbacks to interact with promises...](https://github.com/icicleio/Icicle/wiki/Promises#interacting-with-promises)*
*[More on using callbacks to interact with promises...](https://github.com/icicleio/icicle/wiki/Promises#interacting-with-promises)*

```php
use Icicle\Dns\Executor\Executor;
Expand Down Expand Up @@ -140,7 +144,7 @@ The example above uses the [DNS component](https://github.com/icicleio/Dns) to r
- If `$promise1` is fulfilled, the callback function registered in the call to `$promise1->then()` is executed, using the fulfillment value of `$promise1` as the argument to the function. The callback function then returns the promise from `connect()`. The resolution of `$promise2` will then be determined by the resolution of this returned promise (`$promise2` will adopt the state of the promise returned by `connect()`).
- If `$promise1` is rejected, `$promise2` is rejected since no `$onRejected` callback was registered in the call to `$promise1->then()`

*[More on promise resolution and propagation...](https://github.com/icicleio/Icicle/wiki/Promises#resolution-and-propagation)*
*[More on promise resolution and propagation...](https://github.com/icicleio/icicle/wiki/Promises#resolution-and-propagation)*

##### Brief overview of promise API features

Expand All @@ -153,15 +157,15 @@ The example above uses the [DNS component](https://github.com/icicleio/Dns) to r

## Coroutines

**[Coroutine API documentation](https://github.com/icicleio/Icicle/wiki/Coroutines)**
**[Coroutine API documentation](https://github.com/icicleio/icicle/wiki/Coroutines)**

Coroutines are interruptible functions implemented using [Generators](http://www.php.net/manual/en/language.generators.overview.php). A `Generator` usually uses the `yield` keyword to yield a value from a set to implement an iterator. Coroutines use the `yield` keyword to define interruption points. When a coroutine yields a value, execution of the coroutine is temporarily interrupted, allowing other tasks to be run, such as I/O, timers, or other coroutines.

When a coroutine yields a [promise](#promises), execution of the coroutine is interrupted until the promise is resolved. If the promise is fulfilled with a value, the yield statement that yielded the promise will take on the resolved value. For example, `$value = (yield Icicle\Promise\Promise::resolve(2.718));` will set `$value` to `2.718` when execution of the coroutine is resumed. If the promise is rejected, the exception used to reject the promise will be thrown into the function at the yield statement. For example, `yield Icicle\Promise\Promise::reject(new Exception());` would behave identically to replacing the yield statement with `throw new Exception();`.

Note that **no callbacks need to be registered** with the promises yielded in a coroutine and **errors are reported using thrown exceptions**, which will bubble up to the calling context if uncaught in the same way exceptions bubble up in synchronous code.

The example below creates an `Icicle\Coroutine\Coroutine` instance from a function returning a `Generator`. (`Icicle\Dns\Connector\Connector` in the [DNS component](//github.com/icicleio/Dns) uses a coroutine structured similarly to the one below, except it attempts to connect to other IPs returned from the resolver if the first one fails.)
The example below creates an `Icicle\Coroutine\Coroutine` instance from a function returning a `Generator`. (`Icicle\Dns\Connector\Connector` in the [DNS component](//github.com/icicleio/dns) uses a coroutine structured similarly to the one below, except it attempts to connect to other IPs returned from the resolver if the first one fails.)

```php
use Icicle\Coroutine\Coroutine;
Expand Down Expand Up @@ -199,7 +203,7 @@ An `Icicle\Coroutine\Coroutine` object is also a [promise](#promises), implement

## Loop

**[Loop API documentation](https://github.com/icicleio/Icicle/wiki/Loop)**
**[Loop API documentation](https://github.com/icicleio/icicle/wiki/Loop)**

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.

Expand Down Expand Up @@ -241,7 +245,7 @@ Second.

## Streams

**[Streams API documentation](https://github.com/icicleio/Icicle/wiki/Streams)**
**[Streams API documentation](https://github.com/icicleio/icicle/wiki/Streams)**

Streams represent a common promise-based API that may be implemented by classes that read or write sequences of binary data to facilitate interoperability. The stream component defines three interfaces, one of which should be used by all streams.

Expand All @@ -251,7 +255,7 @@ Streams represent a common promise-based API that may be implemented by classes

## Sockets

**[Sockets API documentation](https://github.com/icicleio/Icicle/wiki/Sockets)**
**[Sockets API documentation](https://github.com/icicleio/icicle/wiki/Sockets)**

The socket component implements network sockets as promise-based streams, server, and datagram. Creating a server and accepting connections is very simple, requiring only a few lines of code.

Expand Down
10 changes: 4 additions & 6 deletions examples/echo.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,12 @@
echo "Echo server running on {$server->getAddress()}:{$server->getPort()}\n";

while ($server->isOpen()) {
try {
$coroutine = new Coroutine($generator(yield $server->accept()));
} catch (Exception $e) {
echo "Error accepting client: {$e->getMessage()}\n";
}
$coroutine = new Coroutine($generator(yield $server->accept()));
}
};

$coroutine = new Coroutine($generator((new ServerFactory())->create('127.0.0.1', 60000)));
$coroutine = new Coroutine($generator(
(new ServerFactory())->create('127.0.0.1', 60000)
));

Loop\run();
7 changes: 1 addition & 6 deletions examples/http.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,18 +28,13 @@
$data .= $message;

yield $client->write($data);

} finally {
$client->close();
}
};

while ($server->isOpen()) {
try {
$coroutine = new Coroutine($generator(yield $server->accept()));
} catch (Exception $exception) {
echo "Error: Could not accept client: {$exception->getMessage()}";
}
$coroutine = new Coroutine($generator(yield $server->accept()));
}
};

Expand Down
18 changes: 9 additions & 9 deletions src/Coroutine/Coroutine.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@ class Coroutine extends Promise implements CoroutineInterface
* @var bool
*/
private $paused = false;

/**
* @var bool
*/
private $initial = true;

/**
* @param \Generator $generator
Expand All @@ -58,19 +63,14 @@ function ($resolve, $reject) {
* @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) {
static $initial = true;
if (!$this->isPending()) { // Coroutine may have been cancelled.
return;
}

if ($this->paused) { // If paused, mark coroutine as ready to resume.
$this->ready = true;
return;
}

try {
if ($initial) { // Get result of first yield statement.
$initial = false;
if ($this->initial) { // Get result of first yield statement.
$this->initial = false;
$this->current = $this->generator->current();
} elseif (null !== $exception) { // Throw exception at current execution point.
$this->current = $this->generator->throw($exception);
Expand Down Expand Up @@ -127,7 +127,7 @@ function (Exception $exception) {
}

/**
* The garbage collector does not automatically detect the deep circular references that can be
* The garbage collector does not automatically detect (at least not quickly) the circular references that can be
* created, so explicitly setting these parameters to null is necessary for proper freeing of memory.
*/
private function close()
Expand All @@ -153,7 +153,7 @@ public function pause()
*/
public function resume()
{
if ($this->isPending() && $this->isPaused()) {
if ($this->isPending() && $this->paused) {
$this->paused = false;

if ($this->ready) {
Expand Down
25 changes: 14 additions & 11 deletions src/Loop/Events/Immediate.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,12 @@ class Immediate implements ImmediateInterface
* @var callable
*/
private $callback;


/**
* @var mixed[]|null
*/
private $args;

/**
* @param \Icicle\Loop\Manager\ImmediateManagerInterface $manager
* @param callable $callback Function called when the interval expires.
Expand All @@ -23,23 +28,21 @@ class Immediate implements ImmediateInterface
public function __construct(ImmediateManagerInterface $manager, callable $callback, array $args = null)
{
$this->manager = $manager;

if (empty($args)) {
$this->callback = $callback;
} else {
$this->callback = function () use ($callback, $args) {
call_user_func_array($callback, $args);
};
}
$this->callback = $callback;
$this->args = $args;
}

/**
* {@inheritdoc}
*/
public function call()
{
$callback = $this->callback;
$callback();
if (empty($this->args)) {
$callback = $this->callback;
$callback();
} else {
call_user_func_array($this->callback, $this->args);
}
}

/**
Expand Down
23 changes: 13 additions & 10 deletions src/Loop/Events/Timer.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ class Timer implements TimerInterface
* @var callable
*/
private $callback;

/**
* @var mixed[]|null
*/
private $args;

/**
* Number of seconds until the timer is called.
Expand Down Expand Up @@ -50,14 +55,8 @@ public function __construct(
$this->manager = $manager;
$this->interval = (float) $interval;
$this->periodic = (bool) $periodic;

if (empty($args)) {
$this->callback = $callback;
} else {
$this->callback = function () use ($callback, $args) {
call_user_func_array($callback, $args);
};
}
$this->callback = $callback;
$this->args = $args;

if (self::MIN_INTERVAL > $this->interval) {
$this->interval = self::MIN_INTERVAL;
Expand All @@ -69,8 +68,12 @@ public function __construct(
*/
public function call()
{
$callback = $this->callback;
$callback();
if (empty($this->args)) {
$callback = $this->callback;
$callback();
} else {
call_user_func_array($this->callback, $this->args);
}
}

/**
Expand Down
9 changes: 4 additions & 5 deletions src/Loop/Manager/Select/TimerManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,7 @@ public function start(TimerInterface $timer)
*/
public function stop(TimerInterface $timer)
{
if ($this->timers->contains($timer)) {
$this->timers->detach($timer);
}
$this->timers->detach($timer);
}

/**
Expand Down Expand Up @@ -150,6 +148,7 @@ public function getInterval()
public function tick()
{
$count = 0;
$time = microtime(true);

while (!$this->queue->isEmpty()) {
list($timer, $timeout) = $this->queue->top();
Expand All @@ -159,15 +158,15 @@ public function tick()
continue;
}

if ($this->timers[$timer] > microtime(true)) { // Timer at top of queue has not expired.
if ($this->timers[$timer] > $time) { // Timer at top of queue has not expired.
return $count;
}

// Remove and execute timer. Replace timer if persistent.
$this->queue->extract();

if ($timer->isPeriodic()) {
$timeout = microtime(true) + $timer->getInterval();
$timeout = $time + $timer->getInterval();
$this->queue->insert([$timer, $timeout], -$timeout);
$this->timers[$timer] = $timeout;
} else {
Expand Down

0 comments on commit a6446f8

Please sign in to comment.