Follow-up to #2281903: Refactor DrupalKernel::initializeContainer in preparation of a replacement for persist services

Problem/Motivation

Recording and restoring service state should be pluggable and not hard-coded into DrupalKernel. Also persist-services did lead to many problems during the 8.x development cycle.

Proposed resolution

Introduce a container.state_recorder capable of recording container state before a container-rebuild and restoring it afterwards.

Remaining tasks

User interface changes

API changes

Comments

Status: Needs review » Needs work

The last submitted patch, 1: 2285621-replace-persist-with-service-state.diff, failed testing.

alexpott’s picture

I don't get all the complexity here. Why don't we just have an interface that services can implement that as the methods getState() and restoreState(). In DrupalKernel we can detect services that implement this interface and store their state using getState() and once the container is rebuilt use restoreState() to get the service into the correct state. For synthetic services we could just detect them and copy them in a similar way we do for persistent - although will we have any synthetic services once the request is removed?

znerol’s picture

We cannot delegate the responsibility for maintaining the state to services themselves because of the following reasons:

  1. Synthetic services cannot be instantiated. If we'd delegate the responsibility of maintaining state to services, we would have to special case that. This is no improvement compared to the current situation.
  2. We want to reuse third-party components unmodified if possible. If for some reason a Symfony service needs to keep its state across container rebuilds, we'd have to subclass it only to implement service state maintenance.
  3. The mechanism should be flexible enough such that one state-recorder can handle multiple services. Otherwise the logic might get fragmented too much which actually might increase complexity compared to the current implementation as well as compared to the proposed approach.

As an example of how this patch actually reduces complexity, here is DrupalKernel::initializeContainer() afterwards:

  /**
   * Initializes the service container.
   */
  protected function initializeContainer() {
    $this->containerNeedsDumping = FALSE;

    // Construct the new container.
    if ($this->allowLoading) {
      $container = $this->loadContainer();
    }
    if (!isset($container)) {
      $container = $this->buildContainer();

      if ($this->allowDumping) {
        $this->containerNeedsDumping = TRUE;
      }
    }

    $this->container = $container;
    \Drupal::setContainer($this->container);
  }

Also it should be mentioned, that state-recording can be used if it is necessary to instantiate a whole new container (like for example in WebTestBase):

+++ b/core/modules/simpletest/src/WebTestBase.php
@@ -1102,35 +1103,16 @@ protected function writeCustomTranslations() {
   protected function rebuildContainer($environment = 'prod') {
-    // Preserve the request object after the container rebuild.
-    $request = \Drupal::request();
-    // When called from InstallerTestBase, the current container is the minimal
-    // container from TestBase::prepareEnvironment(), which does not contain a
-    // request stack.
-    if (\Drupal::getContainer()->initialized('request_stack')) {
-      $request_stack = \Drupal::service('request_stack');
-    }
+    $state_recorder = new ContainerStateRecorder();
+    $state_recorder->setContainer(\Drupal::getContainer());
+    $state = $state_recorder->save();
 
     $this->kernel = new DrupalKernel($environment, drupal_classloader(), TRUE, FALSE);
-    $this->kernel->boot();
+    $this->kernel->boot($state);
+
     // DrupalKernel replaces the container in \Drupal::getContainer() with a
     // different object, so we need to replace the instance on this test class.
     $this->container = \Drupal::getContainer();
-    // The current user is set in TestBase::prepareEnvironment().
-    $this->container->set('request', $request);
-    if (isset($request_stack)) {
-      $this->container->set('request_stack', $request_stack);
-    }
-    else {
-      $this->container->get('request_stack')->push($request);
-    }
-    $this->container->get('current_user')->setAccount(\Drupal::currentUser());
-
-    // The request context is normally set by the router_listener from within
-    // its KernelEvents::REQUEST listener. In the simpletest parent site this
-    // event is not fired, therefore it is necessary to updated the request
-    // context manually here.
-    $this->container->get('router.request_context')->fromRequest($request);
znerol’s picture

Status: Needs work » Needs review
StatusFileSize
new33.53 KB
new5.63 KB

Ids of synthetic services need to be collected during a compiler pass.

Status: Needs review » Needs work

The last submitted patch, 5: 2285621-replace-persist-with-service-state-5.diff, failed testing.

znerol’s picture

Status: Needs work » Needs review
StatusFileSize
new33.06 KB

Status: Needs review » Needs work

The last submitted patch, 7: 2285621-replace-persist-with-service-state-7.diff, failed testing.

znerol’s picture

Status: Needs work » Needs review
StatusFileSize
new33.92 KB

Need to ensure that request_stack is also in the container from TestBase, now that WebTestBase::rebuildContainer does not special case maintenance of the container state anymore.

fabpot’s picture

My 2 cents here: The idea of persisting services is flawed and very dangerous. It should be removed altogether and not replaced with anything.

As of today, there are 7 services that are "persisted" between container rebuilds:

  • class_loader
  • kernel
  • service_container
  • container.namespaces
  • request_stack
  • router.request_context
  • session_manager

Only the first three ones are synthetic services.

Persisting service_container is very wrong as the new container will then get the old container in the service_container service. So, whenever a service depends on the service_container service, it's going to receive the old container, not the new one... or I'm missing something here.

Persisting the kernel is not needed as it's already done when building the container.

The container.namespaces is not a service and should be removed (as mentioned in other issues.)

The session_manager is also very interesting. As it depends on many other objects, it will keep the objects from the old container, not the new ones as those dependencies are not persisted.

So, my point is that whatever you do here, it's going to be wrong for most cases. That's why instead of trying to replace it, it should be removed. From my understanding, persisted objects are only useful when some modules are added from the UI (and updateModules() is called). But in this case, why not flush the cache and immediately redirect the user so that you get a fresh container for the very next request?

I'm pretty sure I'm missing something here, but as to me, this is a major flaw in the way the container is managed currently.

znerol’s picture

  • class_loader
  • kernel
  • service_container

They actually always contain fresh instances. The persist logic checks whether a service is already initialized, if yes it is not restored (overwritten) with the old instance.

  • container.namespaces

Not anymore as of #2277481: Remove persist flag from container.namespaces service.

The session_manager is also very interesting. As it depends on many other objects, it will keep the objects from the old container, not the new ones as those dependencies are not persisted.

This is exactly the reason why I'm working on this issue in the first place, see #2272987: Do not persist session manager.

From my understanding, persisted objects are only useful when some modules are added from the UI (and updateModules() is called)

Regrettably the installer also relies on that, and because the test-runner (UI and CLI) relies on the installer, all of the integration tests depend on a mechanism capable of persisting state of some services across container rebuilds.

olli’s picture

Re #10

The container.namespaces is not a service and should be removed (as mentioned in other issues.)

Filed #2286291: Remove container.namespaces service.

alexpott’s picture

Totally agree with @fabpot - I've been trying to remove persisted services for ages. I think we should move forward with the latest patch on #2272987: Do not persist session manager and once that is in just remove persisted services. I've proved in earlier issues (before the session manager was a service) that we don't need it.

catch’s picture

Getting rid of persisted services altogether sounds great if we can do it.

znerol’s picture

Status: Needs review » Closed (won't fix)