Commit 35c3429c authored by catch's avatar catch
Browse files

Issue #2004370 by xjm, alexpott, Dean Reilly: Batch the configuration synchronisation process.

parent c210e868
......@@ -411,7 +411,7 @@ function _batch_finished() {
if (is_callable($batch_set['finished'])) {
$queue = _batch_queue($batch_set);
$operations = $queue->getAllItems();
$batch_set['finished']($batch_set['success'], $batch_set['results'], $operations, format_interval($batch_set['elapsed'] / 1000));
call_user_func_array($batch_set['finished'], array($batch_set['success'], $batch_set['results'], $operations, format_interval($batch_set['elapsed'] / 1000)));
}
}
}
......
<?php
/**
* @file
* Contains \Drupal\Core\Config\BatchConfigImporter.
*/
namespace Drupal\Core\Config;
/**
* Defines a batch configuration importer.
*
* @see \Drupal\Core\Config\ConfigImporter
*/
class BatchConfigImporter extends ConfigImporter {
/**
* Initializes the config importer in preparation for processing a batch.
*/
public function initialize() {
// Ensure that the changes have been validated.
$this->validate();
if (!$this->lock->acquire(static::ID)) {
// Another process is synchronizing configuration.
throw new ConfigImporterException(sprintf('%s is already importing', static::ID));
}
$this->totalToProcess = 0;
foreach(array('create', 'delete', 'update') as $op) {
$this->totalToProcess += count($this->getUnprocessed($op));
}
}
/**
* Processes batch.
*
* @param array $context.
* The batch context.
*/
public function processBatch(array &$context) {
$operation = $this->getNextOperation();
if (!empty($operation)) {
$this->process($operation['op'], $operation['name']);
$context['message'] = t('Synchronizing @name.', array('@name' => $operation['name']));
$context['finished'] = $this->batchProgress();
}
else {
$context['finished'] = 1;
}
if ($context['finished'] >= 1) {
$this->notify('import');
// The import is now complete.
$this->lock->release(static::ID);
$this->reset();
}
}
/**
* Gets percentage of progress made.
*
* @return float
* The percentage of progress made expressed as a float between 0 and 1.
*/
protected function batchProgress() {
$processed_count = count($this->processed['create']) + count($this->processed['delete']) + count($this->processed['update']);
return $processed_count / $this->totalToProcess;
}
/**
* Gets the next operation to perform.
*
* @return array|bool
* An array containing the next operation and configuration name to perform
* it on. If there is nothing left to do returns FALSE;
*/
protected function getNextOperation() {
foreach(array('create', 'delete', 'update') as $op) {
$names = $this->getUnprocessed($op);
if (!empty($names)) {
return array(
'op' => $op,
'name' => array_shift($names),
);
}
}
return FALSE;
}
}
......@@ -7,6 +7,7 @@
namespace Drupal\Core\Config;
use Drupal\Core\DependencyInjection\DependencySerialization;
use Drupal\Core\Lock\LockBackendInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
......@@ -29,7 +30,7 @@
*
* @see \Drupal\Core\Config\ConfigImporterEvent
*/
class ConfigImporter {
class ConfigImporter extends DependencySerialization {
/**
* The name used to identify events and the lock.
......@@ -204,8 +205,15 @@ public function import() {
// Another process is synchronizing configuration.
throw new ConfigImporterException(sprintf('%s is already importing', static::ID));
}
$this->importInvokeOwner();
$this->importConfig();
// First pass deleted, then new, and lastly changed configuration, in order
// to handle dependencies correctly.
// @todo Implement proper dependency ordering using
// https://drupal.org/node/2080823
foreach (array('delete', 'create', 'update') as $op) {
foreach ($this->getUnprocessed($op) as $name) {
$this->process($op, $name);
}
}
// Allow modules to react to a import.
$this->notify('import');
......@@ -234,25 +242,40 @@ public function validate() {
}
/**
* Writes an array of config changes from the source to the target storage.
* Processes a configuration change.
*
* @param string $op
* The change operation.
* @param string $name
* The name of the configuration to process.
*/
protected function importConfig() {
foreach (array('delete', 'create', 'update') as $op) {
foreach ($this->getUnprocessed($op) as $name) {
$config = new Config($name, $this->storageComparer->getTargetStorage(), $this->eventDispatcher, $this->typedConfigManager);
if ($op == 'delete') {
$config->delete();
}
else {
$data = $this->storageComparer->getSourceStorage()->read($name);
$config->setData($data ? $data : array());
$config->save();
}
$this->setProcessed($op, $name);
}
protected function process($op, $name) {
if (!$this->importInvokeOwner($op, $name)) {
$this->importConfig($op, $name);
}
}
/**
* Writes a configuration change from the source to the target storage.
*
* @param string $op
* The change operation.
* @param string $name
* The name of the configuration to process.
*/
protected function importConfig($op, $name) {
$config = new Config($name, $this->storageComparer->getTargetStorage(), $this->eventDispatcher, $this->typedConfigManager);
if ($op == 'delete') {
$config->delete();
}
else {
$data = $this->storageComparer->getSourceStorage()->read($name);
$config->setData($data ? $data : array());
$config->save();
}
$this->setProcessed($op, $name);
}
/**
* Invokes import* methods on configuration entity storage controllers.
*
......@@ -260,37 +283,43 @@ protected function importConfig() {
* configuration data.
*
* @todo Add support for other extension types; e.g., themes etc.
*
* @param string $op
* The change operation to get the unprocessed list for, either delete,
* create or update.
* @param string $name
* The name of the configuration to process.
*
* @return bool
* TRUE if the configuration was imported as a configuration entity. FALSE
* otherwise.
*/
protected function importInvokeOwner() {
// First pass deleted, then new, and lastly changed configuration, in order
// to handle dependencies correctly.
foreach (array('delete', 'create', 'update') as $op) {
foreach ($this->getUnprocessed($op) as $name) {
// Call to the configuration entity's storage controller to handle the
// configuration change.
$handled_by_module = FALSE;
// Validate the configuration object name before importing it.
// Config::validateName($name);
if ($entity_type = $this->configManager->getEntityTypeIdByName($name)) {
$old_config = new Config($name, $this->storageComparer->getTargetStorage(), $this->eventDispatcher, $this->typedConfigManager);
if ($old_data = $this->storageComparer->getTargetStorage()->read($name)) {
$old_config->initWithData($old_data);
}
$data = $this->storageComparer->getSourceStorage()->read($name);
$new_config = new Config($name, $this->storageComparer->getTargetStorage(), $this->eventDispatcher, $this->typedConfigManager);
if ($data !== FALSE) {
$new_config->setData($data);
}
$method = 'import' . ucfirst($op);
$handled_by_module = $this->configManager->getEntityManager()->getStorageController($entity_type)->$method($name, $new_config, $old_config);
}
if (!empty($handled_by_module)) {
$this->setProcessed($op, $name);
}
protected function importInvokeOwner($op, $name) {
// Call to the configuration entity's storage controller to handle the
// configuration change.
$handled_by_module = FALSE;
// Validate the configuration object name before importing it.
// Config::validateName($name);
if ($entity_type = $this->configManager->getEntityTypeIdByName($name)) {
$old_config = new Config($name, $this->storageComparer->getTargetStorage(), $this->eventDispatcher, $this->typedConfigManager);
if ($old_data = $this->storageComparer->getTargetStorage()->read($name)) {
$old_config->initWithData($old_data);
}
$data = $this->storageComparer->getSourceStorage()->read($name);
$new_config = new Config($name, $this->storageComparer->getTargetStorage(), $this->eventDispatcher, $this->typedConfigManager);
if ($data !== FALSE) {
$new_config->setData($data);
}
$method = 'import' . ucfirst($op);
$handled_by_module = $this->configManager->getEntityManager()->getStorageController($entity_type)->$method($name, $new_config, $old_config);
}
if (!empty($handled_by_module)) {
$this->setProcessed($op, $name);
return TRUE;
}
return FALSE;
}
/**
......
......@@ -11,9 +11,8 @@
use Drupal\Core\Form\FormBase;
use Drupal\Core\Config\StorageInterface;
use Drupal\Core\Lock\LockBackendInterface;
use Drupal\Core\Config\BatchConfigImporter;
use Drupal\Core\Config\StorageComparer;
use Drupal\Core\Config\ConfigImporter;
use Drupal\Core\Config\ConfigException;
use Drupal\Core\Config\TypedConfigManager;
use Drupal\Core\Routing\UrlGeneratorInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
......@@ -218,7 +217,7 @@ public function buildForm(array $form, array &$form_state) {
* {@inheritdoc}
*/
public function submitForm(array &$form, array &$form_state) {
$config_importer = new ConfigImporter(
$config_importer = new BatchConfigImporter(
$form_state['storage_comparer'],
$this->eventDispatcher,
$this->configManager,
......@@ -229,21 +228,59 @@ public function submitForm(array &$form, array &$form_state) {
drupal_set_message($this->t('Another request may be synchronizing configuration already.'));
}
else{
try {
$config_importer->import();
drupal_flush_all_caches();
drupal_set_message($this->t('The configuration was imported successfully.'));
}
catch (ConfigException $e) {
// Return a negative result for UI purposes. We do not differentiate
// between an actual synchronization error and a failed lock, because
// concurrent synchronizations are an edge-case happening only when
// multiple developers or site builders attempt to do it without
// coordinating.
watchdog_exception('config_import', $e);
drupal_set_message($this->t('The import failed due to an error. Any errors have been logged.'), 'error');
}
$config_importer->initialize();
$batch = array(
'operations' => array(
array(array(get_class($this), 'processBatch'), array($config_importer)),
),
'finished' => array(get_class($this), 'finishBatch'),
'title' => t('Synchronizing configuration'),
'init_message' => t('Starting configuration synchronization.'),
'progress_message' => t('Synchronized @current configuration files out of @total.'),
'error_message' => t('Configuration synchronization has encountered an error.'),
'file' => drupal_get_path('module', 'config') . '/config.admin.inc',
);
batch_set($batch);
}
}
/**
* Processes the config import batch and persists the importer.
*
* @param BatchConfigImporter $config_importer
* The batch config importer object to persist.
* @param $context
* The batch context.
*/
public static function processBatch(BatchConfigImporter $config_importer, &$context) {
if (!isset($context['sandbox']['config_importer'])) {
$context['sandbox']['config_importer'] = $config_importer;
}
$config_importer = $context['sandbox']['config_importer'];
$config_importer->processBatch($context);
}
/**
* Finish batch.
*
* This function is a static function to avoid serialising the ConfigSync
* object unnecessarily.
*/
public static function finishBatch($success, $results, $operations) {
if ($success) {
drupal_set_message(\Drupal::translation()->translate('The configuration was imported successfully.'));
}
else {
// An error occurred.
// $operations contains the operations that remained unprocessed.
$error_operation = reset($operations);
$message = \Drupal::translation()->translate('An error occurred while processing %error_operation with arguments: @arguments', array('%error_operation' => $error_operation[0], '@arguments' => print_r($error_operation[1], TRUE)));
drupal_set_message($message, 'error');
}
drupal_flush_all_caches();
}
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment