Skip to content

Commit 0be9828

Browse files
committed
Cap the OOM auto-dump so a stuck worker can't dump forever
A worker pinned at memory_limit OOMs on nearly every request; an unguarded auto-dump would write a fresh 100MB+ file each time and fill the disk. Add a per-worker, cross-request guard on the automatic dump: - rdump.oom_dump_max (default 1, 0 = unlimited): max attempts per process. - rdump.oom_dump_min_interval (default 0 = off): minimum seconds between attempts. Both gates apply together, and an attempt counts whether it succeeds or not so a broken path/full disk can't retry without bound. Explicit rdump_dump() calls are never throttled. https://claude.ai/code/session_017skw8BsHkF8wur7Pf4G9W5
1 parent 57ac552 commit 0be9828

3 files changed

Lines changed: 72 additions & 11 deletions

File tree

README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,8 @@ php.ini:
9999
extension=rdump.so
100100
rdump.oom_dump=/var/log/php-oom-%p.rdump ; empty = disabled
101101
;rdump.oom_dump_full=1 ; also embed read-only segments
102+
;rdump.oom_dump_max=1 ; max auto-dumps per worker (0 = unlimited)
103+
;rdump.oom_dump_min_interval=0 ; min seconds between auto-dumps (0 = off)
102104
```
103105

104106
The path may contain `%p` (PID) and `%t` (Unix time), expanded when the dump
@@ -113,6 +115,25 @@ from the engine's error callback (`zend_error_cb`) — on the intact stack,
113115
before any unwind, and off the `memory_limit` heap (libc `malloc`) — so it
114116
captures even the OOMs a shutdown handler cannot.
115117

118+
#### Don't let it run away
119+
120+
A worker pinned at `memory_limit` tends to OOM on *every* request, so an
121+
unguarded auto-dump would write a fresh (often 100 MB+) file each time and bury
122+
the disk. By default the auto-dump therefore fires **at most once per worker
123+
process** (`rdump.oom_dump_max=1`); the counter is per worker and spans requests,
124+
so restarting the worker re-arms it. Raise the cap if you want more, and/or set a
125+
minimum spacing — both gates apply together:
126+
127+
```ini
128+
rdump.oom_dump_max=5 ; up to 5 dumps over the worker's lifetime (0 = unlimited)
129+
rdump.oom_dump_min_interval=60 ; ...and at least 60s apart
130+
```
131+
132+
A dump attempt counts toward the cap whether it succeeds or not, so a bad path or
133+
a full disk can't retry forever either. This guard applies only to the automatic
134+
OOM dump — explicit `rdump_dump()` calls are never throttled. (Combine the cap
135+
with a `%p` path so each worker still writes a distinct file.)
136+
116137
You can also toggle it at runtime — handy for a per-request/per-pid filename,
117138
or when you cannot edit php.ini:
118139

php_rdump.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,14 @@ extern zend_module_entry rdump_module_entry;
2626
ZEND_BEGIN_MODULE_GLOBALS(rdump)
2727
char *oom_dump; /* rdump.oom_dump (INI): path, or "" to disable */
2828
zend_bool oom_dump_full; /* rdump.oom_dump_full (INI) */
29+
/* Runaway guard for the OOM auto-dump. A worker that keeps hitting
30+
* memory_limit would otherwise dump on every request and bury the disk;
31+
* these cap the dumps over the worker's whole lifetime, not per request.
32+
* count/last_ts are process- (per-thread-) scoped and survive RSHUTDOWN. */
33+
zend_long oom_dump_max; /* rdump.oom_dump_max (INI): 0 = unlimited */
34+
zend_long oom_dump_min_interval; /* rdump.oom_dump_min_interval (INI), seconds; 0 = off */
35+
zend_long oom_dump_count; /* OOM dumps attempted so far this process */
36+
zend_long oom_dump_last_ts; /* time() of the last OOM dump attempt, 0 = none */
2937
/* Runtime override set via rdump_set_oom_dump(). Owned by us (libc
3038
* strdup/free), reset each request. Takes precedence over the INI
3139
* default when oom_dump_runtime_set is true ("" => force-disabled). */

rdump.c

Lines changed: 43 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -584,18 +584,38 @@ static void rdump_zend_error_cb(RDUMP_ERROR_CB_PARAMS)
584584
&& msg != NULL
585585
&& strncmp(msg, RDUMP_OOM_PREFIX, sizeof(RDUMP_OOM_PREFIX) - 1) == 0
586586
) {
587-
char expanded[4096];
588-
const char *out_path = path;
589-
if (strchr(path, '%') != NULL
590-
&& rdump_expand_path(path, expanded, sizeof(expanded)) == 0
591-
) {
592-
out_path = expanded;
587+
/* Runaway guard: a worker pinned at memory_limit OOMs on nearly
588+
* every request, so cap the auto-dump over the worker's lifetime
589+
* by count and/or by minimum spacing. Both gates apply together;
590+
* the default max of 1 keeps it to a single dump per worker. */
591+
zend_long max = RDUMP_G(oom_dump_max);
592+
zend_long interval = RDUMP_G(oom_dump_min_interval);
593+
zend_long now = (zend_long)time(NULL);
594+
int capped = (max > 0 && RDUMP_G(oom_dump_count) >= max);
595+
int too_soon = (interval > 0
596+
&& RDUMP_G(oom_dump_last_ts) != 0
597+
&& now - RDUMP_G(oom_dump_last_ts) < interval);
598+
599+
if (!capped && !too_soon) {
600+
char expanded[4096];
601+
const char *out_path = path;
602+
if (strchr(path, '%') != NULL
603+
&& rdump_expand_path(path, expanded, sizeof(expanded)) == 0
604+
) {
605+
out_path = expanded;
606+
}
607+
/* Count the attempt (success or failure) and stamp the time
608+
* before writing, so a broken path or full disk cannot retry
609+
* without bound either. */
610+
RDUMP_G(oom_dump_count)++;
611+
RDUMP_G(oom_dump_last_ts) = now;
612+
613+
char *err = NULL;
614+
RDUMP_G(in_oom_dump) = 1;
615+
/* Best-effort: this is the death path, so swallow any failure. */
616+
(void)rdump_write_file(out_path, full, &err);
617+
RDUMP_G(in_oom_dump) = 0;
593618
}
594-
char *err = NULL;
595-
RDUMP_G(in_oom_dump) = 1;
596-
/* Best-effort: this is the death path, so swallow any failure. */
597-
(void)rdump_write_file(out_path, full, &err);
598-
RDUMP_G(in_oom_dump) = 0;
599619
}
600620
}
601621
rdump_original_error_cb(RDUMP_ERROR_CB_PASSTHRU);
@@ -666,6 +686,14 @@ PHP_INI_BEGIN()
666686
"rdump.oom_dump_full", "0", PHP_INI_SYSTEM, OnUpdateBool,
667687
oom_dump_full, zend_rdump_globals, rdump_globals
668688
)
689+
STD_PHP_INI_ENTRY(
690+
"rdump.oom_dump_max", "1", PHP_INI_SYSTEM, OnUpdateLong,
691+
oom_dump_max, zend_rdump_globals, rdump_globals
692+
)
693+
STD_PHP_INI_ENTRY(
694+
"rdump.oom_dump_min_interval", "0", PHP_INI_SYSTEM, OnUpdateLong,
695+
oom_dump_min_interval, zend_rdump_globals, rdump_globals
696+
)
669697
PHP_INI_END()
670698

671699
static const zend_function_entry rdump_functions[] = {
@@ -681,6 +709,10 @@ static PHP_GINIT_FUNCTION(rdump)
681709
#endif
682710
rdump_globals->oom_dump = NULL;
683711
rdump_globals->oom_dump_full = 0;
712+
rdump_globals->oom_dump_max = 1;
713+
rdump_globals->oom_dump_min_interval = 0;
714+
rdump_globals->oom_dump_count = 0;
715+
rdump_globals->oom_dump_last_ts = 0;
684716
rdump_globals->oom_dump_runtime = NULL;
685717
rdump_globals->oom_dump_runtime_full = 0;
686718
rdump_globals->oom_dump_runtime_set = 0;

0 commit comments

Comments
 (0)