Automattic\WooCommerce\Internal\BatchProcessing

BatchProcessingController::acquire_enqueued_processors_lock │ private │ WC 11.0.0

Acquire the enqueued-processors lock by claiming a row in the options table.

The claim is an atomic INSERT (which fails if the row already exists, making it a mutex); if the row exists but its stored release time has already passed, a conditional UPDATE takes the stale lock over. Failure to claim is retried up to ENQUEUED_PROCESSORS_LOCK_ATTEMPTS times with a short delay before the caller falls back to its best-effort unguarded path. Modeled on WC_Install::create_lock().

Release times are stored as fixed-width, zero-padded-fraction strings (number_format( ..., 6 )) so the database compares and matches them as plain strings without a numeric cast; for the current epoch the integer part is a stable ten digits, so lexical order matches chronological order.

Method of the class: BatchProcessingController{}

No Hooks.

Returns

string|null. The stored release time when the lock was acquired (which must be passed to release_enqueued_processors_lock()), or null when it could not be acquired.

Usage

// private - for code of main (parent) class only
$result = $this->acquire_enqueued_processors_lock(): ?string;

Changelog

Since 11.0.0 Introduced.

BatchProcessingController::acquire_enqueued_processors_lock() code WC 11.1.2

private function acquire_enqueued_processors_lock(): ?string {
	global $wpdb;

	$attempts    = max( 1, self::ENQUEUED_PROCESSORS_LOCK_ATTEMPTS );
	$delay_micro = (int) ( ( self::ENQUEUED_PROCESSORS_LOCK_TTL / $attempts ) * 1_000_000 );

	// A contended INSERT fails on the duplicate key by design, so silence wpdb error reporting to keep the
	// expected-failure path from spamming the log in debug mode; restore the prior setting when done.
	$suppress = $wpdb->suppress_errors( true );
	try {
		for ( $attempt = 0; $attempt < $attempts; $attempt++ ) {
			$time   = microtime( true );
			$now    = number_format( $time, 6, '.', '' );
			$expiry = number_format( $time + self::ENQUEUED_PROCESSORS_LOCK_TTL, 6, '.', '' );

			// The unique option_name key makes this INSERT a mutex: it fails when the lock row already exists.
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
			$acquired = $wpdb->insert(
				$wpdb->options,
				array(
					'option_name'  => self::ENQUEUED_PROCESSORS_LOCK_OPTION,
					'option_value' => $expiry,
					'autoload'     => 'no',
				),
				array( '%s', '%s', '%s' )
			);

			if ( ! $acquired ) {
				/*
				 * The lock row exists; take it over only if its stored release time is already in the past.
				 * $wpdb->update() cannot express the "<" comparison, so this conditional takeover uses raw SQL.
				 * The table is a trusted internal identifier and every value is bound through a placeholder.
				 */
				$takeover = $wpdb->prepare(
					"UPDATE {$wpdb->options} SET option_value = %s WHERE option_name = %s AND option_value < %s", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
					$expiry,
					self::ENQUEUED_PROCESSORS_LOCK_OPTION,
					$now
				);
				if ( is_string( $takeover ) ) {
					// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared
					$acquired = $wpdb->query( $takeover );
				}
			}

			if ( $acquired ) {
				return $expiry;
			}

			if ( $attempt < $attempts - 1 && $delay_micro > 0 ) {
				usleep( $delay_micro );
			}
		}

		return null;
	} finally {
		$wpdb->suppress_errors( $suppress );
	}
}