Skip to content
This repository has been archived by the owner on Jan 29, 2020. It is now read-only.

Separate Factory for Server Instance #40

Merged
merged 25 commits into from
Nov 13, 2018
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
697e5aa
Initial commit to delegate creation of the server instance to a separ…
gsteel Nov 3, 2018
1659958
Add test case for new HttpServerFactory and update existing tests for…
gsteel Nov 5, 2018
afa36ea
Remove assertions from within worker process
gsteel Nov 5, 2018
b623d6f
Resolves the test for throwing an exception if the server has already…
gsteel Nov 6, 2018
a959411
CS Fix
gsteel Nov 6, 2018
5eadaac
Improve tests to ensure no processes are left running
gsteel Nov 6, 2018
dbb8900
Make setting the initial server options the responsibility of the Htt…
gsteel Nov 7, 2018
7ea2bdf
Remove ServerFactory and ServerFactoryFactory
gsteel Nov 7, 2018
e33cc63
Change RequestHandlerRunner constructor signature, its factory and mo…
gsteel Nov 7, 2018
d5c8f50
Initial stab at documenting usage of async task workers…
gsteel Nov 7, 2018
e634170
Move configuration for SwooleRuntime::enableCoroutine() call to the t…
gsteel Nov 8, 2018
571fd12
Move documentation changes for #40 to v2 docs
weierophinney Nov 8, 2018
906b0de
Fix whitespace in test and missing doc header
gsteel Nov 8, 2018
ad535e1
Remove duplicate import of `extension_loaded`
gsteel Nov 8, 2018
8771a58
Improve async task docs
gsteel Nov 8, 2018
7366afc
Alter factory to allow servers to be started with SSL as reported in #44
gsteel Nov 12, 2018
4dbd90c
Remove deleted `ServerFactory` and `ServerFactoryFactory` from Config…
gsteel Nov 12, 2018
31e405c
Add note in docs about SSL support in Swoole
gsteel Nov 12, 2018
354cbeb
Change visibility of constants in HttpServerFactory
gsteel Nov 12, 2018
53cf520
Reviewed and edited async tasks documentation
weierophinney Nov 13, 2018
b4a4189
Make a true note regarding SSL support
weierophinney Nov 13, 2018
1fd77ce
Documents migration for the ServerFactory class
weierophinney Nov 13, 2018
6bf845a
Better page title for async-tasks document
weierophinney Nov 13, 2018
d4bea82
Final edits to async tasks document
weierophinney Nov 13, 2018
c9fc1d0
Updates CHANGELOG for #40
weierophinney Nov 13, 2018
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
182 changes: 182 additions & 0 deletions docs/book/v2/async-tasks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
# Triggering Async Tasks in Swoole HTTP Server

If a certain request triggers a workload that perhaps takes a long time to process you'd typically offload this
work to some kind of message queue perhaps.

The Swoole server process provides the facility to run and communicate with task worker processes using its
`on('Task')` event handler potentially saving the need for external message queues and additional worker processes.

### Configuring the Server Process

In order to take advantage of this feature within a Zend Expressive application running in the Swoole environment, you
will first need to configure the server to start up task workers. In your local configuration for the server, you'll
need to add `task_worker_num`. The number of workers you configure define the number of concurrent tasks that can be
executed at once. Tasks are queued in the order that they triggered, meaning that a `task_worker_num` of 1
will offer no concurrency and tasks will execute one after the other; the size of that buffer is currently
not known.

```php
'zend-expressive-swoole' => [
'swoole-http-server' => [
'host' => '127.0.0.1',
'port' => 8080,
'options' => [
'worker_num' => 4, // The number of HTTP Server Workers
'task_worker_num' => 4, // The number of Task Workers
],
],
];
```

### Task Event Handlers

The Swoole server will now require that 2 new event callbacks are non null. These are the `onTask` and `onFinish`
events. Without both of these setup, the server will refuse to start.

#### Registering the Handlers

The signature for the `onTask` event handler is this:

```php
use Swoole\Http\Server as HttpServer;
$serverOnTaskCallback = function (HttpServer $server, int $taskId, int $sourceWorkerId, $dataForWorker) {};
```

- `$server` is the main Http Server process
- `$taskId` is a number that increments each time the server triggers a new task.
- `$sourceWorkerId` is an integer that defines the worker process that is executing the workload.
- `$dataForWorker` contains the value passed to the `$server->task()` method when initially triggering the task. This
value can be anything you define with the exception of a `resource`.

To register the handler with the server, you must call it's `on()` method, **before** the server has been started in the
following way:

```php
$server->on('Task', $callable);
```

There can be **only one** event handler per event type. Subsequent calls to `on('<EventName>')` replace the previously
registered callable.

As previously mentioned, you must also register an event handler for the `'Finish'` event. This event handler has the
following signature:

```php
$serverOnFinishCallback = function (HttpServer $server, int $taskId, $userData) {};
```

The first 2 parameters are identical to the `onTask` event handler. The `$userData` parameter will contain the return
value of the `onTask` event handler.

If you do not return anything from your `onTask` event handler, the `onFinish` handler **will not be called**.

Registering your callable for the finish event is accomplished like this:

```php
$server->on('Finish', $callable);
```

## An example task broker

The following example code illustrates dispatching events and performing logging duties as a task handler:

```php
use Psr\EventDispatcher\MessageInterface;
use Psr\EventDispatcher\MessageNotifierInterface;
use Psr\Log\LoggerInterface;

class TaskWorker
{
private $notifier;
private $logger;

public function __construct(LoggerInterface $logger, MessageNotifierInterface $notifier)
{
$this->logger = $logger;
$this->notifier = $notifier;
}

public function __invoke($server, $taskId, $fromId, $data)
{
if (! $data instanceof MessageInterface) {
$this->logger->error('Invalid data provided to task worker: {type}', ['type' => is_object($data) ? get_class($data) : gettype($data)]);
return;
}
$this->logger->info('Starting work on task {taskId} using data: {data}', ['taskId' => $taskId, 'data' => json_encode($data)]);
try {
$this->notifier->notify($data);
} catch (\Throwable $e) {
$this->logger->error('Error processing task {taskId}: {error}', ['taskId' => $taskId, 'error' => $e->getTraceAsString()]);
}
}
}
```

This invokable class needs to be attached to the `$server->on('Task')` event before the server has started. The
easiest place to accomplish this is in a delegator factory targeting the Swoole Http Server:

```php
// config/dependencies.php
return [
'dependencies' => [
'delegators' => [
\Swoole\Http\Server::class => [
TaskWorkerDelegator::class,
],
],
],
];

// TaskWorkerDelegator.php

use Psr\Container\ContainerInterface;
use Swoole\Http\Server as HttpServer;
use Psr\Log\LoggerInterface;

class TaskWorkerDelegator
{
public function __invoke(ContainerInterface $container, $serviceName, callable $callback) : HttpServer
{
$server = $callback();
$server->on('Task', $container->get(TaskWorker::class));

$logger = $container->get(LoggerInterface::class);
$server->on('Finish', function ($server, $taskId, $data) use ($logger) {
$logger->notice('Task #{taskId} has finished processing', ['taskId' => $taskId]);
});
return $server;
}
}
```

### Triggering Tasks in Middleware

Finally, it is likely that tasks will be triggered by an HTTP request that is travelling through a pipeline, therefore
you will need to create a middleware that triggers the task worker in the Swoole HTTP Server.

As previously mentioned, the Swoole server provides the `$server->task($data)` method to accomplish this. The `$data`
parameter can be any value except for a resource.

```php
class TaskTriggeringMiddleware implements MiddlewareInterface
{
private $server;

public function __construct(HttpServer $server)
{
$this->server = $server;
}

public function process(Request $request, Handler $handler) : Response
{
// A fictonal event describing the requirement to send an email:
$event = new EmailEvent([
'to' => 'you@example.com',
'subject' => 'Non-blocking you say?',
]);
$taskIdentifier = $this->server->task($event);
// The method is asyncronous so execution continues immediately
return $handler->handle($request);
}
}
```
2 changes: 1 addition & 1 deletion docs/book/v2/how-it-works.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ be freed after a request. This allows application configuration and artifacts
(such as middleware and handlers) to persist between requests and processes.

Under Swoole 4.1+, for even better performance, you can enable the option
`zend-expressive-swoole.swoole-http-server.options.enable_coroutine`. When this
`zend-expressive-swoole.enable_coroutine`. When this
is enabled, Swoole will run most I/O processes in coroutines. Doing so provides
approximately **10 times faster performance** than without coroutines, meaning a
Swoole-based application can be 40 to 50 times faster than running under nginx
Expand Down
12 changes: 10 additions & 2 deletions docs/book/v2/intro.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,11 @@ demonstrates enabling SSL:
// config/autoload/swoole.local.php
return [
'zend-expressive-swoole' => [
// Available in Swoole 4.1 and up; enables coroutine support
// for most I/O operations:
'enable_coroutine' => true,

// Configure Swoole HTTP Server:
'swoole-http-server' => [
'host' => '192.168.0.1',
'port' => 9501,
Expand All @@ -92,15 +97,18 @@ return [
// Set the SSL certificate and key paths for SSL support:
'ssl_cert_file' => 'path/to/ssl.crt',
'ssl_key_file' => 'path/to/ssl.key',
// Available in Swoole 4.1 and up; enables coroutine support
// for most I/O operations:
// Whether or not the HTTP server should use coroutines;
// enabled by default, and generally should not be disabled:
'enable_coroutine' => true,
],
],
],
];
```

_Note_: By default, Swoole will not be compiled with SSL support. To enable SSL in Swoole, it must be configured with
`--enable-openssl` or `--with-openssl-dir=/path/to/openssl`

### Serving static files

We support serving static files. By default, we serve files with extensions in
Expand Down
27 changes: 27 additions & 0 deletions docs/book/v2/migration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Migration

This document covers changes between version 1 and version 2, and how you may
update your code to adapt to them.

## Coroutine support

In version 1, to enable Swoole's coroutine support, you were expected to pass a
boolean true value to the
`zend-expressive-swoole.swoole-http-server.options.enable_coroutine` flag.

That flag now controls specifically the HTTP server coroutine support, and
defaults to `true`. To set system-wide coroutine support, toggle the
`zend-expressive-swoole.enable_coroutine` flag, which defaults to boolean false:

```php
return [
'zend-expressive-swoole' => [
'enable_coroutine' => true, // system-wide support
'swoole-http-server' => [
'options' => [
'enable_coroutine' => true, // HTTP server coroutine support
],
],
]
];
```
2 changes: 2 additions & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ pages:
- Reference:
- "Static Resources": v2/static-resources.md
- Logging: v2/logging.md
- "Async Tasks": v2/async-tasks.md
- "How it works": v2/how-it-works.md
- "Considerations when using Swoole": v2/considerations.md
- Migration: v2/migration.md
- v1:
- Intro: v1/intro.md
- Reference:
Expand Down
3 changes: 2 additions & 1 deletion src/ConfigProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
namespace Zend\Expressive\Swoole;

use Psr\Http\Message\ServerRequestInterface;
use Swoole\Http\Server as SwooleHttpServer;
use Zend\HttpHandlerRunner\RequestHandlerRunner;
use function extension_loaded;

Expand Down Expand Up @@ -50,9 +51,9 @@ public function getDependencies() : array
Log\AccessLogInterface::class => Log\AccessLogFactory::class,
PidManager::class => PidManagerFactory::class,
SwooleRequestHandlerRunner::class => SwooleRequestHandlerRunnerFactory::class,
ServerFactory::class => ServerFactoryFactory::class,
ServerRequestInterface::class => ServerRequestSwooleFactory::class,
StaticResourceHandlerInterface::class => StaticResourceHandlerFactory::class,
SwooleHttpServer::class => HttpServerFactory::class,
],
'aliases' => [
RequestHandlerRunner::class => SwooleRequestHandlerRunner::class,
Expand Down
101 changes: 101 additions & 0 deletions src/HttpServerFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
<?php
/**
* @see https://github.com/zendframework/zend-expressive-swoole for the canonical source repository
* @copyright Copyright (c) 2018 Zend Technologies USA Inc. (http://www.zend.com)
* @license https://github.com/zendframework/zend-expressive-swoole/blob/master/LICENSE.md New BSD License
*/

declare(strict_types=1);
weierophinney marked this conversation as resolved.
Show resolved Hide resolved

namespace Zend\Expressive\Swoole;

use Psr\Container\ContainerInterface;
use Swoole\Http\Server as SwooleHttpServer;
use SwooleRuntime;

use function defined;
use function in_array;

use const SWOOLE_BASE;
use const SWOOLE_PROCESS;
use const SWOOLE_SOCK_TCP;
use const SWOOLE_SOCK_TCP6;
use const SWOOLE_SOCK_UDP;
use const SWOOLE_SOCK_UDP6;
use const SWOOLE_SSL;
use const SWOOLE_UNIX_DGRAM;
use const SWOOLE_UNIX_STREAM;

class HttpServerFactory
{
public const DEFAULT_HOST = '127.0.0.1';
public const DEFAULT_PORT = 8080;

/**
* Swoole server supported modes
*/
private const MODES = [
SWOOLE_BASE,
SWOOLE_PROCESS
];

/**
* Swoole server supported protocols
*/
private const PROTOCOLS = [
SWOOLE_SOCK_TCP,
SWOOLE_SOCK_TCP6,
SWOOLE_SOCK_UDP,
SWOOLE_SOCK_UDP6,
SWOOLE_UNIX_DGRAM,
SWOOLE_UNIX_STREAM
];

/**
* @see https://www.swoole.co.uk/docs/modules/swoole-server-methods#swoole_server-__construct
* @see https://www.swoole.co.uk/docs/modules/swoole-server/predefined-constants for $mode and $protocol constant
* @throws Exception\InvalidArgumentException for invalid $port values
* @throws Exception\InvalidArgumentException for invalid $mode values
* @throws Exception\InvalidArgumentException for invalid $protocol values
*/
public function __invoke(ContainerInterface $container) : SwooleHttpServer
{
$config = $container->get('config');
$swooleConfig = $config['zend-expressive-swoole'] ?? [];
$serverConfig = $swooleConfig['swoole-http-server'] ?? [];

$host = $serverConfig['host'] ?? static::DEFAULT_HOST;
$port = $serverConfig['port'] ?? static::DEFAULT_PORT;
$mode = $serverConfig['mode'] ?? SWOOLE_BASE;
$protocol = $serverConfig['protocol'] ?? SWOOLE_SOCK_TCP;

if ($port < 1 || $port > 65535) {
throw new Exception\InvalidArgumentException('Invalid port');
}

if (! in_array($mode, static::MODES, true)) {
throw new Exception\InvalidArgumentException('Invalid server mode');
}

$validProtocols = static::PROTOCOLS;
if (defined('SWOOLE_SSL')) {
$validProtocols[] = SWOOLE_SOCK_TCP | SWOOLE_SSL;
$validProtocols[] = SWOOLE_SOCK_TCP6 | SWOOLE_SSL;
}

if (! in_array($protocol, $validProtocols, true)) {
throw new Exception\InvalidArgumentException('Invalid server protocol');
}

$enableCoroutine = $swooleConfig['enable_coroutine'] ?? false;
if ($enableCoroutine && method_exists(SwooleRuntime::class, 'enableCoroutine')) {
SwooleRuntime::enableCoroutine(true);
}

$httpServer = new SwooleHttpServer($host, $port, $mode, $protocol);
$serverOptions = $serverConfig['options'] ?? [];
$httpServer->set($serverOptions);

return $httpServer;
}
}
Loading