Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Segmentation fault with the container, Doctrine and the EntityManagerInterface #34200

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
sylfabre opened this issue Oct 31, 2019 · 10 comments
Closed

Comments

@sylfabre
Copy link
Contributor

Symfony version(s) affected: 4.3.5

Description
@nicolas-grekas Following up on our discussion at the meetup, I've researched the segmentation fault / memory limit error.

The bug comes from the way the method getDoctrine_Dbal_DefaultConnectionService() in the dumped container instantiates $this->services['doctrine.dbal.default_connection'] for Doctrine\DBAL\Connection

The method first instantiate all dependencies as expected:

protected function getDoctrine_Dbal_DefaultConnectionService()
{
	$a = ($this->services['validator'] ?? $this->getValidatorService());

	if (isset($this->services['doctrine.dbal.default_connection'])) {
		return $this->services['doctrine.dbal.default_connection'];
	}
	$b = ($this->services['search.search_indexer_subscriber'] ?? $this->getSearch_SearchIndexerSubscriberService());

	if (isset($this->services['doctrine.dbal.default_connection'])) {
		return $this->services['doctrine.dbal.default_connection'];
	}
	$c = new \Doctrine\DBAL\Configuration();

	$d = new \Doctrine\DBAL\Logging\LoggerChain();

	$e = new \Symfony\Bridge\Monolog\Logger('doctrine');
	$e->pushHandler(($this->privates['monolog.handler.main'] ?? $this->getMonolog_Handler_MainService()));
	$e->pushHandler(($this->privates['monolog.handler.syslog_handler'] ?? $this->getMonolog_Handler_SyslogHandlerService()));

	$d->addLogger(new \Symfony\Bridge\Doctrine\Logger\DbalLogger($e, ($this->privates['debug.stopwatch'] ?? ($this->privates['debug.stopwatch'] = new \Symfony\Component\Stopwatch\Stopwatch(true)))));
	$d->addLogger(new \Doctrine\DBAL\Logging\DebugStack());

	$c->setSQLLogger($d);
	$c->setSchemaAssetsFilter(new \Doctrine\Bundle\DoctrineBundle\Dbal\SchemaAssetsFilterManager([0 => new \Doctrine\Bundle\DoctrineBundle\Dbal\RegexSchemaAssetFilter('~^(?!(BuyPacker_|SMoney_|Spark_|buypacker_|smoney_|spark))~')]));
	$f = new \Symfony\Bridge\Doctrine\ContainerAwareEventManager(new \Symfony\Component\DependencyInjection\Argument\ServiceLocator($this->getService, [
		'doctrine.orm.default_listeners.attach_entity_listeners' => ['privates', 'doctrine.orm.default_listeners.attach_entity_listeners', 'getDoctrine_Orm_DefaultListeners_AttachEntityListenersService.php', true],
	], [
		'doctrine.orm.default_listeners.attach_entity_listeners' => '?',
	]));

	$g = new \App\Doctrine\ORM\Subscriber\LoggerSubscriber();
	$g->setDebugHelper(($this->services['legacy.helpers.debug'] ?? ($this->services['legacy.helpers.debug'] = new \App\Helper\DebugHelper($this->targetDirs[3]))));
	$g->setFormatter(new \App\Formatter\DoctrineLogFormatter());
	$h = new \App\Doctrine\ORM\Subscriber\ValidatorSubscriber();
	$h->setValidator($a);

	$f->addEventSubscriber($g);
	$f->addEventSubscriber($h);
	$f->addEventSubscriber($b);
	$f->addEventListener([0 => 'loadClassMetadata'], 'doctrine.orm.default_listeners.attach_entity_listeners');

	return $this->services['doctrine.dbal.default_connection'] = (new \Doctrine\Bundle\DoctrineBundle\ConnectionFactory($this->parameters['doctrine.dbal.connection_factory.types']))->createConnection(['driver' => 'pdo_mysql', 'charset' => 'utf8mb4', 'url' => 'mysql://root:[email protected]:3306/assoconnect', 'host' => 'localhost', 'port' => NULL, 'user' => 'root', 'password' => NULL, 'driverOptions' => [], 'serverVersion' => '5.7', 'defaultTableOptions' => []], $c, $f, ['bic' => 'string', 'country' => 'string', 'currency' => 'string', 'datetimetz' => 'datetime', 'datetimeutc' => 'datetime', 'email' => 'string', 'iban' => 'string', 'ip' => 'string', 'latitude' => 'decimal', 'locale' => 'string', 'longitude' => 'decimal', 'money' => 'decimal', 'percent' => 'decimal', 'phone' => 'string', 'phonelandline' => 'string', 'phonemobile' => 'string', 'timezone' => 'string', 'uuid_binary_ordered_time' => 'binary']);
}

and ends by defining $this->services['doctrine.dbal.default_connection'] as expected.

BUT in our case there is one dependency $b = ($this->services['search.search_indexer_subscriber'] ?? $this->getSearch_SearchIndexerSubscriberService()); to define Algolia\SearchBundle\EventListener\SearchIndexerSubscriber.

This dependency is used as an event subscriber:
$f->addEventSubscriber($b);

The dependency chain is:

  • Algolia\SearchBundle\EventListener\SearchIndexerSubscriber
  • Algolia\SearchBundle\IndexManager
  • Symfony\Component\Serializer\Serializer

The Serializer lists all the Normalizers for its first argument:
\Symfony\Component\Serializer\Serializer::__construct(array $normalizers = [], array $encoders = []))

One of our normalizers depends on a service depending on EntityManagerInterface which requires the actual service Doctrine\ORM\EntityManager which depends on Doctrine\DBAL\Connection.

So in the end, $this->services['doctrine.dbal.default_connection'] is never defined and we have an infine loop.

Bottom line, I think there are different bugs here:

  • the circular-dependency checker seems to only look at constructors' argument type hints
  • the documentation should state to "never inject EntityManagerInterface but rely on Doctrine\Common\Persistence\ManagerRegistry" as you were saying.

For the 2nd one, https://symfony.com/doc/current/doctrine.html states:

you can add an argument to the action: createProduct(EntityManagerInterface $entityManager)
This works because Controllers usually are not used as dependencies.
But it is misleading for people writing services: a best practice should be given somewhere.

How to reproduce
I haven't written yet a reproducer but I'm not sure it's useful here.

Possible Solution
I can submit a doc PR if you want.

And if updating the circular-dependency checker is too difficult, an alternative may be to create a state checker in the dumper container for each method:

protected function getService_XXX()
{
	if (in_array('some.service', $this->instantiating)) {
		// dump some useful stack for the developer
		
		die(1);
	} else {
		$this->instantiating []= 'some.service';
	}
	
	
	// Current code
	
	
	return $this->services['some.service'] = new SomeService(...);
}
@sylfabre sylfabre changed the title Dependency injection with Doctrine and the EntityManagerInterface Segmentation fault with the container, Doctrine and the EntityManagerInterface Oct 31, 2019
@nicolas-grekas nicolas-grekas self-assigned this Nov 4, 2019
@nicolas-grekas
Copy link
Member

nicolas-grekas commented Dec 2, 2019

The dependency checker is fine: only the constructor's arguments should be considered as any other kind of circular loop is solvable. There are many test cases that prove it already.

About this specific loop that happens at runtime would you be able to provide a reproducing test case in PhpDumperTest::testAlmostCircular()? (see https://github.com/symfony/symfony/blob/master/src/Symfony/Component/DependencyInjection/Tests/Fixtures/containers/container_almost_circular.php for examples)

@nicolas-grekas
Copy link
Member

About the doc PR, please do of course!

@sylfabre
Copy link
Contributor Author

sylfabre commented Dec 9, 2019

@nicolas-grekas I've been working on this issue this morning and I need some help to create the reproducer: why are variables like $a or $b defined like ($this->services['validator'] ?? $this->getValidatorService()); while others like $c or $f are instanciated right in the method getDoctrine_Dbal_DefaultConnectionService()

From what I understand from https://github.com/symfony/symfony/blob/master/src/Symfony/Component/DependencyInjection/Tests/Fixtures/containers/container_almost_circular.php, it is only used for services (like $a or $b). Do you have another example for instances like $c or $f please?

The closer code to the current issue in src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_almost_circular_public.php is:

    /**
     * Gets the public 'foo2' shared service.
     *
     * @return \FooCircular
     */
    protected function getFoo2Service()
    {
        $a = new \BarCircular();

        $this->services['foo2'] = $instance = new \FooCircular($a);

        $a->addFoobar(($this->services['foobar2'] ?? $this->getFoobar2Service()));

        return $instance;
    }

But the service is defined in $this->services before any method is called on the service's dependencies ($a->addFoobar() in the previous code) which solves the issue.

=> In my bug's code, the methods are called before the instantiation of the service, and the instantiation is done on the return line: how may I reproduce this behavior?

@stof
Copy link
Member

stof commented Dec 9, 2019

@nicolas-grekas actually, we used to have such protections against more complex circular references in Symfony 3.2 and older, where all internals service references were using $this->get() as well.
We still have the code for that in Container::get in newer versions (even in 5.0), but it is not effective anymore, because we are now bypassing the layer checking and updating $this->loading.

@nicolas-grekas
Copy link
Member

nicolas-grekas commented Dec 9, 2019

All related issues arise when services are inlined. To prevent a service from being inlined, the reproducer should use public services. I'd recommend trying to reproduce the graph you have at hand using dummy services: all services that are dumped as methods should be public, all the others private.

Then, why the variables are dumped this way is complex, I cannot give you a simple answer.

@stof adding the check would not solve anything I fear. It might actually break solvable graphs as sometimes there need to be two calls to a service factory to create the fully configured service (there are tests doing so already).

In the end, either the circular loop is legit and should be detected at compile time - but to my current understanding, as soon as a loop involves a setter or a property, it should not turn into a runtime loop but can be solved instead, by creating the services (especially the inlined ones) in the correct order.

A way to see this is: how would I instantiate this graph if I were to create the code by hand? If there is a solution to this that is not found currently, that's the bug.

@stof
Copy link
Member

stof commented Dec 9, 2019

@nicolas-grekas there is also another issue: the dumper assumes that any lazy reference won't trigger a circular reference. But that is a valid assumption only as long as they are not resolved during the instantiation.

I see in the dumped code that some of the calls to instantiate inline services are followed by a check whether the requested service is now instantiated (due to some circular reference). I think that the safe logic would be to have such check after all method calls. Or at least after any such method call that involves a soft reference to the current service (i.e. including lazy references, references through method calls, and potential references through injecting the service_container service) in the object graph of the service on which we call the method (in case of method calls) or that we instantiate (in case of instantiation of a different service).

It might actually break solvable graphs as sometimes there need to be two calls to a service factory to create the fully configured service (there are tests doing so already).

That is actually the root of the bug, not a feature. If we call the factory twice, we will have 2 different instances of the service in different parts of the object graph if assignments to $this->services (or $this->privates) are not done in the right place. And if assignments are done properly, the second call should not happen, as it will use the existing instance.

The big benefit of these runtime checks is that you have a (more or less) clear exception message here, rather than having a broken object graph (like having 2 different instances of your entity manager in different parts of the project). And you know that you have an issue in your service instantiation.

@nicolas-grekas
Copy link
Member

nicolas-grekas commented Dec 9, 2019

I think I've grown gray hairs from this issue so I would be very happy if anyone else could take over and do the improvements :)

@stof
Copy link
Member

stof commented Dec 9, 2019

btw, re-adding these loading checks as safeguards will not be easy. We could add such $this->loading checks in the dumped code of factories. But things will become much harder in places where we inline the factories.

@TamasSzigeti
Copy link
Contributor

I've just bumped into the exact same problem, circular dep via doctrine subscriber and serializer led to segfault, running with xdebug showed a proper trace to track this down.

The interesting bit was that it only showed up when I tried to install symfony/proxy-manager-bridge.

I never looked into the internals of DI, so can't really contribute here, just making this comment in case it will be useful for somebody.

I solved it by replacing the doctrine subscriber with an entity listener.

@jderusse
Copy link
Member

jderusse commented Jan 12, 2021

does #39799 fix your issue ?

nicolas-grekas added a commit that referenced this issue Jan 14, 2021
…russe)

This PR was merged into the 4.4 branch.

Discussion
----------

[DoctrineBridge] Fix circular loop with EntityManager

| Q             | A
| ------------- | ---
| Branch?       | 4.4
| Bug fix?      | yes
| New feature?  | no
| Deprecations? | no
| Tickets       | fix #39619
| License       | MIT
| Doc PR        | -

This PR fix a segfault in EntityManager by making the LazyEventManager handle EventSubscriber in a lazy way.

Maybe #34200 too

Commits
-------

23d6921 Fix circular loop with EntityManager
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

8 participants