Updated: Comment #N
Problem/Motivation
Bad things happen when we serialize the container, for example when we serialize services that have the container injected. See #2190421: Fix some services that shouldn't be serialized for some crazy numbers. It also makes the testbot go bunkers.
Proposed resolution
Throw an exception in __sleep().
Remaining tasks
User interface changes
API changes
| Comment | File | Size | Author |
|---|---|---|---|
| #40 | 2190643-40-interdiff.txt | 1.14 KB | berdir |
| #40 | 2190643-40.patch | 13.1 KB | berdir |
| #37 | 2190643-36-sleep_only.patch | 1.84 KB | berdir |
| #36 | 2190643-36-interdiff.txt | 6.31 KB | berdir |
| #36 | 2190643-36.patch | 13.08 KB | berdir |
Comments
Comment #1
berdirCompletely untested.
Comment #3
yched commentedWow.
Comment #4
yched commentedViews with exposed filters seem to trigger this
admin/content
admin/people
Comment #5
amateescu commented@Berdir found that this helps.
Comment #6
berdirAlso doesn't hurt to have it ViewExecuteable, that also has Request.
We also found that PathPluginBase injects the RouteProvider but its never used anywhere. Not changing that here.
Comment #7
yched commentedFound that this also helps for CKEditorAdminTest.
I don't see why editor_form_filter_format_form_alter() has a need to serialize the Editor(Plugin)Manager.
Note that without this fix, a very weird $manager comes out of unserialize():
$manager->factory->discovery == 'breakpoint' (?! - it should be the EditorManager object itself)
Which makes me think that our current DependencySerialization code has flaws - maybe on cases of circular references like $service->foo->bar === $service.
[edit: couldn't isolate this on a simpler case though]
Also, the var_dump() debug code in PluginBase is handy but breaks AJAX :-)
Left it commented so that testbot gives us accurate results [edit: and cancelled test for #6]
Comment #11
yched commented(CKEditorAdminTest still fails with the changes from #7, but it fatals earlier without them)
Comment #12
catchComment #13
xanoLet me try this. I've got some documentation/string fixes at least.
Comment #14
xanoI updated some code comments and the exception message to be a bit more professional and descriptive. I also prevented container serialization for two services, but the test still fails.
Comment #15
damien tournoud commentedSerializing *any service* seems like a really bad idea. When unserializing, you will get a *second copy* of the service, potentially with a different configuration (and/or internal state).
On the other hand, serializing a reference to a service (aka. the service name) is fine and probably useful.
This probably requires some thinking, because the hack that is
DependencySerializationis going to possibly miss a lot of references to services.Comment #16
berdirWe don't want to serialize any services, but we won't be able to not serialize form objects, entities, plugins, entity controllers and so on that do reference services.
And yes, finding them all is going to be tricky, but there's no magic trick to make it work I think, injection and serialize-all-the-things is a combination that is very complicated.
Comment #17
amateescu commented@Damien:
DependencySerialization cannot miss any service since the container adds the _serviceId property to all of them?
@yched: The crap in editor_form_filter_format_form_alter() has been fixed in another issue: #2182077: Editor manager in $form_state causes test failures on PHP 5.4
@Xano: The goal is not to add __sleep() methods willy-nilly all over the place, but fix things as upstream as possible.
That said, this interdiff is agains #5 and it should have only two failing tests, CKEditorAdminTest and ConfigTranslationUiTest.
Comment #18
damien tournoud commented@amateescu: yes, that will work for services that are top-level properties, not to anything deeper then that.
Comment #19
damien tournoud commented@Berdir: in this list, it is arguable that we really should prevent serializing plugins and entity storage controllers. Those two can have an internal state (entity storage controllers have, at least, a cache), and allowing them to be (un)serialized is a recipe for hard-to-debug bugs.
Comment #20
amateescu commented@Damien, that's why the patch adds DependencySerialization to EntityControllerBase and (copies it, until we have a trait) to PluginBase.
Comment #21
yched commentednope, DependencySerialization does catch nested services, since serialize() runs __sleep() & serialize() recursively on each referenced object.
Comment #22
amateescu commented@yched, I think @Damien means that DependencySerialization doesn't help in cases where a property can hold an object which is not itself a service, but holds a reference to a service. This is exactly the case with
Drupal/Core/Config/Config, which I had to fix in #17.Comment #23
yched commentedAlso, this thing about "we should not serialize services to begin with" has been discussed at length before. The conclusion was that Form API makes this unavoidable without major rework - and D8's Form API even more, since ;
- FormControllerBase does $form_state['form_controller'] = $this
- $form['#some_callback'] = array($this, 'someMethod') is supported and very tempting
That led to DependencySerialization as a sorry safeguard.
The issue with DependencySerialization, as either a base class or as a trait, is that :
- it's left to each class to figure out "oh, turns out some weird code somewhere is serializing me because of a form or whatever, so I'd better use DependencySerialization so it doesn't mess up my service dependencies, + some custom __sleep() code to deal with my properties that happen to be static caches or similar".
- "some weird code somewhere is serializing me" is extremely easy to simply go unnoticed in the first place.
[crosspost with #22 - in short, yes, DependencySerialization only works on objects that extend DependencySerialization...]
Comment #26
damien tournoud commentedApparently it's hard to get this point across.
DependencySerializationonly work to, erm, serialize the direct dependencies of some object. It doesn't handle the object itself.As a consequence:
You don't actually want to allow serializing most of the services, you want to serialize *references* to those services (which is essentially what DependencySerialization does, but for a very narrow use case).
Allowing services to be serialized is a recipe for failure, so we should probably at the minimum add a
__sleepto some of them and see what breaks.Comment #27
yched commentedNot hard to get across, #26 and #23 are saying the same thing :-p
Comment #28
berdirCrazy stuff.
CkEditor test failed because that managed to get its hands on a ContainerBuilder $container, that didn't have the get() override, added it there too.
Config translation are apparently the only forms in core that receive a $request to buildForm(), that is then put into $form_state['build_info']['args'], that had a reference to HtmlFormController, which has $this->container. That required two different fixes, a) to extend that from DependencySerialization and b) to special case ContainerInterface in there, as that has a shortcut in the dumped container and is directly referenced as $this and therefore doesn't run through get().
All fixes make perfect sense, the crazy part is that they seem fairly common but both only resulted in a exactly one failing test, of all the tests that we have...
Comment #29
berdirWondering what to do about this? While I really think we should have this on by default, those issues are fairly hard to fix and figure out, for developers and obviously even more so for users.
And putting a "Ask @berdir to fix it" in there might not scale that well :D
Maybe we could log it somewhere, preferrable in a way that would fail a test?
On the other side, stuff like this is apparently causing serialize() notices in PHP 5.4 and even segfaults on my laptop with 5.5, so we must not let it happen.
Comment #30
yched commented#29 raises valid concerns. But not sure of how we address them.
Yes, a flat exception / fail leaves you with no clues as to what causes the bug, or even against which component / module to report it. Even a callstack is of little help, it's not the code that triggers serialization that is at fault.
We could look for a couple typical serialization callstacks in core and display the form_id, or the key of the k/v record... - kind of ugly though :-/ We cannot even report the path to the container in the overall serialized object.
So, yeah, what we'd really need is print @Berdir's phone number in the exception message...
Having this silently work IRL but fail in tests would be a bit confusing when trying to debug a failed test ? ("cannot reproduce the failure outside of the test"). Do we already have similar behavior on other stuff ?
Comment #31
dawehnerNot really a review.
Where is the place we use the container builder instead of a dumped container instance?
It would be great to expand the existing unit test.
Comment #32
berdirContainerBuilder was a *single* test fail in a CkEditor test, that enabled a module, then requested a patch, which created a new container there so for the time of that request, it was a ContainerBuilder instance.
Comment #33
tstoecklerNote that I had opened #2095203: DependencySerialization should work with ContainerBuilder as well, not just Container already at some point when config_translation mappers were still storing the entity manager. We might close that as duplicate now.
Comment #34
tstoecklerAlso, that issue contains test coverage, which we could bring over here.
Comment #35
berdir+1 on dupllicate and merging thest coverage, we also need test coverage for the ContainerBuilder as @dawehner said.
Comment #36
berdirOk, merged the other issue and test improvements from there, looks like they partially already existed in slightly different ways, so adopted accordingly.
Also added the __sleep() to ContainerBuilder. I think that was the reason that we only had a single test fail for this, that test there had a weird combination of both Container and ContainerBuilder. don't ask.
Also, I discussed this with @catch, and he suggested to use trigger_error() instead. That still causes test fails, but doesn't immediately break the site. Coming up with a good error message is pretty hard though, suggestions?
Comment #37
berdirAh, I also wanted to add a new sleep only patch to see how that looks.
Comment #38
dawehnerLet's just make them public as well.
Comment #40
berdirYep. The patch in #37 failed as expected.
Comment #41
dawehnerThank you
Comment #42
alexpottCommitted 1cce899 and pushed to 8.x. Thanks!
Comment #44
rjacobs commentedI'm sorry to be posting on a closed issue, but I'm wondering if someone can point me toward any follow-up on the points in number #26? Specifically this point:
Am I to understand that this is still the case? Does this mean that it is not good practice to include any properties that may contain their own injected services within an object that might get serialized? I'm working with a views style plugin at the moment (extends Drupal\views\Plugin\views\style\StylePluginBase) which is something that appears to be serialized as part of various views UI form operations. If I add a helper object that contains its own injected services as a property of this plugin all kinds of problems arise. These problems are clearly related to this issue as their calling card is "The container was serialized" errors in the logs.
Perhaps there is a follow-up that deals with this situation that I could explore? The serialization of objects that contain properties, that further contain services, must be an allowable practice?
Comment #45
berdirThere's nothing I can imagine doing that we aren't already, other adding the DependencySerializationTrait to specific objects. All views plugins should already have that.
Comment #46
rjacobs commentedThanks, yes I see now that views style plugins contain DependencySerializationTrait. Unless I'm mistaken though, that does not seem to recursively look for services within the plugin's properties, so services that are nested inside another property on the plugin could still cause problems upon serialization.
Are you saying that this "nested" case has already been accounted for somewhere? If so perhaps I've found a regression. Or, are you saying that core is already doing all that it should be doing and that this idea of "nested" properties (again, services inside of other properties that are themselves not services) must be avoided by anyone implementing plugins?
Comment #47
fabianx commentedThe point is your nested object itself needs to implement the DependencySerializationTrait - that is how to fix this.
As soon as you do:
$myobject = $otherobject->service
Or
otherobject::doSomething() {
$this->someObj = new MyObject($this->someService);
}
then MyObject needs to implement the DependencySerializationTrait itself for this to work.
That is how it works.
Comment #48
berdirI think the question is more about having something like this:
$this->someObject = $this_is_a_service;
vs.
$this->someArray['someObject'] = $this_is_a_service;
And yes, the trait currently doesn't support that.
Maybe we could try to do that, it might especially help FormState, but it's also a question of performance, this will make it a bit slower.
Comment #49
fabianx commentedI would argue in that case you need to implement __wakeup and __sleep yourself.
Deep array search is usually very costly, so don't think we want to go that route.
Comment #50
rjacobs commentedIf I recall correctly, what I had was along these lines:
$some_core_service = $my_views_plugin->my_helper_object->some_core_serviceSuch that
my_helper_objectcontained some helper methods that were also used by another display formatter that my module implemented. These methods neededsome_core_service(s) for config management and URL generation, so I simply injected them intomy_helper_objectinstead of callingDrupal::serviceinternally. Initially I reasoned that I should not makemy_helper_objectits own service because it was only applicable within the internal architecture of my module. Since then however, I've simply refactored it into a service. As best I can tell, that would be considered best practice for this kind of thing.I see now that I could have added DependencySerializationTrait, as suggested in #47. To be honest, back then I just didn't understand how this serialization could have related to views UI forms (and how on earth the serialization of the actual container could have related to the processing of those forms).
Comment #51
yched commentedYep, that's not new, but it's exactly the issue with OO forms in D8 : it makes it scaringly easy to end up serializing objects that were never written with serialization in mind, and generate huge serialization strings if they have deep object reference chains. And this can stay unnnoticed for months :-/
Comment #52
amateescu commented@alexpott doesn't agree with you in #2430813-24: Selecting the 'views' selection handler on an entity_reference field causes a fatal error and the comments below that. I'm tired of arguing, I hope you'll have better luck.