Issue #1891980 by Berdir, amateescu: Add Service termination API to reliable terminate/shut down services in the container.

8.0.x
catch 2013-03-07 13:13:43 +00:00
parent b0666783f5
commit a92b264dce
10 changed files with 242 additions and 24 deletions

View File

@ -14,6 +14,7 @@ use Drupal\Core\DependencyInjection\Compiler\RegisterPathProcessorsPass;
use Drupal\Core\DependencyInjection\Compiler\RegisterRouteFiltersPass;
use Drupal\Core\DependencyInjection\Compiler\RegisterRouteEnhancersPass;
use Drupal\Core\DependencyInjection\Compiler\RegisterParamConvertersPass;
use Drupal\Core\DependencyInjection\Compiler\RegisterServicesForDestructionPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Reference;
@ -302,6 +303,10 @@ class CoreBundle extends Bundle {
$container->register('plugin.manager.condition', 'Drupal\Core\Condition\ConditionManager');
$container->register('kernel_destruct_subscriber', 'Drupal\Core\EventSubscriber\KernelDestructionSubscriber')
->addMethodCall('setContainer', array(new Reference('service_container')))
->addTag('event_subscriber');
$container->addCompilerPass(new RegisterMatchersPass());
$container->addCompilerPass(new RegisterRouteFiltersPass());
// Add a compiler pass for registering event subscribers.
@ -312,6 +317,8 @@ class CoreBundle extends Bundle {
// Add a compiler pass for upcasting of entity route parameters.
$container->addCompilerPass(new RegisterParamConvertersPass());
$container->addCompilerPass(new RegisterRouteEnhancersPass());
// Add a compiler pass for registering services needing destruction.
$container->addCompilerPass(new RegisterServicesForDestructionPass());
}
/**

View File

@ -0,0 +1,34 @@
<?php
/**
* @file
* Contains \Drupal\Core\DependencyInjection\Compiler\RegisterServicesForDestructionPass.
*/
namespace Drupal\Core\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
/**
* Adds services tagged "needs_destruction" to the "kernel_destruct_subscriber"
* service.
*/
class RegisterServicesForDestructionPass implements CompilerPassInterface {
/**
* Implements \Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface::process().
*/
public function process(ContainerBuilder $container) {
if (!$container->hasDefinition('kernel_destruct_subscriber')) {
return;
}
$definition = $container->getDefinition('kernel_destruct_subscriber');
$services = $container->findTaggedServiceIds('needs_destruction');
foreach ($services as $id => $attributes) {
$definition->addMethodCall('registerService', array($id));
}
}
}

View File

@ -0,0 +1,19 @@
<?php
/**
* @file
* Contains \Drupal\Core\DestructableInterface.
*/
namespace Drupal\Core;
/**
* The interface for services that need explicit destruction.
*/
interface DestructableInterface {
/**
* Performs destruct operations.
*/
public function destruct();
}

View File

@ -0,0 +1,67 @@
<?php
/**
* @file
* Contains \Drupal\Core\EventSubscriber\KernelDestructionSubscriber.
*/
namespace Drupal\Core\EventSubscriber;
use Symfony\Component\DependencyInjection\ContainerAware;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\PostResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
/**
* Destructs services that are initiated and tagged with "needs_destruction".
*/
class KernelDestructionSubscriber extends ContainerAware implements EventSubscriberInterface {
/**
* Holds an array of service ID's that will require destruction.
*
* @var array
*/
protected $services = array();
/**
* Registers a service for destruction.
*
* Calls to this method are set up in
* RegisterServicesForDestructionPass::process().
*
* @param string $id
* Name of the service.
*/
public function registerService($id) {
$this->services[] = $id;
}
/**
* Invoked by the terminate kernel event.
*
* @param \Symfony\Component\HttpKernel\Event\PostResponseEvent $event
* The event object.
*/
public function onKernelTerminate(PostResponseEvent $event) {
foreach ($this->services as $id) {
// Check if the service was initialized during this request, destruction
// is not necessary if the service was not used.
if ($this->container->initialized($id)) {
$service = $this->container->get($id);
$service->destruct();
}
}
}
/**
* Registers the methods in this class that should be listeners.
*
* @return array
* An array of event listener definitions.
*/
static function getSubscribedEvents() {
$events[KernelEvents::TERMINATE][] = array('onKernelTerminate', 100);
return $events;
}
}

View File

@ -23,7 +23,7 @@ class AliasManager implements AliasManagerInterface {
/**
* The Key/Value Store to use for state
*
* @var \Drupal\Core\KeyValueStore\DatabaseStorage
* @var \Drupal\Core\KeyValueStore\KeyValueStoreInterface
*/
protected $state;

View File

@ -0,0 +1,68 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\DrupalKernel\ServiceDestructionTest.
*/
namespace Drupal\system\Tests\DrupalKernel;
use Drupal\simpletest\DrupalUnitTestBase;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\PostResponseEvent;
/**
* Tests the service destruction functionality.
*/
class ServiceDestructionTest extends DrupalUnitTestBase {
public static function getInfo() {
return array(
'name' => 'Service destruction',
'description' => 'Tests that services are correctly destructed.',
'group' => 'DrupalKernel',
);
}
/**
* Verifies that services are destructed when used.
*/
public function testDestructionUsed() {
// Enable the test module to add it to the container.
$this->enableModules(array('bundle_test'));
// The service has not been destructed yet.
$this->assertNull(state()->get('bundle_test.destructed'));
// Get the service destructor.
$service_destruction = $this->container->get('kernel_destruct_subscriber');
// Call the class and then invoke the kernel terminate event.
$this->container->get('bundle_test_class');
$response = new Response();
$event = new PostResponseEvent($this->container->get('kernel'), $this->container->get('request'), $response);
$service_destruction->onKernelTerminate($event);
$this->assertTrue(state()->get('bundle_test.destructed'));
}
/**
* Verifies that services are not unnecessarily destructed when not used.
*/
public function testDestructionUnused() {
// Enable the test module to add it to the container.
$this->enableModules(array('bundle_test'));
// The service has not been destructed yet.
$this->assertNull(state()->get('bundle_test.destructed'));
// Get the service destructor.
$service_destruction = $this->container->get('kernel_destruct_subscriber');
// Simulate a shutdown. The test class has not been called, so it should not
// be destructed.
$response = new Response();
$event = new PostResponseEvent($this->container->get('kernel'), $this->container->get('request'), $response);
$service_destruction->onKernelTerminate($event);
$this->assertNull(state()->get('bundle_test.destructed'));
}
}

View File

@ -19,7 +19,9 @@ class BundleTestBundle extends Bundle
{
public function build(ContainerBuilder $container) {
$container->register('bundle_test_class', 'Drupal\bundle_test\TestClass')
->addTag('event_subscriber');
->addArgument(new Reference('state'))
->addTag('event_subscriber')
->addTag('needs_destruction');
// Override a default bundle used by core to a dummy class.
$container->register('file.usage', 'Drupal\bundle_test\TestFileUsage');

View File

@ -7,11 +7,30 @@
namespace Drupal\bundle_test;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Drupal\Core\KeyValueStore\KeyValueStoreInterface;
use Drupal\Core\DestructableInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
class TestClass implements EventSubscriberInterface {
class TestClass implements EventSubscriberInterface, DestructableInterface {
/**
* The state keyvalue collection.
*
* @var \Drupal\Core\KeyValueStore\KeyValueStoreInterface
*/
protected $state;
/**
* Constructor.
*
* @param \Drupal\Core\KeyValueStore\KeyValueStoreInterface $state
* The state key value store.
*/
public function __construct(KeyValueStoreInterface $state) {
$this->state = $state;
}
/**
* A simple kernel listener method.
@ -30,4 +49,11 @@ class TestClass implements EventSubscriberInterface {
$events[KernelEvents::REQUEST][] = array('onKernelRequestTest', 100);
return $events;
}
/**
* Implements \Drupal\Core\DestructableInterface::destruct().
*/
public function destruct() {
$this->state->set('bundle_test.destructed', TRUE);
}
}

View File

@ -35,7 +35,8 @@ class ViewsBundle extends Bundle {
$container->register('views.views_data', 'Drupal\views\ViewsDataCache')
->addArgument(new Reference('cache.views_info'))
->addArgument(new Reference('config.factory'));
->addArgument(new Reference('config.factory'))
->addTag('needs_destruction');
$container->register('views.executable', 'Drupal\views\ViewExecutableFactory');

View File

@ -7,13 +7,14 @@
namespace Drupal\views;
use Drupal\Core\Config\ConfigFactory;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Config\ConfigFactory;
use Drupal\Core\DestructableInterface;
/**
* Class to manage and lazy load cached views data.
*/
class ViewsDataCache {
class ViewsDataCache implements DestructableInterface {
/**
* The base cache ID to use.
@ -241,25 +242,18 @@ class ViewsDataCache {
}
/**
* Destructs the ViewDataCache object.
* Implements \Drupal\Core\DestructableInterface::destruct().
*/
public function __destruct() {
try {
if ($this->rebuildCache && !empty($this->storage)) {
// Keep a record with all data.
$this->set($this->baseCid, $this->storage);
// Save data in seperate cache entries.
foreach ($this->storage as $table => $data) {
$cid = $this->baseCid . ':' . $table;
$this->set($cid, $data);
}
public function destruct() {
if ($this->rebuildCache && !empty($this->storage)) {
// Keep a record with all data.
$this->set($this->baseCid, $this->storage);
// Save data in seperate cache entries.
foreach ($this->storage as $table => $data) {
$cid = $this->baseCid . ':' . $table;
$this->set($cid, $data);
}
}
catch (\Exception $e) {
// During testing the table is gone before this fires.
// @todo Use terminate() instead of __destruct(), see
// http://drupal.org/node/512026.
}
}
}