insert() ) { return true; } if ( ! $this->is_stale() ) { return false; } delete_option( self::OPTION ); return $this->insert(); } /** * Gibt den Lock frei — aber nur, wenn er noch dem eigenen Lauf gehoert. * * Ein Lauf, der laenger als die TTL braucht, verliert seinen Lock an einen * nachfolgenden Prozess (acquire() raeumt ihn dann als verwaist ab). Ein * bedingungsloses delete_option() wuerde beim spaeten Aufraeumen den Lock * des Nachfolgers loeschen — und damit einen dritten Lauf freigeben, der * parallel zum zweiten arbeitet. Genau das soll diese Klasse verhindern. * * @return void */ public function release(): void { if ( '' === $this->own_value ) { return; } global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->options} WHERE option_name = %s AND option_value = %s", self::OPTION, $this->own_value ) ); $this->own_value = ''; // Der rohe DELETE geht wie der INSERT am Options-Cache vorbei. wp_cache_delete( self::OPTION, 'options' ); wp_cache_delete( 'notoptions', 'options' ); } /** * Der nackte INSERT. * * Kein ON DUPLICATE KEY UPDATE und kein vorheriges get_option() — genau * darin laege die Race. $wpdb->query() liefert false, wenn der * UNIQUE-Index den zweiten INSERT ablehnt. * * Der Wert ist ":": den Zeitstempel braucht * is_stale(), die Kennung macht den Lock einem Lauf zuordenbar, damit * release() nicht den Lock eines anderen loesen kann. * * @return bool True, wenn die Zeile geschrieben wurde. */ private function insert(): bool { global $wpdb; $value = time() . ':' . wp_generate_uuid4(); // Der abgelehnte zweite INSERT ist der Normalfall, nicht der Ausnahmefall: // Ohne diese Klammer schriebe jeder verlierende Lauf einen // "Duplicate entry"-Datenbankfehler ins Debug-Log — stuendliches Rauschen, // das wie ein Defekt aussieht. $previously_suppressed = $wpdb->suppress_errors( true ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $written = $wpdb->query( $wpdb->prepare( "INSERT INTO {$wpdb->options} ( option_name, option_value, autoload ) VALUES ( %s, %s, 'off' )", self::OPTION, $value ) ); $wpdb->suppress_errors( $previously_suppressed ); if ( ! $written ) { return false; } $this->own_value = $value; // Der rohe INSERT geht am Options-Cache vorbei; ohne dieses Aufraeumen // liefert ein spaeteres get_option() den Stand von vorher. wp_cache_delete( self::OPTION, 'options' ); wp_cache_delete( 'notoptions', 'options' ); return true; } /** * Ob der vorhandene Lock aelter als die TTL ist. * * Gelesen wird direkt aus der Datenbank, damit kein Cache-Eintrag eines * anderen Prozesses dazwischenkommt. * * @return bool */ private function is_stale(): bool { global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $value = $wpdb->get_var( $wpdb->prepare( "SELECT option_value FROM {$wpdb->options} WHERE option_name = %s", self::OPTION ) ); if ( null === $value ) { // Der Lock ist zwischen INSERT und Nachschauen verschwunden. return true; } // Vor dem Doppelpunkt steht der Zeitstempel. Ein Lock aus einer // aelteren Version traegt nur die Zahl — explode() liefert dann // denselben Wert zurueck. $parts = explode( ':', (string) $value, 2 ); return ( time() - (int) $parts[0] ) > self::TTL; } }