@@ -132,6 +132,8 @@ static zend_result (*orig_post_startup_cb)(void);
132132
133133static zend_result accel_post_startup (void );
134134static zend_result accel_finish_startup (void );
135+ static void zend_reset_cache_vars (void );
136+ static void accel_interned_strings_restore_state (void );
135137
136138static void preload_shutdown (void );
137139static void preload_activate (void );
@@ -398,6 +400,147 @@ static inline void accel_unlock_all(void)
398400#endif
399401}
400402
403+ /* ====================================================================
404+ * Epoch-based reclamation (EBR) for safe opcache_reset()/invalidate()
405+ *
406+ * Solves: php-src#8739, #14471, #18517, frankenphp#1737, #2265, #2170
407+ *
408+ * Design:
409+ * - Readers (per-request) publish their epoch on enter, clear on leave.
410+ * Cost: two atomic stores per request. No locks.
411+ * - Writers (opcache_reset) increment the global epoch and defer the
412+ * actual cleanup until all pre-epoch readers have completed.
413+ * - This guarantees that no reader holds a pointer to shared memory
414+ * that is about to be reclaimed.
415+ * ==================================================================== */
416+
417+ void accel_epoch_init (void )
418+ {
419+ int i ;
420+
421+ ZCSG (current_epoch ) = 1 ;
422+ ZCSG (drain_epoch ) = 0 ;
423+ ZCSG (reset_deferred ) = false;
424+ ZCSG (epoch_slot_next ) = 0 ;
425+
426+ for (i = 0 ; i < ACCEL_EPOCH_MAX_SLOTS ; i ++ ) {
427+ ZCSG (epoch_slots )[i ].epoch = ACCEL_EPOCH_INACTIVE ;
428+ }
429+ }
430+
431+ static int accel_epoch_alloc_slot (void )
432+ {
433+ int slot ;
434+
435+ #if defined(ZEND_WIN32 )
436+ slot = (int )InterlockedIncrement ((volatile LONG * )& ZCSG (epoch_slot_next )) - 1 ;
437+ #elif defined(__GNUC__ )
438+ slot = __atomic_fetch_add (& ZCSG (epoch_slot_next ), 1 , __ATOMIC_SEQ_CST );
439+ #else
440+ slot = ZCSG (epoch_slot_next )++ ;
441+ #endif
442+
443+ if (slot >= ACCEL_EPOCH_MAX_SLOTS ) {
444+ return ACCEL_EPOCH_NO_SLOT ;
445+ }
446+ return slot ;
447+ }
448+
449+ void accel_epoch_enter (void )
450+ {
451+ int slot = ZCG (epoch_slot );
452+
453+ if (UNEXPECTED (slot == ACCEL_EPOCH_NO_SLOT )) {
454+ slot = accel_epoch_alloc_slot ();
455+ ZCG (epoch_slot ) = slot ;
456+ }
457+
458+ if (EXPECTED (slot >= 0 && slot < ACCEL_EPOCH_MAX_SLOTS )) {
459+ uint64_t epoch = ACCEL_ATOMIC_LOAD_64 (& ZCSG (current_epoch ));
460+ ZCG (local_epoch ) = epoch ;
461+ ACCEL_ATOMIC_STORE_64 (& ZCSG (epoch_slots )[slot ].epoch , epoch );
462+ }
463+ }
464+
465+ void accel_epoch_leave (void )
466+ {
467+ int slot = ZCG (epoch_slot );
468+
469+ if (EXPECTED (slot >= 0 && slot < ACCEL_EPOCH_MAX_SLOTS )) {
470+ ACCEL_ATOMIC_STORE_64 (& ZCSG (epoch_slots )[slot ].epoch , ACCEL_EPOCH_INACTIVE );
471+ }
472+ }
473+
474+ static uint64_t accel_min_active_epoch (void )
475+ {
476+ uint64_t min_epoch = ACCEL_EPOCH_INACTIVE ;
477+ int i ;
478+
479+ for (i = 0 ; i < ACCEL_EPOCH_MAX_SLOTS ; i ++ ) {
480+ uint64_t e = ACCEL_ATOMIC_LOAD_64 (& ZCSG (epoch_slots )[i ].epoch );
481+ if (e < min_epoch ) {
482+ min_epoch = e ;
483+ }
484+ }
485+ return min_epoch ;
486+ }
487+
488+ bool accel_deferred_reset_pending (void )
489+ {
490+ return ZCSG (reset_deferred );
491+ }
492+
493+ void accel_try_complete_deferred_reset (void )
494+ {
495+ uint64_t drain_epoch ;
496+ uint64_t min_epoch ;
497+
498+ if (!ZCSG (reset_deferred )) {
499+ return ;
500+ }
501+
502+ drain_epoch = ACCEL_ATOMIC_LOAD_64 (& ZCSG (drain_epoch ));
503+ min_epoch = accel_min_active_epoch ();
504+
505+ if (min_epoch > drain_epoch ) {
506+ zend_shared_alloc_lock ();
507+
508+ if (ZCSG (reset_deferred )) {
509+ zend_accel_error (ACCEL_LOG_DEBUG , "Completing deferred opcache reset (drain_epoch=%" PRIu64 ", min_active=%" PRIu64 ")" ,
510+ drain_epoch , min_epoch );
511+
512+ zend_map_ptr_reset ();
513+ zend_reset_cache_vars ();
514+ zend_accel_hash_clean (& ZCSG (hash ));
515+
516+ if (ZCG (accel_directives ).interned_strings_buffer ) {
517+ accel_interned_strings_restore_state ();
518+ }
519+
520+ zend_shared_alloc_restore_state ();
521+
522+ if (ZCSG (preload_script )) {
523+ preload_restart ();
524+ }
525+
526+ #ifdef HAVE_JIT
527+ zend_jit_restart ();
528+ #endif
529+
530+ ZCSG (accelerator_enabled ) = ZCSG (cache_status_before_restart );
531+ if (ZCSG (last_restart_time ) < ZCG (request_time )) {
532+ ZCSG (last_restart_time ) = ZCG (request_time );
533+ } else {
534+ ZCSG (last_restart_time )++ ;
535+ }
536+
537+ ZCSG (reset_deferred ) = false;
538+ }
539+
540+ zend_shared_alloc_unlock ();
541+ }
542+ }
543+
401544/* Interned strings support */
402545
403546/* O+ disables creation of interned strings by regular PHP compiler, instead,
@@ -2671,11 +2814,24 @@ zend_result accel_activate(INIT_FUNC_ARGS)
26712814 ZCG (counted ) = false;
26722815 }
26732816
2817+ /* Enter the current epoch BEFORE checking for pending restart.
2818+ * This ensures that if a reset is initiated after this point,
2819+ * the deferred-reset logic will wait for us to leave. */
2820+ accel_epoch_enter ();
2821+
2822+ /* Check if a deferred reset from a previous opcache_reset() can
2823+ * now be completed (all pre-epoch readers have finished). */
2824+ if (ZCSG (reset_deferred )) {
2825+ accel_try_complete_deferred_reset ();
2826+ }
2827+
26742828 if (ZCSG (restart_pending )) {
26752829 zend_shared_alloc_lock ();
26762830 if (ZCSG (restart_pending )) { /* check again, to ensure that the cache wasn't already cleaned by another process */
26772831 if (accel_is_inactive ()) {
2678- zend_accel_error (ACCEL_LOG_DEBUG , "Restarting!" );
2832+ /* All processes/threads are inactive (legacy flock check).
2833+ * We can do the cleanup immediately. */
2834+ zend_accel_error (ACCEL_LOG_DEBUG , "Restarting! (immediate — no active readers)" );
26792835 ZCSG (restart_pending ) = false;
26802836 switch ZCSG (restart_reason ) {
26812837 case ACCEL_RESTART_OOM :
@@ -2714,6 +2870,31 @@ zend_result accel_activate(INIT_FUNC_ARGS)
27142870 ZCSG (last_restart_time )++ ;
27152871 }
27162872 accel_restart_leave ();
2873+ } else if (!ZCSG (reset_deferred )) {
2874+ /* Readers are still active. Defer the actual cleanup to a
2875+ * future request end, after all current readers have left.
2876+ * This is the core of the epoch-based reclamation approach. */
2877+ zend_accel_error (ACCEL_LOG_DEBUG , "Deferring opcache restart (active readers detected)" );
2878+ ZCSG (restart_pending ) = false;
2879+
2880+ switch ZCSG (restart_reason ) {
2881+ case ACCEL_RESTART_OOM :
2882+ ZCSG (oom_restarts )++ ;
2883+ break ;
2884+ case ACCEL_RESTART_HASH :
2885+ ZCSG (hash_restarts )++ ;
2886+ break ;
2887+ case ACCEL_RESTART_USER :
2888+ ZCSG (manual_restarts )++ ;
2889+ break ;
2890+ }
2891+
2892+ ACCEL_ATOMIC_STORE_64 (& ZCSG (drain_epoch ), ACCEL_ATOMIC_LOAD_64 (& ZCSG (current_epoch )));
2893+ ACCEL_ATOMIC_INC_64 (& ZCSG (current_epoch ));
2894+
2895+ ZCSG (cache_status_before_restart ) = ZCSG (accelerator_enabled );
2896+ ZCSG (accelerator_enabled ) = false;
2897+ ZCSG (reset_deferred ) = true;
27172898 }
27182899 }
27192900 zend_shared_alloc_unlock ();
@@ -2768,6 +2949,18 @@ zend_result accel_post_deactivate(void)
27682949 return SUCCESS ;
27692950 }
27702951
2952+ /* Leave the epoch — this thread/process no longer holds references
2953+ * to shared OPcache data. */
2954+ accel_epoch_leave ();
2955+
2956+ /* If a deferred reset is pending, check if we were the last reader
2957+ * from the old epoch. If so, complete the reset now. */
2958+ if (!file_cache_only && accel_shared_globals && ZCSG (reset_deferred )) {
2959+ SHM_UNPROTECT ();
2960+ accel_try_complete_deferred_reset ();
2961+ SHM_PROTECT ();
2962+ }
2963+
27712964 zend_shared_alloc_safe_unlock (); /* be sure we didn't leave cache locked */
27722965 accel_unlock_all ();
27732966 ZCG (counted ) = false;
@@ -2911,6 +3104,9 @@ static zend_result zend_accel_init_shm(void)
29113104
29123105 zend_reset_cache_vars ();
29133106
3107+ /* Initialize epoch-based reclamation tracking */
3108+ accel_epoch_init ();
3109+
29143110 ZCSG (oom_restarts ) = 0 ;
29153111 ZCSG (hash_restarts ) = 0 ;
29163112 ZCSG (manual_restarts ) = 0 ;
@@ -2937,6 +3133,7 @@ static void accel_globals_ctor(zend_accel_globals *accel_globals)
29373133 memset (accel_globals , 0 , sizeof (zend_accel_globals ));
29383134 accel_globals -> key = zend_string_alloc (ZCG_KEY_LEN , true);
29393135 GC_MAKE_PERSISTENT_LOCAL (accel_globals -> key );
3136+ accel_globals -> epoch_slot = ACCEL_EPOCH_NO_SLOT ;
29403137}
29413138
29423139static void accel_globals_dtor (zend_accel_globals * accel_globals )
0 commit comments