Problem/Motivation

We have services which are persisting things in __destruct(), e.g. ViewsDataCache and with #512026: Move $form_state storage from cache to new state/key-value system also the expirable key/value store. We have no control over when __destruct() gets called for services in the container. That results in issues with testing, as these terminations attempt to write caches or do garbage collection (key/value store) using tables or services that do no longer exist.

In some cases, this even seems to result in segmentation faults within PHP's GC handling.

Proposed resolution

Allow services to be tagged with needs_termination, add an event listener that terminates all services which were actually used during that request by calling the terminate() method.

Remaining tasks

-

User interface changes

-

API changes

- Services should use the needs_termination flag and implement the termination interface instead of __destruct().

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

Berdir’s picture

Extracted the service termination API related changes from the form_state issue. No other changes.

Berdir’s picture

Status: Active » Needs review

Status: Needs review » Needs work

The last submitted patch, service-termination-1891980-1.patch, failed testing.

sun’s picture

Copying over my comment from #512026-134: Move $form_state storage from cache to new state/key-value system:

any reason why we're not using Symfony\Component\HttpKernel\TerminableInterface? That's used by the Kernel for bundles that need termination already. Not sure whether it would be legit, but we could possibly fake/mock a Request and Response, in case there is none (e.g., in tests).

In light of that, at minimum, the name of Symfony's interface is also more correct for an interface. AFAIK, interfaces should always be *able or similar.

Berdir’s picture

Re-roll.

I am unsure about TerminableInterface. It already exists and agreed that the name is nicer. However, the problem I have with it is that it is about HTTP. It lives in the Symfony\Component\HttpKernel namespace and is about a Request and Response.

Why should something like a key value store, the database or a cache collector (CacheArray 2.0) have to depend on a HTTP Component?

Berdir’s picture

Status: Needs work » Needs review
FileSize
16.95 KB

Erm, now with patch.

Berdir’s picture

Renamed to TerminableInterface, as discussed in IRC.

effulgentsia’s picture

Related to #5, what will we want to do with the 'http_kernel' service when DrupalKernel is rebooted (due to module_enable() calling updateModules() on it)? Currently in HEAD, we let it get garbage collected along with all the other services, but we don't explicitly call terminate() on it, and then a new one gets instantiated in the rebuilt container. If we want to call terminate() on it before it gets destroyed, a problem we'll run into is that we don't have a $response yet. Perhaps it would be better to tag it with 'persist' so that the same 'http_kernel' instance persists into the rebuilt container? But then, what should we do with 'needs_termination' services that aren't also tagged with 'persist'? Presumably, we would need to terminate() them, but again, without a $response, and if so, then we need it to be our own interface, not Symfony's.

katbailey’s picture

I don't think it makes sense at all to use Symfony's TerminableInterface - it is currently used exclusively by the HttpKernel and its decorators (Kernel and HttpCache) for the purpose of dispatching the very event upon which our services need to act. The terminate() method takes a request and response so it can dispatch a kernel event terminating the request/response cycle. We'd have to pull these out of the $event in our listener in order to pass them on to our services needing termination even though none of them currently have any use for them... That seems pretty crazy to me.

Crell’s picture

So it sounds like what we actually need is a ShutdownableInterface (or something) that just has a shutdown() method. Then we can call that method on all known shutdownable objects from the terminate event, OR from other places where it makes sense.

Status: Needs review » Needs work

The last submitted patch, service-termination-1891980-6.patch, failed testing.

Berdir’s picture

Status: Needs work » Needs review
FileSize
749 bytes
17.44 KB

Fixed the wrong argument definition.

The patch is *not* using Symfony's TerminableInterface. I just renamed our own interface from TerminationInterface to TerminableInterface, which makes more sense for an interface.

@Crell: I don't understand why Shutdownable would be better? This happens as part of kernel termination, *not* shutdown (It can't because that shuts down bundles but does not invoke an event... why?)

@effulgentsia: Not sure I follow you either. Rebooting/building/placing a kernel does *not* invoke terminate (maybe it should?) so persist and needs_termination should not conflict.

Status: Needs review » Needs work

The last submitted patch, service-termination-1891980-12-interdiff.patch, failed testing.

Berdir’s picture

Status: Needs work » Needs review
FileSize
780 bytes
16.68 KB

Forgot a change in the key value tests. The other changes I'm not sure about, random maybe...

Status: Needs review » Needs work

The last submitted patch, service-termination-1891980-14.patch, failed testing.

Berdir’s picture

Status: Needs work » Needs review
FileSize
16.69 KB

Fixed the actions and dblog test case, a php warning because $this->services was not an array was logged to watchdog.

Berdir’s picture

effulgentsia’s picture

Rebooting/building/placing a kernel does *not* invoke terminate (maybe it should?) so persist and needs_termination should not conflict.

There's the 'kernel' service (DrupalKernel) and the 'http_kernel' service (HttpKernel). When the 'kernel' service is rebooted, it's still the same instance, so terminate() isn't and shouldn't be called on it. However, the 'http_kernel' service is not currently tagged as 'persist'. I'm not yet clear on whether we should persist it or not, but allowing it to get destroyed and replaced with a new instance without calling terminate() on the old instance seems potentially problematic. What we decide on that is relevant, because it's the http_kernel that fires the KernelEvents::TERMINATE event that we're subscribing to for terminating 'needs_termination' services.

Suppose we choose to persist 'http_kernel' and therefore, not call terminate() on it during a 'kernel' reboot. Then, any services tagged 'needs_termination', but not tagged 'persist', will get destroyed and replaced with new instances without terminate() being called on the old instances. Wouldn't that get us back to the same problem this issue is meant to solve (PHP garbage collection happening in unpredictable order)? I think we can solve that though by making DrupalKernel call terminate() itself on all such services.

I think figuring out this reboot flow can be punted to a follow up, but I bring it up, because I think it provides an additional reason for doing what this patch does in terms of introducing its own interface rather than using Symfony's TerminableInterface.

Crell’s picture

Berdir: I'm suggesting exactly what the issue summary says: Some services have a shutdown, or cleanup, or commit, or whatever method. Those need to get called at termination, but we don't want services to listen to terminate themselves because that couples them to being event listeners. So instead we have a single terminate listener that calls shutdown on all of those services.

Berdir’s picture

@Crell: Which is exactly what the patch is doing, that's why I'm not sure if you're saying the patch is good or not :)

Except we call the method and interface terminate (but it's our own method, invoked by our own custom terminate event listener and our own interface that has the same name as Symfony's interface). And terminate() makes more sense to me than shutdown(). It is happening on kernel terminate() and not shutdown() (which exists too but is something different)

Crell’s picture

Oh, now that read the patch, so it is! :-) My concern is that reusing the "terminate" name may be confusing; didn't we say above that the kernel terminate event is not synonymous with "we need to call this cleanup method on services"? And there's already a \Symfony\Component\HttpKernel\Terminatable interface. That's going to be needlessly confusing.

I'm not at all wedding to shutdown() as a name; I just think it should be something that won't be confused with the existing Symfony interface. Otherwise the patch above looks fine to me.

Berdir’s picture

Ok. IMHO, terminate makes more sense than shutdown() because it does happen during the terminate() cycle of the kernel and not the shutdown cycle (which I still don't understand). I think shutdown() would even be more confusing. And something like persist() might be too specific (key value doesn't persist something but does garbage collection).

Maybe name the interface TerminableServiceInterface to be able to more easily separate it from Symfony's TerminableInterface?

sun’s picture

I can see these options:

  1. The most simple + most concrete + most obvious: DestructableInterface + public function destruct(). That's what we're actually replacing here; i.e., __destruct() » destruct().
  2. OTOH, I already wondered whether we're perhaps supposed to override the Kernel::shutdown() method? CoreBundle::shutdown() gets already called from there.
  3. We could add a tag to all container services that need destruction, or we could also iterate over all and check instanceof DestructableInterface or ShutdownableInterface (although I don't think that's a name).
  4. In any case, we'd have to ensure that the new shutdown() / destruct() methods will be invoked for both HttpKernel terminations AND manually enforced Kernel shutdowns. Speaking of, I briefly looked, but wasn't able to figure out where and when or how Kernel::terminate() or HttpKernel::terminate() is actually registered as a PHP shutdown function... is terminate() invoked at all currently? :)
Berdir’s picture

In regards to 4: terminate() is called in index.php. It is not guaranteed that this function is called, which i the reason the lock backend went in/was updated with a drupal_register_shutdown_function() in the constructor...

Crell’s picture

The symfony approach seems to be to not trust PHP's shutdown at all (understandable), and rely on the front controller to call the terminate method on the kernel, which happens after the request is set. Basically it is our drupal_register_shutdown_function() equivalent, but with an in-this-case-undesirable HTTP object dependency.

Berdir’s picture

DestructableInterface sounds fine to me. Any arguments against that? :)

Basically it is our drupal_register_shutdown_function() equivalent

No it is not, that's the problem :) shutdown functions are called on a php fatal error too, which is important for the lock backend. terminate() is not. That's why the lock service can't use needs_termination.

sun’s picture

Sleeping over this, I'd recommend:

  1. Drupal\Core\Kernel\DestructableInterface: public function destruct()
  2. Iterate over all instantiated services, instead of tagging them.
  3. Make CoreBundle::shutdown() trigger the destruction stack, or override Kernel::shutdown() directly.
  4. Make DrupalKernel::terminate() trigger the destruction stack, after parent::terminate().
  5. Additionally register DrupalKernel::shutdown() as a standalone PHP shutdown function. Essentially the replacement for drupal_register_shutdown_function(). If this gets erroneously invoked in tests, there must have been a fatal error or similar to begin with. Instead of fixing those, we should make Lock + whatever fail-safe enough to not break horribly in case they aren't triggered on shutdown.
  6. That said, a DrupalKernel::shutdown() can always be complemented with a DrupalKernel::NoShutdownPlease() to set an internal flag on DrupalKernel to not trigger the destruction stack. More or less what drsf() is currently doing. (Current tests are copy/swapping-out the registered list of functions upon tearDown(), but I seriously doubt that this would ever swap-back-in previous/original shutdown functions. Thus, just making sure to kick out a previously registered stack that wasn't destructed should be sufficient.)
Berdir’s picture

That was some rename and reroll fun. Renamed Terminate to Destruct.

At least the following issues are now affected by this:
- #1209226: Avoid slow query for path alias whitelists (segfaults, so blocked)
- #512026: Move $form_state storage from cache to new state/key-value system (also segfaults)
- #1786490: Add caching to the state system (so far only exceptions which can be fixed with a workaround just like ViewsDataCache)
- the cache_update issue will very likely also be blocked on this as it also requires the expirable keyvalue store as a service

So it would be great to see this in ASAP.

Notes
- Not yet moved into the Core namespace as that doesn't exist and DrupalKernel would have to be moved too.
- Kept the kernel pass/tagging for now. Not yet 100% about that although it probably makes sense as we can otherwise not handle shutdown() as there is no event for that
- Can we move the shutdown function part to a separate issue? That might be tricky, it's not required to get it in and not sure if we have an agreement if we really want that.

Crell’s picture

Overall the approach here looks solid. Some comments, though: (Leaving as CNR for testbot, but these need to be addressed.)

+++ b/core/lib/Drupal/Core/DependencyInjection/Compiler/RegisterServicesForDestructionPass.php
@@ -0,0 +1,46 @@
+    // Reverse the order of tagged services to destruct them in the opposite
+    // order of which they are defined.
+    // @todo: Does this need a better system to detect references?
+    $services = array_reverse($container->findTaggedServiceIds('needs_destruction'));

The order in which services are defined in the DIC is entirely arbitrary. Who knows what people may put into their bundles. Why does the order matter? And how can we make it not matter, because I foresee that being a major point of failure?

+++ b/core/lib/Drupal/Core/DependencyInjection/Compiler/RegisterServicesForDestructionPass.php
@@ -0,0 +1,46 @@
+      $refClass = new \ReflectionClass($class);
+      $interface = 'Drupal\Core\DestructableInterface';
+      if (!$refClass->implementsInterface($interface)) {
+        throw new \InvalidArgumentException(sprintf('Service "%s" must implement interface "%s".', $id, $interface));

We don't do this sort of protection anywhere else in the DIC. I don't think we need to here, especially since it means loading all distructable classes in order to do so. We can just leave this out.

+++ b/core/lib/Drupal/Core/EventSubscriber/KernelDestructionSubscriber.php
@@ -0,0 +1,74 @@
+  /**
+   * Invoked by the terminate kernel event.
+   *
+   * @param \Symfony\Component\HttpKernel\Event\PostResponseEvent $event
+   *   The event object.
+   */
+  public function onKernelTerminate(PostResponseEvent $event) {
+    $this->destructIniatedServices();
+  }
+
+  /**
+   * Destructs registered services if necessary.
+   */
+  protected function destructIniatedServices() {
+    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();
+      }
+    }
+  }

Why separate these? Especially as destructInitiatedServices (which is misspelled) is protected, it's useless elsewhere. This code is trivial enough to just go in onKernelTerminate.

+++ b/core/modules/system/lib/Drupal/system/Tests/DrupalKernel/ServiceDestructionTest.php
@@ -0,0 +1,53 @@
+  /**
+   * Verifies that services are correctly destructed.
+   */
+  public function testDestruction() {
+    // 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 destruction.
+    $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'));
+
+    // Now call the class and then invoke the kernel terminate event again.
+    $this->container->get('bundle_test_class');
+    $service_destruction->onKernelTerminate($event);
+    $this->assertTrue(state()->get('bundle_test.destructed'));

This is in DUTB, not WebTest. Please break it up to 2 separate test methods. Please. With sugar on top.

damiankloip’s picture

Most of Crell's points are things I noticed aswell, so +1 for those. Just a few more doc nits...

+++ b/core/lib/Drupal/Core/DependencyInjection/Compiler/RegisterServicesForDestructionPass.phpundefined
@@ -0,0 +1,46 @@
+   * Implements CompilerPassInterface::process().

Does this need the full namespace?

+++ b/core/lib/Drupal/Core/EventSubscriber/KernelDestructionSubscriber.phpundefined
@@ -0,0 +1,74 @@
+ * This destructs iniated services tagged with needs_destruction

typo here.

+++ b/core/lib/Drupal/Core/EventSubscriber/KernelDestructionSubscriber.phpundefined
@@ -0,0 +1,74 @@
+   * Registers a a service for destruction.

Just one a is fine :)

+++ b/core/modules/system/lib/Drupal/system/Tests/DrupalKernel/ServiceDestructionTest.phpundefined
@@ -0,0 +1,53 @@
+    // Get the service destruction.

This comment sounds weird.

+++ b/core/modules/views/lib/Drupal/views/ViewsDataCache.phpundefined
@@ -241,25 +242,18 @@ public function fetchBaseTables() {
+  public function destruct() {

I would like to get some test coverage to actually test this is cached as expected in ViewsDataCache. But this is definitely a follow up for me to get on with and not something that should be tackled in this issue.

Berdir’s picture

Thanks for the reviews. What about doing this directly in the kernel (which would be the only way to support shutdown() too) vs. tag/compilation pass/event listener.

@Crell: Uh, there might be a misunderstanding here but the method is splitted because of you: #512026-117: Move $form_state storage from cache to new state/key-value system:

1) We should probably split the "do I need to do any termination?" logic out to a separate method for tidiness. Making services integrate that into a single method seems wrong.

Didn't you mean that method with that?

Can change the DUBT test unless you intend to tomorrow request that I should merge it again :p

sun’s picture

Thanks for resurrecting this, @Berdir.

The @todo about the destruction order is concerning. AFAICS, to destruct all services in the right order of dependencies, we'd have to implement Graph to determine the actual dependencies, based on the service Definitions that are passed via References. I wonder whether there isn't a concept for that in ContainerBuilder already?

Berdir’s picture

I don't think ContainerBuilder supports that.

As discussed in the original issue, I'd prefer to improve the dependency detection stuff in a follow-up issue. It shouldn't be too hard I hope and we have a graph component but it's not something that is currently blocking us, it will only start to get interesting contrib starts to provide and alter service definitions.

Crell’s picture

Berdir: :-P

My statement in the other issue was suggesting that the service that implements "Terminate me!" have two methods: "I will need to do cleanup" and "clean me up".

My statement above is that the event listener that is calling all of the various things that need to be terminated doesn't need to split the loop out of the listener method itself. It's still simple glue code either way.

A test method should test one and only one thing. I've been very consistent on that for years. :-) We frequently violate that in WebTest because of how expensive it is to re-initialize WebTest for a new test method, but that doesn't mean it's not still wrong to do so.

Let's leave out all of the ordering and dependency stuff for now, including the array_reverse() call. That is only useful if we assume that services in CoreBundle are in dependent order, and I don't think that's even true now.

Berdir’s picture

Ah, that was a misunderstanding then :)

Not quite sure about the "I will need to do cleanup". What would be an example where this would be useful? We already only check instantiated services and if a service has nothing to terminate when being called, he can always return without doing anything. Adding an additional check would just mean another method call.

Crell’s picture

It's not a hard requirement, I suppose. Thinking about it further it may not make sense here. It just seemed odd to have a lot of if-wrapped methods.

Berdir’s picture

Ok, here we go.

- Removed array_reverse() and the interface check there and opened #1924166: Improve dependency handling in service destructor
- Merged the method in the subscriber
- Split the test methods into two. Agreed that this looks nicer.
- Fixed the comment issues.

amateescu’s picture

Status: Needs review » Reviewed & tested by the community
FileSize
3.65 KB
13.9 KB

Found a few more doc issues and fixed them in the patch attached. I believe all the concerns have been adressed in the patch from #37, so RTBC.

Berdir’s picture

Note: I re-rolled both #1786490: Add caching to the state system and #1209226: Avoid slow query for path alias whitelists to use this and both patches came back green.

catch’s picture

Status: Reviewed & tested by the community » Fixed

This looks good to me. We'll have to revisit the dependencies, but it's not making anything worse than using __destruct(). Committed/pushed to 8.x.

Automatically closed -- issue fixed for 2 weeks with no activity.