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
<?php
/**
* Swoole entry point for a Tina4 PHP app. Generated by
* `tina4 deploy docker --runtime swoole`.
*
* App::__invoke() is the seam: it takes a Swoole\Http\Request and returns a
* Tina4\Response. Routes, ORM, middleware and Frond are untouched - only the
* transport changes.
*/
require __DIR__ . '/vendor/autoload.php';
$app = new \Tina4\App(__DIR__);
$host = getenv('TINA4_SWOOLE_HOST') ?: '0.0.0.0';
$port = (int)(getenv('TINA4_SWOOLE_PORT') ?: 7145);
$http = new Swoole\Http\Server($host, $port);
/**
* How many workers, on whichever extension is actually installed.
*
* OpenSwoole 22+ REMOVED the procedural swoole_*() aliases: on openswoole
* 26.2.0, swoole_cpu_num() and swoole_version() are both gone and the CPU count
* lives on OpenSwoole\Util. Mainline Swoole is the mirror image - it has the
* functions and no OpenSwoole\Util. Calling either one unguarded is a fatal at
* STARTUP on the other build, which is the worst place for it: the image builds
* fine, then the container exits 255 the moment it runs.
*
* (Measured: an earlier version of this file called swoole_cpu_num() directly
* and did exactly that.)
*/
$workers = (int)(getenv('TINA4_SWOOLE_WORKERS') ?: 0);
if ($workers <= 0) {
if (class_exists('OpenSwoole\Util')) {
$workers = \OpenSwoole\Util::getCPUNum() * 2;
} elseif (function_exists('swoole_cpu_num')) {
$workers = swoole_cpu_num() * 2;
} else {
$workers = 4;
}
}
$http->set([
'worker_num' => $workers,
// Recycle a worker after this many requests. A Swoole worker is resident,
// so anything an app leaks into a static accumulates across every request
// it ever serves. This bounds the damage from a leak you have not found.
'max_request' => (int)(getenv('TINA4_SWOOLE_MAX_REQUEST') ?: 10000),
'enable_coroutine' => true,
]);
$http->on('request', function ($req, $res) use ($app) {
try {
$response = $app($req);
$res->status($response->getStatusCode());
foreach ($response->getHeaders() as $name => $value) {
$res->header($name, $value);
}
// Cookies are a SEPARATE bag from headers on Tina4\Response. Drop this
// loop and every Set-Cookie vanishes, which silently breaks sessions
// and logins while every response still returns 200.
foreach ($response->getCookies() as $name => $cookie) {
$res->cookie(
$name,
$cookie['value'],
$cookie['expires'],
$cookie['path'],
$cookie['domain'],
$cookie['secure'],
$cookie['httponly'],
$cookie['samesite']
);
}
$res->end($response->getBody());
} catch (\Throwable $e) {
// A throw that escapes here would kill the worker and take every
// in-flight coroutine with it, so it is contained and logged instead.
\Tina4\Log::error('Unhandled error in the Swoole request handler: ' . $e->getMessage(), [
'file' => $e->getFile(),
'line' => $e->getLine(),
]);
$res->status(500);
$res->end('Internal Server Error');
}
});
$http->start();