cms_content_sync-3.0.x-dev/src/Controller/UpdateLock.php
src/Controller/UpdateLock.php
<?php
namespace Drupal\cms_content_sync\Controller;
use Drupal\Core\Controller\ControllerBase;
/**
* Class UpdateLock.
*
* Make sure we never try to update the same entity simultaneously. This can
* happen if e.g. the execution time of PHP is at 5 minutes and our request
* timeout is at 1 minute. In this case the update request will fail after 1
* minute and the Sync Core will retry the update between 1-60 seconds later.
* But as the execution limit is 5 minutes, the previous update is actually
* still running, so now we have two updates on the same entity running
* simultaneously, leading to unexpected behavior.
* Updates can go missing, references like paragraphs can be removed etc.
* It's also hard to recover from if the "skip-unchanged" optimization is on,
* because we assume that the previous update was a success even though it left
* the content in a broken state.
*/
class UpdateLock extends ControllerBase {
/**
* @var string COLLECTION_NAME
* The prefix to use for saving entity update state.
*/
public const COLLECTION_NAME = 'cms_content_sync:update_lock';
/**
* @var string COLLECTION_NAME
* The prefix to use for saving entity update state.
*/
public const CACHE_TAG_UPDATE_LOCK = 'cms_content_sync:update_lock';
/**
* When the lock on an entity expires automatically, in seconds, assuming that
* the previous update failed or was cancelled.
* Defaults to the max execution time or 60 if none is given.
*
* Can be set by environment variable, e.g.:
* CMS_CONTENT_SYNC_UPDATE_LOCK_EXPIRATION="120"
*
* @return int
*/
public static function getLockExpiration() {
static $value = NULL;
if ($value !== NULL) {
return $value;
}
$env = getenv('CMS_CONTENT_SYNC_UPDATE_LOCK_EXPIRATION');
if ($env && (int) $env) {
return $value = (int) $env;
}
return $value = (int) ini_get('max_execution_time') ?? 60;
}
protected static $locked = [];
protected static $last_lock = NULL;
/**
*
*/
public static function lock($entity_type_id, $shared_entity_id) {
$cache = \Drupal::cache();
$cache_item_id = self::COLLECTION_NAME . ':' . $entity_type_id . ':' . $shared_entity_id;
$cache_item_data = new \stdClass();
$cache_item_data->lock = time();
$expiration = time() + self::getLockExpiration();
$cache->set($cache_item_id, $cache_item_data, $expiration, [self::CACHE_TAG_UPDATE_LOCK]);
self::renew();
if (!in_array($cache_item_id, UpdateLock::$locked)) {
UpdateLock::$locked[] = $cache_item_id;
}
}
/**
*
*/
public static function isLocked($entity_type_id, $shared_entity_id) {
$cache = \Drupal::cache();
$cache_item_id = self::COLLECTION_NAME . ':' . $entity_type_id . ':' . $shared_entity_id;
$cache_item = $cache->get($cache_item_id);
return !!$cache_item;
}
/**
*
*/
public static function unlock($entity_type_id, $shared_entity_id) {
$cache = \Drupal::cache();
$cache_item_id = self::COLLECTION_NAME . ':' . $entity_type_id . ':' . $shared_entity_id;
$cache->delete($cache_item_id);
UpdateLock::$locked = array_diff(UpdateLock::$locked, [$cache_item_id]);
}
/**
*
*/
public static function renew() {
$now = time();
$expire_after = self::getLockExpiration();
// Locks have recently been renewed (<20% of expiry passed).
if (self::$last_lock && self::$last_lock > $now - $expire_after / 5) {
return;
}
$cache = \Drupal::cache();
$cache_item_data = new \stdClass();
$cache_item_data->lock = time();
$expiration = $now + $expire_after;
foreach (UpdateLock::$locked as $cache_item_id) {
$cache->set($cache_item_id, $cache_item_data, $expiration, [self::CACHE_TAG_UPDATE_LOCK]);
}
self::$last_lock = $now;
}
}
