Repository navigation
Expand file tree
/
Copy pathMemoryLimitHandler.php
More file actions
148 lines (133 loc) · 4.95 KB
/
Copy pathMemoryLimitHandler.php
File metadata and controls
148 lines (133 loc) · 4.95 KB
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
<?php
/**
* This file is part of the reliforp/reli-prof package.
*
* (c) sji <sji@sj-i.dev>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Reli\Sidecar\Client;
/**
* Registers a shutdown handler that requests a memory dump
* from the reli sidecar daemon when a memory_limit error occurs.
*
* Usage:
* MemoryLimitHandler::register();
*
* Or with options:
* MemoryLimitHandler::register(
* socket_path: '/custom/path.sock',
* on_response: function (SidecarClientResponse $r) {
* error_log('reli dump: ' . $r->path);
* },
* );
*
* Socket path is resolved via:
* 1. $socket_path argument
* 2. RELI_SIDECAR_SOCKET environment variable
* 3. $XDG_RUNTIME_DIR/reli/sidecar.sock (throws if XDG_RUNTIME_DIR unset)
*
* ## Emergency Memory Reserve
*
* When PHP hits memory_limit, the shutdown handler runs with almost no
* free memory. Even `stream_socket_client()` needs to allocate buffers.
* To ensure the handler can function, this class pre-allocates a block
* of memory (default 256 KB) at `register()` time and releases it at
* the very start of the shutdown handler, freeing enough headroom for
* the socket operations.
*
* Adjust the reserve size via the $reserve_bytes parameter if needed.
*/
final class MemoryLimitHandler
{
const DEFAULT_RESERVE_BYTES = 256 * 1024;
/** @var string|null Pre-allocated memory block, released on shutdown */
private static $reserve;
/**
* @param string|null $socket_path UDS path (null = auto-detect)
* @param (callable(SidecarClientResponse): void)|null $on_response
* @param (callable(string): void)|null $on_error called with error message on failure
* @param int $reserve_bytes emergency memory reserve size (default: 256 KB)
* @param int $timeout_seconds socket I/O timeout passed to {@see SidecarClient}.
* Sidecar dump throughput is roughly 100 MB/s, and the client blocks on
* the response read for the full dump duration, so size this against the
* target's `memory_limit` (default 30 s comfortably covers ~3 GB heaps).
*/
public static function register(
$socket_path = null,
$on_response = null,
$on_error = null,
int $reserve_bytes = self::DEFAULT_RESERVE_BYTES,
int $timeout_seconds = 30
) {
// Pre-load classes so autoload doesn't run inside the shutdown handler.
// Autoloading involves file I/O + parsing which can easily exceed
// the emergency reserve when memory_limit is already exhausted.
class_exists(SidecarClient::class);
class_exists(SidecarClientResponse::class);
self::$reserve = str_repeat("\0", $reserve_bytes);
register_shutdown_function(
self::createHandler($socket_path, $on_response, $on_error, $timeout_seconds)
);
}
/**
* @param string|null $socket_path
* @param (callable(SidecarClientResponse): void)|null $on_response
* @param (callable(string): void)|null $on_error
* @return \Closure
*/
private static function createHandler(
$socket_path = null,
$on_response = null,
$on_error = null,
int $timeout_seconds
): \Closure {
return static function () use ($socket_path, $on_response, $on_error, $timeout_seconds) {
// Release emergency reserve to free memory for socket operations
self::$reserve = null;
$error = error_get_last();
if ($error === null) {
return;
}
if (!self::isMemoryLimitError($error['message'])) {
return;
}
$pid = getmypid();
if ($pid === false) {
return;
}
$client = new SidecarClient($socket_path, $timeout_seconds);
$response = $client->requestDump(
$pid,
$error['file'] ?? null,
$error['line'] ?? null
);
if ($response === null) {
if ($on_error !== null) {
$on_error('failed to connect to reli sidecar');
}
return;
}
if ($on_response !== null) {
$on_response($response);
} elseif ($response->isOk()) {
error_log(sprintf(
'reli-sidecar: memory dump saved to %s (%.1f MB)',
$response->path ?? '',
(float)($response->bytes ?? 0) / 1024.0 / 1024.0
));
} else {
error_log(sprintf(
'reli-sidecar: dump failed: %s',
$response->message ?? 'unknown error'
));
}
};
}
private static function isMemoryLimitError(string $message): bool
{
return strpos($message, 'Allowed memory size') !== false;
}
}