diff --git a/.travis.yml b/.travis.yml index b756d3d..6f73ecf 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,8 +18,8 @@ cache: install: - composer install - - composer show + - composer show -t script: - - find -name "*.php" -not -path "./vendor/*" -print0 | xargs -n 1 -0 php -l - - $(php -r 'if (PHP_MAJOR_VERSION >= 7) echo "phpdbg -qrr"; else echo "php";') vendor/bin/phpunit --coverage-text --coverage-clover build/logs/clover.xml + - php vendor/bin/parallel-lint --exclude vendor . + - php vendor/bin/phpunit --coverage-text diff --git a/README.md b/README.md index 9b86b55..bc66f8c 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,13 @@ # Event Loop Interopability -The purpose of this proposal is to provide a common interface for event loop -implementations. This will allow libraries and components from different -vendors to operate in an event driven architecture, sharing a common event -loop. +The purpose of this specification is to provide a common interface for +event loop implementations. This allows libraries and components from +different vendors to operate in an event driven architecture, sharing a +common event loop. + +## Current Status + +This project is currently on hold and to be seen as failed for now. It might be reconsidered at a later point in time. The specification in its current state has been merged into [Amp](https://github.com/amphp/amp). Interoperability between [ReactPHP](https://github.com/reactphp/event-loop) and Amp will be solved via adapters instead of a common interface. [Icicle](https://github.com/icicleio/icicle) has been deprecated and parts of it been merged into Amp libraries. ## Why Bother? @@ -12,7 +16,7 @@ native to the execution environment. This allows package vendors to easily create asynchronous software that uses this native event loop. Although PHP is historically a synchronous programming environment, it is still possible to use asynchronous programming techniques. Using these techniques, package -vendors have created PHP event loop implementations that have seen success. +vendors have created event loop implementations that have seen success. However, as these event loop implementations are from package vendors, it is not yet possible to create event driven software components that are @@ -30,6 +34,14 @@ The functionality exposed by this interface should include the ability to: - Listen for signals - Defer the execution of callables +## Implementations + +You can find [available implementations on Packagist](https://packagist.org/providers/async-interop/event-loop-implementation). + +## Compatible Packages + +You can find [compatible packages on Packagist](https://packagist.org/packages/async-interop/event-loop/dependents). + ## Contributors * [Aaron Piotrowski](https://github.com/trowski) diff --git a/composer.json b/composer.json index f553628..b47d6e4 100644 --- a/composer.json +++ b/composer.json @@ -7,16 +7,18 @@ "php": ">=5.5.0" }, "require-dev": { - "phpunit/phpunit": "^4|^5" + "phpunit/phpunit": "^4|^5", + "jakub-onderka/php-parallel-lint": "^0.9.2", + "jakub-onderka/php-console-highlighter": "^0.3.2" }, "autoload": { "psr-4": { - "Interop\\Async\\": "src" + "AsyncInterop\\": "src" } }, "autoload-dev": { "psr-4": { - "Interop\\Async\\Loop\\Test\\": "test" + "AsyncInterop\\Loop\\Test\\": "test" } } } diff --git a/src/Loop.php b/src/Loop.php index ed4cec9..27ddf28 100644 --- a/src/Loop.php +++ b/src/Loop.php @@ -1,12 +1,17 @@ create(); if (!$driver instanceof Driver) { $type = is_object($driver) ? "an instance of " . get_class($driver) : gettype($driver); - throw new \LogicException("Loop driver factory returned {$type}, but must return an instance of Driver."); + throw new \Exception("Loop driver factory returned {$type}, but must return an instance of Driver."); } return $driver; @@ -109,6 +122,9 @@ public static function get() /** * Stop the event loop. * + * When an event loop is stopped, it continues with its current tick and exits the loop afterwards. Multiple calls + * to stop MUST be ignored and MUST NOT raise an exception. + * * @return void */ public static function stop() @@ -120,10 +136,17 @@ public static function stop() /** * Defer the execution of a callback. * - * @param callable(string $watcherId, mixed $data) $callback The callback to defer. - * @param mixed $data Arbitrary data given to the callback function as the $data parameter. + * The deferred callable MUST be executed before any other type of watcher in a tick. Order of enabling MUST be + * preserved when executing the callbacks. + * + * The created watcher MUST immediately be marked as enabled, but only be activated (i.e. callback can be called) + * right before the next tick. Callbacks of watchers MUST NOT be called in the tick they were enabled. * - * @return string An identifier that can be used to cancel, enable or disable the watcher. + * @param callable(string $watcherId, mixed $data) $callback The callback to defer. The `$watcherId` will be + * invalidated before the callback call. + * @param mixed $data Arbitrary data given to the callback function as the `$data` parameter. + * + * @return string An unique identifier that can be used to cancel, enable or disable the watcher. */ public static function defer(callable $callback, $data = null) { @@ -134,13 +157,18 @@ public static function defer(callable $callback, $data = null) /** * Delay the execution of a callback. * - * The delay is a minimum and approximate, accuracy is not guaranteed. + * The delay is a minimum and approximate, accuracy is not guaranteed. Order of calls MUST be determined by which + * timers expire first, but timers with the same expiration time MAY be executed in any order. * - * @param int $time The amount of time, in milliseconds, to delay the execution for. - * @param callable(string $watcherId, mixed $data) $callback The callback to delay. - * @param mixed $data Arbitrary data given to the callback function as the $data parameter. + * The created watcher MUST immediately be marked as enabled, but only be activated (i.e. callback can be called) + * right before the next tick. Callbacks of watchers MUST NOT be called in the tick they were enabled. * - * @return string An identifier that can be used to cancel, enable or disable the watcher. + * @param int $delay The amount of time, in milliseconds, to delay the execution for. + * @param callable(string $watcherId, mixed $data) $callback The callback to delay. The `$watcherId` will be + * invalidated before the callback call. + * @param mixed $data Arbitrary data given to the callback function as the `$data` parameter. + * + * @return string An unique identifier that can be used to cancel, enable or disable the watcher. */ public static function delay($time, callable $callback, $data = null) { @@ -151,14 +179,18 @@ public static function delay($time, callable $callback, $data = null) /** * Repeatedly execute a callback. * - * The interval between executions is a minimum and approximate, accuracy is not guaranteed. + * The interval between executions is a minimum and approximate, accuracy is not guaranteed. Order of calls MUST be + * determined by which timers expire first, but timers with the same expiration time MAY be executed in any order. * The first execution is scheduled after the first interval period. * + * The created watcher MUST immediately be marked as enabled, but only be activated (i.e. callback can be called) + * right before the next tick. Callbacks of watchers MUST NOT be called in the tick they were enabled. + * * @param int $interval The time interval, in milliseconds, to wait between executions. * @param callable(string $watcherId, mixed $data) $callback The callback to repeat. - * @param mixed $data Arbitrary data given to the callback function as the $data parameter. + * @param mixed $data Arbitrary data given to the callback function as the `$data` parameter. * - * @return string An identifier that can be used to cancel, enable or disable the watcher. + * @return string An unique identifier that can be used to cancel, enable or disable the watcher. */ public static function repeat($interval, callable $callback, $data = null) { @@ -167,13 +199,23 @@ public static function repeat($interval, callable $callback, $data = null) } /** - * Execute a callback when a stream resource becomes readable. + * Execute a callback when a stream resource becomes readable or is closed for reading. + * + * Warning: Closing resources locally, e.g. with `fclose`, might not invoke the callback. Be sure to `cancel` the + * watcher when closing the resource locally. Drivers MAY choose to notify the user if there are watchers on invalid + * resources, but are not required to, due to the high performance impact. Watchers on closed resources are + * therefore undefined behavior. + * + * Multiple watchers on the same stream MAY be executed in any order. + * + * The created watcher MUST immediately be marked as enabled, but only be activated (i.e. callback can be called) + * right before the next tick. Callbacks of watchers MUST NOT be called in the tick they were enabled. * * @param resource $stream The stream to monitor. * @param callable(string $watcherId, resource $stream, mixed $data) $callback The callback to execute. - * @param mixed $data Arbitrary data given to the callback function as the $data parameter. + * @param mixed $data Arbitrary data given to the callback function as the `$data` parameter. * - * @return string An identifier that can be used to cancel, enable or disable the watcher. + * @return string An unique identifier that can be used to cancel, enable or disable the watcher. */ public static function onReadable($stream, callable $callback, $data = null) { @@ -182,13 +224,23 @@ public static function onReadable($stream, callable $callback, $data = null) } /** - * Execute a callback when a stream resource becomes writable. + * Execute a callback when a stream resource becomes writable or is closed for writing. + * + * Warning: Closing resources locally, e.g. with `fclose`, might not invoke the callback. Be sure to `cancel` the + * watcher when closing the resource locally. Drivers MAY choose to notify the user if there are watchers on invalid + * resources, but are not required to, due to the high performance impact. Watchers on closed resources are + * therefore undefined behavior. + * + * Multiple watchers on the same stream MAY be executed in any order. + * + * The created watcher MUST immediately be marked as enabled, but only be activated (i.e. callback can be called) + * right before the next tick. Callbacks of watchers MUST NOT be called in the tick they were enabled. * * @param resource $stream The stream to monitor. * @param callable(string $watcherId, resource $stream, mixed $data) $callback The callback to execute. - * @param mixed $data Arbitrary data given to the callback function as the $data parameter. + * @param mixed $data Arbitrary data given to the callback function as the `$data` parameter. * - * @return string An identifier that can be used to cancel, enable or disable the watcher. + * @return string An unique identifier that can be used to cancel, enable or disable the watcher. */ public static function onWritable($stream, callable $callback, $data = null) { @@ -199,13 +251,22 @@ public static function onWritable($stream, callable $callback, $data = null) /** * Execute a callback when a signal is received. * + * Warning: Installing the same signal on different instances of this interface is deemed undefined behavior. + * Implementations MAY try to detect this, if possible, but are not required to. This is due to technical + * limitations of the signals being registered globally per process. + * + * Multiple watchers on the same signal MAY be executed in any order. + * + * The created watcher MUST immediately be marked as enabled, but only be activated (i.e. callback can be called) + * right before the next tick. Callbacks of watchers MUST NOT be called in the tick they were enabled. + * * @param int $signo The signal number to monitor. * @param callable(string $watcherId, int $signo, mixed $data) $callback The callback to execute. * @param mixed $data Arbitrary data given to the callback function as the $data parameter. * - * @return string An identifier that can be used to cancel, enable or disable the watcher. + * @return string An unique identifier that can be used to cancel, enable or disable the watcher. * - * @throws UnsupportedFeatureException Thrown if signal handling is not supported. + * @throws UnsupportedFeatureException If signal handling is not supported. */ public static function onSignal($signo, callable $callback, $data = null) { @@ -214,13 +275,16 @@ public static function onSignal($signo, callable $callback, $data = null) } /** - * Enable a watcher. + * Enable a watcher to be active starting in the next tick. + * + * Watchers MUST immediately be marked as enabled, but only be activated (i.e. callbacks can be called) right before + * the next tick. Callbacks of watchers MUST NOT be called in the tick they were enabled. * * @param string $watcherId The watcher identifier. * * @return void * - * @throws InvalidWatcherException Thrown if the watcher identifier is invalid. + * @throws InvalidWatcherException If the watcher identifier is invalid. */ public static function enable($watcherId) { @@ -229,13 +293,17 @@ public static function enable($watcherId) } /** - * Disable a watcher. + * Disable a watcher immediately. + * + * A watcher MUST be disabled immediately, e.g. if a defer watcher disables a later defer watcher, the second defer + * watcher isn't executed in this tick. + * + * Disabling a watcher MUST NOT invalidate the watcher. Calling this function MUST NOT fail, even if passed an + * invalid watcher. * * @param string $watcherId The watcher identifier. * * @return void - * - * @throws InvalidWatcherException If the watcher identifier is invalid. */ public static function disable($watcherId) { @@ -244,8 +312,10 @@ public static function disable($watcherId) } /** - * Cancel a watcher. This will detatch the event loop from all resources that are associated to the watcher. After - * this operation the watcher is permanently invalid. + * Cancel a watcher. + * + * This will detatch the event loop from all resources that are associated to the watcher. After this operation the + * watcher is permanently invalid. Calling this function MUST NOT fail, even if passed an invalid watcher. * * @param string $watcherId The watcher identifier. * @@ -294,52 +364,56 @@ public static function unreference($watcherId) } /** - * Stores information in the loop bound registry. This can be used to store loop bound information. Stored - * information is package private. Packages MUST NOT retrieve the stored state of other packages. + * Stores information in the loop bound registry. * - * Therefore packages SHOULD use the following prefix to keys: `vendor.package.` + * This can be used to store loop bound information. Stored information is package private. Packages MUST NOT + * retrieve the stored state of other packages. Packages MUST use the following prefix for keys: `vendor.package.` * - * @param string $key namespaced storage key - * @param mixed $value the value to be stored + * @param string $key The namespaced storage key. + * @param mixed $value The value to be stored. * * @return void */ - public static function storeState($key, $value) + public static function setState($key, $value) { $driver = self::$driver ?: self::get(); - $driver->storeState($key, $value); + $driver->setState($key, $value); } /** - * Fetches information stored bound to the loop. Stored information is package private. Packages MUST NOT retrieve - * the stored state of other packages. + * Gets information stored bound to the loop. * - * Therefore packages SHOULD use the following prefix to keys: `vendor.package.` + * Stored information is package private. Packages MUST NOT retrieve the stored state of other packages. Packages + * MUST use the following prefix for keys: `vendor.package.` * - * @param string $key namespaced storage key + * @param string $key The namespaced storage key. * - * @return mixed previously stored value or null if it doesn't exist + * @return mixed The previously stored value or `null` if it doesn't exist. */ - public static function fetchState($key) + public static function getState($key) { $driver = self::$driver ?: self::get(); - return $driver->fetchState($key); + return $driver->getState($key); } /** * Set a callback to be executed when an error occurs. * + * The callback receives the error as the first and only parameter. The return value of the callback gets ignored. + * If it can't handle the error, it MUST throw the error. Errors thrown by the callback or during its invocation + * MUST be thrown into the `run` loop and stop the driver. + * * Subsequent calls to this method will overwrite the previous handler. * - * @param callable(\Throwable|\Exception $error)|null $callback The callback to execute; null will clear the current - * handler. + * @param callable(\Throwable|\Exception $error)|null $callback The callback to execute. `null` will clear the + * current handler. * - * @return void + * @return callable(\Throwable|\Exception $error)|null The previous handler, `null` if there was none. */ public static function setErrorHandler(callable $callback = null) { $driver = self::$driver ?: self::get(); - $driver->setErrorHandler($callback); + return $driver->setErrorHandler($callback); } /** @@ -347,25 +421,25 @@ public static function setErrorHandler(callable $callback = null) * * The returned array MUST contain the following data describing the driver's currently registered watchers: * - * [ - * "defer" => ["enabled" => int, "disabled" => int], - * "delay" => ["enabled" => int, "disabled" => int], - * "repeat" => ["enabled" => int, "disabled" => int], - * "on_readable" => ["enabled" => int, "disabled" => int], - * "on_writable" => ["enabled" => int, "disabled" => int], - * "on_signal" => ["enabled" => int, "disabled" => int], - * "watchers" => ["referenced" => int, "unreferenced" => int], - * ]; - * - * Implementations MAY optionally add more information in the array but at minimum the above key => value format + * [ + * "defer" => ["enabled" => int, "disabled" => int], + * "delay" => ["enabled" => int, "disabled" => int], + * "repeat" => ["enabled" => int, "disabled" => int], + * "on_readable" => ["enabled" => int, "disabled" => int], + * "on_writable" => ["enabled" => int, "disabled" => int], + * "on_signal" => ["enabled" => int, "disabled" => int], + * "enabled_watchers" => ["referenced" => int, "unreferenced" => int], + * ]; + * + * Implementations MAY optionally add more information in the array but at minimum the above `key => value` format * MUST always be provided. * - * @return array + * @return array Statistics about the loop in the described format. */ - public static function info() + public static function getInfo() { $driver = self::$driver ?: self::get(); - return $driver->info(); + return $driver->getInfo(); } /** diff --git a/src/Loop/Driver.php b/src/Loop/Driver.php index f989152..669e296 100644 --- a/src/Loop/Driver.php +++ b/src/Loop/Driver.php @@ -1,11 +1,30 @@ registry[$key]); + } else { + $this->registry[$key] = $value; + } + } /** - * Fetches information stored bound to the loop. Stored information is package private. Packages MUST NOT retrieve - * the stored state of other packages. + * Gets information stored bound to the loop. * - * Therefore packages SHOULD use the following prefix to keys: `vendor.package.` + * Stored information is package private. Packages MUST NOT retrieve the stored state of other packages. Packages + * MUST use the following prefix for keys: `vendor.package.` * - * @param string $key Namespaced storage key. + * @param string $key The namespaced storage key. * - * @return mixed previously stored value or null if it doesn't exist + * @return mixed The previously stored value or `null` if it doesn't exist. */ - public function fetchState($key); + final public function getState($key) + { + return isset($this->registry[$key]) ? $this->registry[$key] : null; + } /** * Set a callback to be executed when an error occurs. * + * The callback receives the error as the first and only parameter. The return value of the callback gets ignored. + * If it can't handle the error, it MUST throw the error. Errors thrown by the callback or during its invocation + * MUST be thrown into the `run` loop and stop the driver. + * * Subsequent calls to this method will overwrite the previous handler. * - * @param callable(\Throwable|\Exception $error)|null $callback The callback to execute; null will clear the current - * handler. + * @param callable(\Throwable|\Exception $error)|null $callback The callback to execute. `null` will clear the + * current handler. * - * @return void + * @return callable(\Throwable|\Exception $error)|null The previous handler, `null` if there was none. */ - public function setErrorHandler(callable $callback = null); + abstract public function setErrorHandler(callable $callback = null); /** * Retrieve an associative array of information about the event loop driver. * * The returned array MUST contain the following data describing the driver's currently registered watchers: * - * [ - * "defer" => ["enabled" => int, "disabled" => int], - * "delay" => ["enabled" => int, "disabled" => int], - * "repeat" => ["enabled" => int, "disabled" => int], - * "on_readable" => ["enabled" => int, "disabled" => int], - * "on_writable" => ["enabled" => int, "disabled" => int], - * "on_signal" => ["enabled" => int, "disabled" => int], - * "watchers" => ["referenced" => int, "unreferenced" => int], - * ]; - * - * Implementations MAY optionally add more information in the array but at minimum the above key => value format + * [ + * "defer" => ["enabled" => int, "disabled" => int], + * "delay" => ["enabled" => int, "disabled" => int], + * "repeat" => ["enabled" => int, "disabled" => int], + * "on_readable" => ["enabled" => int, "disabled" => int], + * "on_writable" => ["enabled" => int, "disabled" => int], + * "on_signal" => ["enabled" => int, "disabled" => int], + * "enabled_watchers" => ["referenced" => int, "unreferenced" => int], + * ]; + * + * Implementations MAY optionally add more information in the array but at minimum the above `key => value` format * MUST always be provided. * - * @return array + * @return array Statistics about the loop in the described format. */ - public function info(); + abstract public function getInfo(); /** * Get the underlying loop handle. * - * Example: the uv_loop resource for libuv or the EvLoop object for libev or null for a native driver + * Example: the `uv_loop` resource for `libuv` or the `EvLoop` object for `libev` or `null` for a native driver. * - * Note: This function is *not* exposed in the Loop class; users shall access it directly on the respective loop + * Note: This function is *not* exposed in the `Loop` class. Users shall access it directly on the respective loop * instance. * - * @return null|object|resource The loop handle the event loop operates on. Null if there is none. + * @return null|object|resource The loop handle the event loop operates on. `null` if there is none. */ - public function getHandle(); + abstract public function getHandle(); } diff --git a/src/Loop/DriverFactory.php b/src/Loop/DriverFactory.php index 83ba560..bb4d992 100644 --- a/src/Loop/DriverFactory.php +++ b/src/Loop/DriverFactory.php @@ -1,7 +1,12 @@ watcherId = $watcherId; + + if ($message === null) { + $message = "An invalid watcher identifier has been used: '{$watcherId}'"; + } + + parent::__construct($message); + } + + /** + * @return string The watcher identifier. + */ + public function getWatcherId() + { + return $this->watcherId; + } } diff --git a/src/Loop/Registry.php b/src/Loop/Registry.php deleted file mode 100644 index 548806e..0000000 --- a/src/Loop/Registry.php +++ /dev/null @@ -1,49 +0,0 @@ -registry[$key]); - } else { - $this->registry[$key] = $value; - } - } - - /** - * Fetches information stored bound to the loop. Stored information is package private. Packages MUST NOT retrieve - * the stored state of other packages. - * - * Therefore packages SHOULD use the following prefix to keys: `vendor.package.` - * - * @param string $key namespaced storage key - * - * @return mixed previously stored value or null if it doesn't exist - */ - public function fetchState($key) - { - return isset($this->registry[$key]) ? $this->registry[$key] : null; - } -} diff --git a/src/Loop/UnsupportedFeatureException.php b/src/Loop/UnsupportedFeatureException.php index 03c8c9c..db2dcad 100644 --- a/src/Loop/UnsupportedFeatureException.php +++ b/src/Loop/UnsupportedFeatureException.php @@ -1,13 +1,13 @@ loop = $this->getMockForAbstractClass(Driver::class); + } + + /** @test */ + public function defaultsToNull() + { + $this->assertNull($this->loop->getState("foobar")); + } + + /** + * @test + * @dataProvider provideValues + */ + public function getsPreviouslySetValue($value) + { + $this->loop->setState("foobar", $value); + $this->assertSame($value, $this->loop->getState("foobar")); + } + + public function provideValues() + { + return [ + ["string"], + [42], + [1.001], + [true], + [false], + [null], + [new \StdClass], + ]; + } +} diff --git a/test/LoopTest.php b/test/LoopTest.php index 363ffb0..e5d80b4 100644 --- a/test/LoopTest.php +++ b/test/LoopTest.php @@ -1,8 +1,8 @@ registry = $this->getMockForTrait(Registry::class); - } - - /** @test */ - public function defaultsToNull() - { - $this->assertNull($this->registry->fetchState("foobar")); - } - - /** - * @test - * @dataProvider provideValues - */ - public function fetchesStoredValue($value) - { - $this->assertNull($this->registry->fetchState("foobar")); - $this->registry->storeState("foobar", $value); - - $this->assertSame($value, $this->registry->fetchState("foobar")); - } - - public function provideValues() - { - return [ - ["string"], - [42], - [1.001], - [true], - [false], - [null], - [new \StdClass], - ]; - } -}