8000 [FrameworkBundle] add config option for resetting services by dmaicher · Pull Request #24554 · symfony/symfony · GitHub
[go: up one dir, main page]

Skip to content

[FrameworkBundle] add config option for resetting services #24554

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 5 commits into from

Conversation

dmaicher
Copy link
Contributor
@dmaicher dmaicher commented Oct 13, 2017
Q A
Branch? 3.4
Bug fix? yes
New feature? yes
BC breaks? no
Deprecations? no
Tests pass? no
Fixed tickets #24552
License MIT
Doc PR todo

As mentioned in #24552 the new resettable services feature has some BC breaks when it comes to functional tests and accessing services from the client's container after performing a request.

This PR makes the feature opt-in so it can be enabled if its needed.

TODOs:

  • fix tests
  • add tests
  • update changelog
  • update docs

@@ -107,6 +107,7 @@ public function getConfigTreeBuilder()
->beforeNormalization()->ifString()->then(function ($v) { return array($v); })->end()
->prototype('scalar')->end()
->end()
->booleanNode('reset_services')->defaultFalse()->end()
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

any better idea for the name?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

reset_services_on_terminate?
no more comment :)

@dmaicher
Copy link
Contributor Author

@nicolas-grekas @derrabus @xabbuh any comments before I tackle the remaining todos?

@nicolas-grekas nicolas-grekas added this to the 3.4 milestone Oct 14, 2017
@@ -317,6 +318,10 @@ public function load(array $configs, ContainerBuilder $container)
$loader->load('web_link.xml');
}

if (!$config['reset_services']) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of registering and removing the service, I'd rather invert that condition and only register the service if the feature has been enabled.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense 👍

@dmaicher
Copy link
Contributor Author

Should I work on the documentation PR? Any more comments here? How do we move forward? 😊

@@ -317,6 +318,10 @@ public function load(array $configs, ContainerBuilder $container)
$loader->load('web_link.xml');
}

if ($config['reset_services_on_terminate']) {
$container->register(ServiceResetListener::class)->addTag('kernel.event_subscriber');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

->setPublic(false)

@@ -74,11 +74,5 @@
<service id="Symfony\Component\Config\Resource\SelfCheckingResourceChecker">
<tag name="config_cache.resource_checker" priority="-990" />
</service>

<service id="Symfony\Component\HttpKernel\EventListener\ServiceResetListener">
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As discussed in #24634, we should keep that in xml, and remove it in the pass (sorry for the back and forth)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok no problem 😉 Done.

@nicolas-grekas
Copy link
Member

I'm trying to figure out a way to not add any option (because any option makes Symfony harder to use).
What about resetting service at the very beginning, early on "kernel.request" instead (but only on the 2nd master request?) Could you check if that would also fix your issue with tests @dmaicher?

@dmaicher
Copy link
Contributor Author

@nicolas-grekas interesting idea! Testing it now 😉

@dmaicher
Copy link
Contributor Author

@nicolas-grekas my test problems are fixed!

I tried this:

class ServiceResetListener implements EventSubscriberInterface
{
    private $services;
    private $resetMethods;
    private $calledOnMasterRequest = false;

    public function __construct(\Traversable $services, array $resetMethods)
    {
        $this->services = $services;
        $this->resetMethods = $resetMethods;
    }

    public function onKernelRequest(GetResponseEvent $e)
    {
        if (!$e->isMasterRequest()) {
            return;
        }

        if ($this->calledOnMasterRequest) {
            foreach ($this->services as $id => $service) {
                $method = $this->resetMethods[$id];
                $service->$method();
            }
        } else {
            $this->calledOnMasterRequest = true;
        }
    }

    /**
     * {@inheritdoc}
     */
    public static function getSubscribedEvents()
    {
        return array(
            KernelEvents::REQUEST => array('onKernelRequest', 2048),
        );
    }
}

However to simulate two master requests being handled this should work in theory in my app.php, right?

$request = Request::createFromGlobals();
$kernel->handle($request);
$response = $kernel->handle($request);
$response->send();

I'm getting a weird error:

LogicException:
Event "__section__" is not started.

  at vendor/symfony/symfony/src/Symfony/Component/Stopwatch/Section.php:151
  at Symfony\Component\Stopwatch\Section->stopEvent('__section__')
     (vendor/symfony/symfony/src/Symfony/Component/Stopwatch/Stopwatch.php:130)
  at Symfony\Component\Stopwatch\Stopwatch->stop('__section__')
     (vendor/symfony/symfony/src/Symfony/Component/Stopwatch/Stopwatch.php:86)
  at Symfony\Component\Stopwatch\Stopwatch->stopSection('68d560')
     (vendor/symfony/symfony/src/Symfony/Component/HttpKernel/Debug/TraceableEventDispatcher.php:69)
  at Symfony\Component\HttpKernel\Debug\TraceableEventDispatcher->postDispatch('kernel.response', object(FilterResponseEvent))
     (vendor/symfony/symfony/src/Symfony/Component/EventDispatcher/Debug/TraceableEventDispatcher.php:150)
  at Symfony\Component\EventDispatcher\Debug\TraceableEventDispatcher->dispatch('kernel.response', object(FilterResponseEvent))
     (vendor/symfony/symfony/src/Symfony/Component/HttpKernel/HttpKernel.php:193)
  at Symfony\Component\HttpKernel\HttpKernel->filterResponse(object(Response), object(Request), 1)
     (vendor/symfony/symfony/src/Symfony/Component/HttpKernel/HttpKernel.php:175)
  at Symfony\Component\HttpKernel\HttpKernel->handleRaw(object(Request), 1)
     (vendor/symfony/symfony/src/Symfony/Component/HttpKernel/HttpKernel.php:68)
  at Symfony\Component\HttpKernel\HttpKernel->handle(object(Request), 1, true)
     (vendor/symfony/symfony/src/Symfony/Component/HttpKernel/Kernel.php:189)
  at Symfony\Component\HttpKernel\Kernel->handle(object(Request))
     (web/app.php:41)

I'm guessing something is reset too early now maybe?

@nicolas-grekas
Copy link
Member

@derrabus WDYT here?

@dmaicher
Copy link
Contributor Author

Ok so I understand the problem now. The StopWatch service is also resettable.

So on the second master request the TraceableEventDispatcher starts the stopwatch to track the request time. But after starting it our ServiceResetListener now resets it. So stopping the watch results in an error.

Catching the exception works fine:

case KernelEvents::RESPONSE:
    $token = $event->getResponse()->headers->get('X-Debug-Token');
    try {
        $this->stopwatch->stopSection($token);
    } catch (\LogicException $e) {
    }
    break;

This means that we cannot correctly profile the total master request time now for master requests > 1 though. Is that a problem though?

Otherwise we need to reset the StopWatch maybe inside TraceableEventDispatcher? 😕

In general I like this new idea though.

@derrabus
Copy link
Member
derrabus commented Oct 20, 2017

@nicolas-grekas If we want to get the service reset off the terminate event, I see two options. Either we do the reset as the first thing in Kernel::handle() (should be a no-op on the first request) or we add a reset method to the kernel itself.

@nicolas-grekas
Copy link
Member

@derrabus why not make that early request listener?

@derrabus
Copy link
Member

Because we profile the event dispatcher. If we reset at the beginning of the request, we need to do it before we use the event dispatcher.

@dmaicher
Copy link
Contributor Author
dmaicher commented Oct 22, 2017

@nicolas-grekas @derrabus actually also the current solution merged into 3.4 makes it impossible to profile the kernel.terminate event, right?

What if we have a ServiceResetter or similar and invoke it before handling the second master request within the Kernel?

Something like this:

diff --git a/src/Symfony/Component/HttpKernel/Kernel.php b/src/Symfony/Component/HttpKernel/Kernel.php
index 6f4f2b7..8b2efa4 100644
--- a/src/Symfony/Component/HttpKernel/Kernel.php
+++ b/src/Symfony/Component/HttpKernel/Kernel.php
@@ -186,6 +186,10 @@ abstract class Kernel implements KernelInterface, RebootableInterface, Terminabl
             $this->boot();
         }
 
+        if ($type === HttpKernelInterface::MASTER_REQUEST && $handledMasterRequest) {
+            $this->container->get(ServiceResetter::class)->reset();
+        }
+
         return $this->getHttpKernel()->handle($request, $type, $catch);
     }

Not sure about adding Kernel::reset() as it does not really reset the Kernel or? It just resets some services 😋

Or we do it inside HttpKernel?

@derrabus
Copy link
Member
derrabus commented Oct 23, 2017

@nicolas-grekas @derrabus actually also the current solution merged into 3.4 makes it impossible to profile the kernel.terminate event, right?

@dmaicher Wrong. The profiler itself has always been triggered on the kernel.terminate event, so that very event has never been profiled completely. Our current service reset is triggered after it, so we do not break anything that wasn't broken already. Yet, the service reset will not appear in the event dispatcher profiler tab, although it's actually an event subscriber.

What if we have a ServiceResetter or similar and invoke it before handling the second master request within the Kernel?

Well yes, that's what I meant when I said:

[…] we do the reset as the first thing in Kernel::handle() […]

This would lower the impact of the feature on existing Symfony projects and thus fix your problem. Downside is that we would move logic into a kernel that should be kept small and generic.

Not sure about adding Kernel::reset() as it does not really reset the Kernel or?

Well, call it prepare (since it prepares the kernel for the next request) or resetServiceState() or whatever you like.

@nicolas-grekas
Copy link
Member

Looking at the code, doing it in RequestStack might be the best place: when the stack goes from empty to non-empty for the 2nd time, it could call a list of reset methods that would be provided as a constructor arg. WDYT?

@derrabus
Copy link
Member
derrabus commented Oct 25, 2017

I don't think, RequestStack should have that responsibility. How about a middleware around HttpKernel?

Prototype: https://github.com/derrabus/symfony/tree/3.4-resetter-middleware

@nicolas-grekas
Copy link
Member

See #24689

@nicolas-grekas
Copy link
Member
nicolas-grekas commented Oct 25, 2017

@derrabus I see no reason why it cannot be the responsibility of RequestStack. The middleware approach works, but adds a lot of boilerplate IMHO.

@derrabus
Copy link
Member

Well, so far, RequestStack has been a dump data structure with a single responsibility, as the name of the class suggests: It has been a stack of Response objects.

You're introducing an unexpected side-effect to something that's supposed to be a very simple data structure operation. I'd strongly advise against that.

@nicolas-grekas
Copy link
Member
nicolas-grekas commented Oct 25, 2017

It's not unexpected: "you" passed the arguments to the constructor. Another way could be to decorate the original RequestStack. Not sure it's required, but if that's the only thing we can agree on, why not. That'd be still better than doing an HttpKernel middleware.

@derrabus
Copy link
Member
derrabus commented Oct 25, 2017

It's not unexpected: "you" passed the argument to the constructor.

Actually, it's the framework that does that. If I pull the request stack from the container, I'd expect a dumb data structure with requests in it.

Another way could be to decorate the original RequestStack.

Better, because that decorator could be part of the HttpKernel component, so we keep HttpFoundation clean. But the main problem remains for me. Stack.push is a data structure operation. Moving side effects into it does not feel good from an architectural point of view.

That'd be still better that doing an HttpKernel middleware.

What's so bad about that approach? The middleware in my prototype has 66 LOC, is self-contained and completely decoupled from the kernel.

(I could even imagine that the profiler could benefit from running as a middleware, but that's a different story.)

@dmaicher
Copy link
Contributor Author

See #24697

@dmaicher dmaicher closed this Oct 26, 2017
@dmaicher dmaicher deleted the fix-24552 branch October 26, 2017 14:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants
0