Skip to content

Commit 0e29bb9

Browse files
committed
Simplify the ZTS reader count, wait for restarts of other processes
- Enter and leave share one function, accel_zts_readers_add(): the lock-free path changes a positive count that stays positive, the 0 <-> 1 transitions take the mutex and call fcntl(). - A thread that becomes the first reader of its process while another process restarts the cache now waits for the restart (F_SETLKW) instead of running its whole request uncached and resetting its PCRE cache. - Comments on opcache.force_restart_timeout under ZTS: the restart waits for running requests of the own process, other processes are killed with all their threads, as without ZTS. - Test: opcache_reset() in the CLI server with two workers; the restart must happen in the next request and the cache must work after it. A reference that is never released makes it fail.
1 parent 5492ce3 commit 0e29bb9

2 files changed

Lines changed: 84 additions & 47 deletions

File tree

‎ext/opcache/ZendAccelerator.c‎

Lines changed: 29 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -344,7 +344,7 @@ static MUTEX_T accel_zts_readers_mutex = NULL;
344344
static zend_atomic_int accel_zts_readers;
345345
static time_t accel_zts_next_warning;
346346

347-
static bool accel_zts_lock_reader_byte(int type)
347+
static bool accel_zts_lock_reader_byte(int cmd, int type)
348348
{
349349
struct flock lock;
350350

@@ -353,59 +353,39 @@ static bool accel_zts_lock_reader_byte(int type)
353353
lock.l_start = 1;
354354
lock.l_len = 1;
355355

356-
return fcntl(lock_file, F_SETLK, &lock) != -1;
356+
return fcntl(lock_file, cmd, &lock) != -1;
357357
}
358358

359-
/* Must only be called from RINIT; on success the caller sets ZCG(counted) */
360-
static bool accel_zts_reader_enter(void)
359+
/* Adds (+1, only from RINIT) or releases (-1) a reference of the calling thread.
360+
* Does not use ZCG() as it is also called from accel_globals_dtor(). */
361+
static bool accel_zts_readers_add(int delta)
361362
{
362363
int count;
363364

364365
#ifndef HAVE_NO_ATOMICS
365-
/* the process already holds the reader lock: only a positive count may grow */
366366
count = zend_atomic_int_load_ex(&accel_zts_readers);
367-
while (count > 0) {
368-
if (zend_atomic_int_compare_exchange_ex(&accel_zts_readers, &count, count + 1)) {
367+
while (count > 0 && count + delta > 0) {
368+
if (zend_atomic_int_compare_exchange_ex(&accel_zts_readers, &count, count + delta)) {
369369
return true;
370370
}
371371
}
372372
#endif
373373

374374
tsrm_mutex_lock(accel_zts_readers_mutex);
375375
count = zend_atomic_int_load_ex(&accel_zts_readers);
376-
/* fails while another process restarts the cache (it holds the write lock) */
377-
if (count == 0 && !accel_zts_lock_reader_byte(F_RDLCK)) {
376+
/* waits while another process restarts the cache (it holds the write lock) */
377+
if (count == 0 && !accel_zts_lock_reader_byte(F_SETLKW, F_RDLCK)) {
378378
zend_accel_error(ACCEL_LOG_DEBUG, "UpdateC(+1): %s (%d)", strerror(errno), errno);
379379
tsrm_mutex_unlock(accel_zts_readers_mutex);
380380
return false;
381381
}
382-
while (!zend_atomic_int_compare_exchange_ex(&accel_zts_readers, &count, count + 1));
383-
tsrm_mutex_unlock(accel_zts_readers_mutex);
384-
return true;
385-
}
386-
387-
/* Releases one reference; does not use ZCG() as it is also called from accel_globals_dtor() */
388-
static void accel_zts_reader_leave(void)
389-
{
390-
int count;
391-
392-
#ifndef HAVE_NO_ATOMICS
393-
count = zend_atomic_int_load_ex(&accel_zts_readers);
394-
while (count > 1) {
395-
if (zend_atomic_int_compare_exchange_ex(&accel_zts_readers, &count, count - 1)) {
396-
return;
397-
}
398-
}
399-
#endif
400-
401-
tsrm_mutex_lock(accel_zts_readers_mutex);
402-
count = zend_atomic_int_load_ex(&accel_zts_readers);
403-
while (!zend_atomic_int_compare_exchange_ex(&accel_zts_readers, &count, count - 1));
404-
if (count == 1) {
382+
while (!zend_atomic_int_compare_exchange_ex(&accel_zts_readers, &count, count + delta));
383+
if (count + delta == 0) {
405384
/* the last reader of this process; unlock before another thread may relock */
406-
accel_zts_lock_reader_byte(F_UNLCK);
385+
accel_zts_lock_reader_byte(F_SETLK, F_UNLCK);
407386
}
408387
tsrm_mutex_unlock(accel_zts_readers_mutex);
388+
return true;
409389
}
410390
#endif
411391

@@ -417,7 +397,7 @@ static inline zend_result accel_activate_add(void)
417397
INCREMENT(mem_usage);
418398
SHM_PROTECT();
419399
#elif defined(ZTS)
420-
/* A thread becomes a reader only in RINIT, see accel_zts_reader_enter(). A request
400+
/* A thread becomes a reader only in RINIT, see accel_zts_readers_add(). A request
421401
* that was not admitted there must not start to use SHM later. */
422402
return FAILURE;
423403
#else
@@ -471,7 +451,7 @@ static inline void accel_unlock_all(void)
471451
* belong to the process, and the SHM write lock (byte 0) and the restart lock
472452
* (byte 2) may be held by other threads. */
473453
if (ZCG(counted)) {
474-
accel_zts_reader_leave();
454+
accel_zts_readers_add(-1);
475455
}
476456
#else
477457
if (lock_file == -1) {
@@ -1032,32 +1012,34 @@ static inline bool accel_is_inactive(void)
10321012
static bool accel_zts_restart_begin(void)
10331013
{
10341014
int readers;
1015+
time_t now;
10351016

10361017
tsrm_mutex_lock(accel_zts_readers_mutex);
10371018
readers = zend_atomic_int_load_ex(&accel_zts_readers);
1038-
if (readers == 0 && accel_zts_lock_reader_byte(F_WRLCK)) {
1019+
if (readers == 0 && accel_zts_lock_reader_byte(F_SETLK, F_WRLCK)) {
10391020
return true;
10401021
}
10411022
tsrm_mutex_unlock(accel_zts_readers_mutex);
10421023

1024+
now = time(NULL);
10431025
if (readers == 0) {
1044-
/* readers in other processes: keep the force_restart_timeout handling */
1026+
/* readers in other processes: as without ZTS, force_restart_timeout kills them,
1027+
* here with all their threads */
10451028
(void) accel_is_inactive();
1046-
} else if (ZCG(accel_directives).force_restart_timeout
1047-
&& ZCSG(force_restart_time)
1048-
&& time(NULL) >= ZCSG(force_restart_time)
1049-
&& time(NULL) >= accel_zts_next_warning) {
1050-
/* threads cannot be killed, and this process must not kill itself */
1029+
} else if (ZCG(accel_directives).force_restart_timeout && ZCSG(force_restart_time)
1030+
&& now >= ZCSG(force_restart_time) && now >= accel_zts_next_warning) {
1031+
/* Threads cannot be killed and this process must not kill itself: the restart
1032+
* waits for the running requests, new ones run uncached meanwhile */
10511033
zend_accel_error(ACCEL_LOG_WARNING, "Restart delayed for %ld seconds by %d threads of process %d still using the cache",
1052-
(long)(time(NULL) - ZCSG(force_restart_time) + ZCG(accel_directives).force_restart_timeout), readers, getpid());
1053-
accel_zts_next_warning = time(NULL) + ZCG(accel_directives).force_restart_timeout;
1034+
(long)(now - ZCSG(force_restart_time) + ZCG(accel_directives).force_restart_timeout), readers, getpid());
1035+
accel_zts_next_warning = now + ZCG(accel_directives).force_restart_timeout;
10541036
}
10551037
return false;
10561038
}
10571039

10581040
static void accel_zts_restart_end(void)
10591041
{
1060-
accel_zts_lock_reader_byte(F_UNLCK);
1042+
accel_zts_lock_reader_byte(F_SETLK, F_UNLCK);
10611043
accel_zts_next_warning = 0;
10621044
tsrm_mutex_unlock(accel_zts_readers_mutex);
10631045
}
@@ -2886,7 +2868,7 @@ ZEND_RINIT_FUNCTION(zend_accelerator)
28862868
#if defined(ZTS) && !defined(ZEND_WIN32)
28872869
/* Under ZTS this is the only place where a request starts to use SHM */
28882870
if (ZCG(accelerator_enabled)) {
2889-
if (accel_zts_reader_enter()) {
2871+
if (accel_zts_readers_add(1)) {
28902872
ZCG(counted) = true;
28912873
/* SHM-backed keys may be added to the PCRE cache again */
28922874
ZCG(pcre_reseted) = false;
@@ -3127,7 +3109,7 @@ static void accel_globals_dtor(zend_accel_globals *accel_globals)
31273109
* all threads from the current one. */
31283110
if (accel_globals->counted && accel_zts_readers_mutex) {
31293111
accel_globals->counted = false;
3130-
accel_zts_reader_leave();
3112+
accel_zts_readers_add(-1);
31313113
}
31323114
#endif
31333115
zend_string_free(accel_globals->key);

‎ext/opcache/tests/gh8739.phpt‎

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
--TEST--
2+
GH-8739: OPcache restart in a server with several workers
3+
--DESCRIPTION--
4+
A scheduled restart happens in the next request once no request uses the cache, and the cache
5+
works after it. Under ZTS this checks the per-process reader count and the reader lock shared by
6+
the worker processes: a reference that is never released would delay the restart forever.
7+
--EXTENSIONS--
8+
opcache
9+
--INI--
10+
opcache.enable=1
11+
opcache.enable_cli=1
12+
--CONFLICTS--
13+
server
14+
--FILE--
15+
<?php
16+
$dir = __DIR__ . '/gh8739';
17+
@mkdir($dir);
18+
file_put_contents("$dir/status.php", <<<'PHP'
19+
<?php
20+
$status = opcache_get_status(false);
21+
echo $status['opcache_enabled'] ? 'enabled' : 'disabled',
22+
', restarts: ', $status['opcache_statistics']['manual_restarts'],
23+
', ', opcache_is_script_cached(__FILE__) ? 'cached' : 'not cached', "\n";
24+
PHP);
25+
file_put_contents("$dir/reset.php", "<?php\nvar_dump(opcache_reset());\n");
26+
27+
include __DIR__ . '/php_cli_server.inc';
28+
29+
if (PHP_OS_FAMILY !== 'Windows') {
30+
putenv('PHP_CLI_SERVER_WORKERS=2');
31+
}
32+
$ini = '-d opcache.enable=1 -d opcache.enable_cli=1 -d opcache.file_update_protection=0';
33+
$ext = ini_get('extension_dir');
34+
if (file_exists($ext . DIRECTORY_SEPARATOR . (PHP_OS_FAMILY === 'Windows' ? 'php_opcache.dll' : 'opcache.so'))) {
35+
$ini .= ' -d extension_dir=' . $ext . ' -d zend_extension=opcache';
36+
}
37+
php_cli_server_start($ini);
38+
39+
$base = 'http://' . PHP_CLI_SERVER_ADDRESS . '/gh8739/';
40+
foreach (['status', 'reset', 'status', 'status'] as $script) {
41+
echo @file_get_contents($base . $script . '.php') ?: "no response\n";
42+
}
43+
?>
44+
--CLEAN--
45+
<?php
46+
$dir = __DIR__ . '/gh8739';
47+
@unlink("$dir/status.php");
48+
@unlink("$dir/reset.php");
49+
@rmdir($dir);
50+
?>
51+
--EXPECT--
52+
enabled, restarts: 0, cached
53+
bool(true)
54+
enabled, restarts: 1, cached
55+
enabled, restarts: 1, cached

0 commit comments

Comments
 (0)