Baldinof / roadrunner-bundle

A RoadRunner worker integrated in your Symfony app
MIT License
255 stars 46 forks source link

Entity manager bleeding entities over different requests #116

Closed FluffyDiscord closed 1 year ago

FluffyDiscord commented 1 year ago

Since each request uses the same booted kernel, the same entity manager is being used for each request. The issue with this is that the entities are partly still kept in memory and after few requests that touch the same entity (changing the entity and persisting) the entity manager starts throwing A managed+dirty entity xxx can not be scheduled for insertion. I can reproduce it consistently.

This issue can not be solved without rebooting the whole kernel.

Baldinof commented 1 year ago

Hello @FluffyDiscord!

Can you provide a working reproducer?

FluffyDiscord commented 1 year ago

You would not believe what the issue was. This is insane - I may be missing something, but I think this is symfony bug candidate.


After days of debugging I narrowed down the issue down to custom form type. Steps taken:

I stopped using my custom form extension that adds event subscriber to some custom form types which were working with entities (fetching and persisting). This resolved the weird doctrine errors (whole bunch of them, none of them made sense and showed up only in prod mode)

Then I noticed that now after persisting new entity with my modified custom form type I could see it inside the db, also could fetch it outside the form just fine, but inside the form in choices option it was not included for some reason.

Rebooting kernel or restarting RR fixed this missing entity.

This behavior was weird, so after a bit more I randomly placed notification sender (static class that sends Discord message without any reliance on DI) inside configureOptions of my form type to check when it was called.

To my surprise, it was called only once, on first request, then never again until kernel reboot or RR restarted.

So on first request it loaded entities from DB, placed them in choices and then they weren't ever refreshed since options were resolved in first request and symfony didn't feel like resolving them again after that.

I checked how symfony creates these forms and found that Symfony\Component\Form\FormRegistry is caching resolved form types. I guess symfony thinks that form types are "stateless", but you often use DI inside your form type to add additional config options or process some data dynamically.

I have "hotfixed" this behavior by registering my own copy of Symfony\Component\Form\FormRegistry (along with Symfony\Component\Form\FormFactoryBuilder since the original Symfony's FormRegistry is hardcoded inside this factory) and then reset the resolved form types using ResetInterface after each request. This is not a full fix, since if I use the same "stateful" form twice within one request, I will probably run into the same issue (no idea, didn't try and I won't get in this situation)

So this isn't issue within this bundle, but maybe this bundle should help out and apply fix. Just like other middlewares are for.

Baldinof commented 1 year ago

Crazy investigation!

I see ChoiceType supports a choice_loader option with a CallbackChoiceLoader class, would it help?

Also, do you know if there is the issue with EntityType? Is it resolving entities only once?

If there is no viable workaround with Symfony, I can look to add something here.

FluffyDiscord commented 1 year ago

The choice_loader might work, I did not consider it to be honest since I have yet to use it anywhere. I will check it out and the EntityType tomorrow. I would guess every form type, internal or custom, will be behaving the same way - load configureOptions once.

This still will be an issue for other form options though.

It might be also possible to replace only Symfony\Component\Form\ResolvedFormType and refresh only the $optionResolver instead of FormRegistry and it's Factory

FluffyDiscord commented 1 year ago

Just a heads up - as long as configureOptions are static, eg. without dependency on outside service, everything seems to work as intended. Also if you are cloning entity using the build-in php's clone then don't, doctrine proxies do not work. Make fresh object.