1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
|
RoadRunner
==========
[![Latest Stable Version](https://poser.pugx.org/spiral/roadrunner/version)](https://packagist.org/packages/spiral/roadrunner)
[![GoDoc](https://godoc.org/github.com/spiral/roadrunner?status.svg)](https://godoc.org/github.com/spiral/roadrunner)
[![Build Status](https://travis-ci.org/spiral/roadrunner.svg?branch=master)](https://travis-ci.org/spiral/roadrunner)
[![Go Report Card](https://goreportcard.com/badge/github.com/spiral/roadrunner)](https://goreportcard.com/report/github.com/spiral/roadrunner)
[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/spiral/roadrunner/badges/quality-score.png)](https://scrutinizer-ci.com/g/spiral/roadrunner/?branch=master)
[![Codecov](https://codecov.io/gh/spiral/roadrunner/branch/master/graph/badge.svg)](https://codecov.io/gh/spiral/roadrunner/)
RoadRunner is an open source (MIT licensed), high-performance PSR-7 PHP application server, load balancer and process manager.
It supports running as a service with the ability to extend its functionality on a per-project basis.
Features:
--------
- production ready
- PSR-7 HTTP server (file uploads, error handling, static files, hot reload, middlewares, event listeners)
- extendable service model (plus PHP compatible RPC server)
- flexible ENV configuration
- no external PHP dependencies, drop-in (based on [Goridge](https://github.com/spiral/goridge))
- load balancer, process manager and task pipeline
- frontend agnostic (queue, REST, PSR-7, async php, etc)
- works over TCP, unix sockets and standard pipes
- automatic worker replacement and safe PHP process destruction
- worker lifecycle management (create/allocate/destroy timeouts)
- payload context and body
- control over max jobs per worker
- protocol, worker and job level error management (including PHP errors)
- memory leak failswitch
- very fast (~250k rpc calls per second on Ryzen 1700X using 16 threads)
- works on Windows
Getting Started:
--------
#### Downloading RoadRunner
The easiest way to get the latest RoadRunner version is to use one of the pre-built release binaries which are available for
OSX, Linux, FreeBSD, and Windows. Instructions for using these binaries are on the GitHub [releases page](https://github.com/spiral/roadrunner/releases).
#### Building RoadRunner
RoadRunner can be compiled on Linux, OSX, Windows and other 64 bit environments as the only requirement is Go 1.8+ itself.
To build:
```
$ make
```
To test:
```
$ make test
```
Using RoadRunner:
--------
In order to use RoadRunner you only have to place a `.rr.yaml` config file in the root of your PHP project:
```yaml
# defines environment variables for all underlying php processes
env:
key: value
# rpc bus allows php application and external clients to talk to rr services.
rpc:
# enable rpc server
enable: true
# rpc connection DSN. Supported TCP and Unix sockets.
listen: tcp://127.0.0.1:6001
# http service configuration.
http:
# set to false to disable http server.
enable: true
# http host to listen.
address: 0.0.0.0:8080
# max POST request size, including file uploads in MB.
maxRequest: 200
# file upload configuration.
uploads:
# list of file extensions which are forbidden for uploading.
forbid: [".php", ".exe", ".bat"]
# http worker pool configuration.
workers:
# php worker command.
command: "php psr-worker.php"
# connection method (pipes, tcp://:9000, unix://socket.unix).
relay: "pipes"
# worker pool configuration.
pool:
# number of workers to be serving.
numWorkers: 4
# maximum jobs per worker, 0 - unlimited.
maxJobs: 0
# for how long pool should attempt to allocate free worker (request timeout).
allocateTimeout: 60
# amount of time given to worker to gracefully destruct itself.
destroyTimeout: 30
# static file serving.
static:
# serve http static files
enable: true
# root directory for static file (http would not serve .php and .htaccess files).
dir: "public"
# list of extensions to forbid for serving.
forbid: [".php", ".htaccess"]
```
> You can use json or any config type supported by `spf13/viper`.
Where `psr-worker.php`:
```php
ini_set('display_errors', 'stderr');
require 'vendor/autoload.php';
$relay = new Spiral\Goridge\StreamRelay(STDIN, STDOUT);
$psr7 = new Spiral\RoadRunner\PSR7Client(new Spiral\RoadRunner\Worker($relay));
while ($req = $psr7->acceptRequest()) {
try {
$resp = new \Zend\Diactoros\Response();
$resp->getBody()->write("hello world");
$psr7->respond($resp);
} catch (\Throwable $e) {
$psr7->getWorker()->error((string)$e);
}
}
```
Run `composer require spiral/roadrunner` to load php library.
> Check how to init relay [here](./php-src/tests/client.php).
Working with RoadRunner service:
--------
RoadRunner application can be started by calling simple command from the root of your PHP application.
```
$ rr serve -v
```
You can also run RR in debug mode to view all incoming requests.
```
$ rr serve -d -v
```
You can force RR service to reload its http workers.
```
$ rr http:reset
```
> You can attach this command as file watcher in your IDE.
To view status of all active workers in interactive mode.
```
$ rr http:workers -i
```
```
+---------+-----------+---------+---------+--------------------+
| PID | STATUS | EXECS | MEMORY | CREATED |
+---------+-----------+---------+---------+--------------------+
| 9440 | ready | 42,320 | 31 MB | 22 minutes ago |
| 9447 | ready | 42,329 | 31 MB | 22 minutes ago |
| 9454 | ready | 42,306 | 31 MB | 22 minutes ago |
| 9461 | ready | 42,316 | 31 MB | 22 minutes ago |
+---------+-----------+---------+---------+--------------------+
```
Writing Services:
--------
RoadRunner uses a service bus to organize its internal services and their dependencies, this approach is similar to the PHP Container implementation. You can create your own services, event listeners, middlewares, etc.
RoadRunner will not start as a service without a proper config section at the moment. To do this, simply add the following section section to your `.rr.yaml` file.
```yaml
service:
enable: true
option: value
```
You can write your own config file now:
```golang
package service
type config struct {
Enable bool
Option string
}
```
To create the service, implement this interface:
```golang
// Service provides high level functionality for road runner modules.
type Service interface {
// Init must return configure service and return true if service hasStatus enabled. Must return error in case of
// misconfiguration. Services must not be used without proper configuration pushed first.
Init(cfg Config, c Container) (enabled bool, err error)
// Serve serves.
Serve() error
// Stop stops the service.
Stop()
}
```
A simple service might look like this:
```golang
package service
import "github.com/spiral/roadrunner/service"
const ID = "service"
type Service struct {
cfg *config
}
func (s *Service) Init(cfg service.Config, c service.Container) (enabled bool, err error) {
config := &config{}
if err := cfg.Unmarshal(config); err != nil {
return false, err
}
if !config.Enable {
return false, nil
}
s.cfg = config
return true, nil
}
func (s *Service) Serve() error {
return nil
}
func (s *Service) Stop() {
// nothing
}
```
Service can be added to RR bus by creating your own version of [main.go](https://github.com/spiral/roadrunner/blob/master/cmd/rr/main.go) file:
```golang
rr.Container.Register(service.ID, &service.Service{})
```
Your service should now work. In addition, you can create your own RPC adapters which are available via commands or from PHP using Goridge:
```golang
// in Init() method
if r, ok := c.Get(rpc.ID); ok >= service.StatusConfigured {
if h, ok := r.(*rpc.Service); ok {
h.Register("service", &rpcServer{s})
}
}
```
> RPC server must be written based on net/rpc rules: https://golang.org/pkg/net/rpc/
You can now connect to this service from PHP:
```php
// make sure to use same port as in .rr config for RPC service
$rpc = new Spiral\Goridge\RPC(new Spiral\Goridge\SocketRelay('localhost', 6001));
print_r($rpc->call('service.Method', $ars));
```
HTTP service provides its own methods as well:
```php
print_r($rpc->call('http.Workers', true));
//print_r($rpc->call('http.Reset', true));
```
You can register http middleware or event listener using this approach:
```golang
import (
rrttp "github.com/spiral/roadrunner/service/http"
)
//...
if h, ok := c.Get(rrttp.ID); ok >= service.StatusConfigured {
if h, ok := h.(*rrttp.Service); ok {
h.AddMiddleware(s.middleware)
h.AddListener(s.middleware)
}
}
```
Standalone Usage:
--------
You can also use RoadRunner as a library in order to drive your application without any additional protocol on top of it.
```go
srv := NewServer(
&ServerConfig{
Command: "php client.php echo pipes",
Relay: "pipes",
Pool: &Config{
NumWorkers: int64(runtime.NumCPU()),
AllocateTimeout: time.Second,
DestroyTimeout: time.Second,
},
})
defer srv.Stop()
srv.Start()
res, err := srv.Exec(&Payload{Body: []byte("hello")})
```
```php
<?php
/**
* @var Goridge\RelayInterface $relay
*/
use Spiral\Goridge;
use Spiral\RoadRunner;
ini_set('display_errors', 'stderr');
$rr = new RoadRunner\Worker($relay);
while ($body = $rr->receive($context)) {
try {
$rr->send((string)$body, (string)$context);
} catch (\Throwable $e) {
$rr->error((string)$e);
}
}
```
> Check how to init relay [here](./php-src/tests/client.php).
You can find more examples in tests and `php-src` directory.
License:
--------
The MIT License (MIT). Please see [`LICENSE`](./LICENSE) for more information.
|