Skip to content

[DoctrineBridge] ManagerRegistry::resetService breaks entity manager identities #35216

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
ostrolucky opened this issue Jan 5, 2020 · 21 comments

Comments

@ostrolucky
Copy link
Contributor

ostrolucky commented Jan 5, 2020

This method is inherently very unsafe to use, because it changes instance of wrapped entity manager, while other instances of said entity manager are untouched, which causes desynchronized identity maps.

We should think about other approach how to reopen entity manager, without breaking identitiees.

More info in doctrine/DoctrineBundle#1118 and doctrine/DoctrineBundle#1114

@ostrolucky ostrolucky changed the title [DoctrineBridge] Service [DoctrineBridge] ManagerRegistry::resetService breaks entity manager identities Jan 5, 2020
@dnna
Copy link

dnna commented Jan 5, 2020

I think the main issue here is that the entity manager once closed cannot really be reopened (since Doctrine cannot guarantee that the entity state is consistent any more). AFAIK this is the root cause that forces resetService to be written in a way that creates a new EM, so that the user can always get a usable instance.

The problem here arises when the Manager - @doctrine.orm.default_entity_manager itself is injected to a service (instead of the ManagerRegistry - @doctrine), combined with an execution model like PHP-PM which does not clear everything at the end of the request. In this case, when the entity manager is reset, the services that had it injected directly do not know that their instance of the EM is no longer valid and will attempt to use the previous version. On the other hand, services that inject ManagerRegistry will return the new version when getManager is called, so in the end some services will be using one instance of the manager and others the other.

As long as Doctrine doesn't provide a way to cleanly reopen the existing entity manager (and basically ensuring that ManagerRegistry always provides the same instance of the EM), I personally can't think of a good solution to this other than discouraging users from setting the entity manager as a property in their services.

@ostrolucky
Copy link
Contributor Author

As you can see, this method is willing to break encapsulation with Closure::bind already, so I don't see why it can't just do clear() and force set the closed flag to false.

Also I don't really buy reasoning that doctrine closes entity manager because it can no longer guarantee entity state -> there is no entity state when error happens, because close does clear() first.

@dnna
Copy link

dnna commented Jan 5, 2020

Apologies, I didn't realize Closure::bind was being used already. It does feel a bit hacky and I'm not really sure what kind of side-effects it could have, but I guess reopening it in this way would work.

I wonder though, is this maybe something deeper that should be addressed within Doctrine? If it's safe to reopen a closed EM after a clear, shouldn't Doctrine provide a reopen method or even just reopen the EM automatically when clear is called? This would make the bridge's job here so much easier.

@ostrolucky
Copy link
Contributor Author

Yes but that would be a new feature, while this is a bug. Bridge should first fix resetting in a way that does not break identities, doctrine itself can in future provide easier way to do it.

@acasademont
Copy link
Contributor

Would be great to understand what are the deeper implications in the ORM (if there are some, which is implied by reading the EM code comments) of reopening an EM. I always thought it was more of a DBAL thing, being unable to reopen the actual DB connections after a failure.

@stof
Copy link
Member

stof commented Jan 9, 2020

Injecting @doctrine.orm.default_entity_manager in a service is fine with resetting (except maybe in some weird cases triggering undetected circular references breaking the proper initialization, but then not only resetService will be affected).

The issue happens when you define an entity repository as a service without using the ServiceEntityRepository feature. this is a known issue. and that's why DoctrineBundle and Symfony said for years that Doctrine repositories should not be defined as services (and that's why we implemented ServiceEntityRepository to have a way to define them as services).

The always-working way to reopen an entity manager would require to be able to reopen the existing entity manager, which would require the ORM itself to have such feature (and I don't see that coming to a 2.x release).

@acasademont
Copy link
Contributor

@stof afaik with a Closure::bind you could reopen the EM without having to mess with the ORM, we're already "hacking" the proxy in the same way to reset it. So why not actually clear + reopen the same EM (it's basically just reverting the closed property of the EM)? What are the implications of doing it? Even the proxy wouldn't be needed anymore.

For reference, your issue in doctrine/orm#5933

@jaapio
Copy link
Contributor

jaapio commented Sep 1, 2020

For future reference once anyone has the same issue as I had:

class MyEntityServiceRepository
{
    /**
     * @var EntityManagerInterface
     */
    private EntityManagerInterface $entityManager;

    /**
     * @var ObjectRepository
     */
    private ObjectRepository $repository;

    public function __construct(EntityManagerInterface $entityManager)
    {
        $this->entityManager = $entityManager;
        $this->repository = $entityManager->getRepository(InternalTrack::class);
    }

When using this in a Symfony messenger setup. One of your jobs triggers an exception in the EntityManager which will close the entitymanager. You will run into very strange errors. Since on the second job your repository and entitymanager in this situation are not having the same UnitOfWork.

The way to fix this is not having a property for your repository, but fetch it on access from the entitymanager which is nicely reset.

@carsonbot
Copy link

Hey, thanks for your report!
There has not been a lot of activity here for a while. Is this bug still relevant? Have you managed to find a workaround?

@ostrolucky
Copy link
Contributor Author

.

@carsonbot carsonbot removed the Stalled label Mar 2, 2021
@acasademont
Copy link
Contributor

@carsonbot This is still relevant, yep!

@carsonbot
Copy link

Hey, thanks for your report!
There has not been a lot of activity here for a while. Is this bug still relevant? Have you managed to find a workaround?

@carsonbot
Copy link

Friendly ping? Should this still be open? I will close if I don't hear anything.

@acasademont
Copy link
Contributor

acasademont commented Sep 14, 2021 via email

@carsonbot carsonbot removed the Stalled label Sep 14, 2021
@carsonbot
Copy link

Hey, thanks for your report!
There has not been a lot of activity here for a while. Is this bug still relevant? Have you managed to find a workaround?

@acasademont
Copy link
Contributor

acasademont commented Mar 15, 2022 via email

@carsonbot carsonbot removed the Stalled label Mar 15, 2022
@nicolas-grekas
Copy link
Member

The workaround is similar to what has been described before:

The way to fix this is not having a property for your repository, but fetch it on access from the entitymanager which is nicely reset.

Of course it'd be better if we could have a working "reset" method on Doctrine's side.
Which makes me wonder: why do we need this issue on the Symfony repo?

@ostrolucky
Copy link
Contributor Author

Because the way how symfony resets em breaks identities. Argument that there is no support for it in orm doesn't stand considering symfony already uses closure:: bind to break through encapsulation.

@nicolas-grekas
Copy link
Member

nicolas-grekas commented Mar 17, 2022

this method is willing to break encapsulation with Closure::bind already

Closure:bind is used on an instance of the container, which is something that is in Symfony's responsibility area.

so I don't see why it can't just do clear() and force set the closed flag to false.

Because then we'd be crossing a responsibility boundary, namely doing something that is within Doctrine's area. /cc @acasademont also since you were wondering about why we don't use Closure::bind to hack into Doctrine's state.

Resetting this state should be done on Doctrine's side.

@nicolas-grekas
Copy link
Member

Closing as explained in favor of doctrine/orm#5933

@acasademont
Copy link
Contributor

Yep, makes sense to fix it upstream and then get rid of the nasty proxy hack in Symfony

fabpot added a commit that referenced this issue May 27, 2022
…ed as ghost objects (nicolas-grekas)

This PR was merged into the 4.4 branch.

Discussion
----------

[DoctrineBridge] Don't reinit managers when they are proxied as ghost objects

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

Paving the way to #35345

Interface `GhostObjectInterface` extends `LazyLoadingInterface` but breaks LSP because `setProxyInitializer()` takes another kind of closure as argument.

This won't solve #35216 since resetting a closed entity manager won't happen anymore if we start to use ghost object proxies. But at least this code won't explode.

/cc @ostrolucky any idea what we could put inside the added "if" to solve #35216? Would you be up to submit a PR doing that, branch 6.2 I guess since that'd be a new feature?

Commits
-------

1dbf3a6 [DoctrineBridge] Don't reinit managers when they are proxied as ghost objects
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