Let me tell you a sad Drupal story:
- Imagine a module called Pastafazoul. It provides a content entity type. It also provides a file field and field storage in its default configuration (although this bug would happen with any field type), attached to the entity type it provides. The important thing is that the field storage does not persist if there are no fields. Imagine also that the module provides some other form of configuration, like a bundle or view display, which uses, and therefore depends on, that field.
- You install Pastafazoul. All is hunky-dory.
- Without having created any content, you decide to uninstall Pastafazoul. When you try, you get a screen confirming the configuration changes that will occur as a result. Among those changes is that the field and field storage will be deleted. So far, so good.
- You uninstall Pastafazoul. The field, as expected, is deleted. However, it is not purged. But that's OK, because purging is done during cron runs (
field_cron()
). The uninstall goes OK otherwise, and the entity type it provided no longer exists. All good. - You now try to uninstall File (let's pretend that Pastafazoul was the only thing depending on it), but it won't let you because it says that File has fields pending deletion. Ah, no problem, you think -- you haven't run cron yet.
- You run cron and try again to uninstall File. It still says that File has fields pending deletion.
- Mildly puzzled, you run cron again. Then you try to uninstall File again. It still says that File has fields pending deletion.
- You run cron until you are blue in the face. File continues to insist that has fields pending deletion. You try again and again, eventually go insane, and die of old age. Later, the sun becomes a red giant and incinerates the Earth. Still the damn module sits there, welded to your site, insisting that it has fields pending deletion.
What went wrong?
The problem lies in core/modules/field/field.purge.inc
, circa line 92:
if (!isset($info[$entity_type])) {
continue;
}
The entity type to which the field was attached was destroyed when Pastafazoul was uninstalled, but the field was merely deleted, with the expectation of being purged later. Therefore the isset()
failed, and the field was not purged.
Meanwhile, \Drupal\field\FieldUninstallValidator
has been loading that deleted, effectively unpurgeable field (see the getFieldStorageByModule()
method). Because it was able to load the deleted field, it thinks the field is still waiting to be purged (and it's not wrong). Therefore, it is not allowing File to be uninstalled, because as far as it can tell, there are file fields pending deletion.
So in other words: if you remove a content entity type by uninstalling the module that provides it, any fields attached to that entity type will stick around forever, in limbo, deleted but not purgeable. And the modules which provide those fields will never be uninstallable, because they will continue to think that their fields are pending deletion -- not realizing that those fields are trapped in a state where they can never be purged.
I think this is a pretty major bug because it can crap up a Drupal site with zombie fields, prevent modules from being uninstalled, and trip up core tests like InstallUninstallTest, which expect to be able to install and uninstall modules in a logical way. Not sure what the right fix would be -- I suspect there are several possible approaches -- but we need to start thinking about this.
This bug was turned up by InstallUninstallTest and #2831936: Add "File" MediaSource plugin.
Comment | File | Size | Author |
---|---|---|---|
#20 | non_persistent_fields-2884202-20-interdiff.txt | 511 bytes | Berdir |
#20 | non_persistent_fields-2884202-20.patch | 12.96 KB | Berdir |
#16 | non_persistent_fields-2884202-16-interdiff.txt | 832 bytes | Berdir |
#16 | non_persistent_fields-2884202-16.patch | 12.96 KB | Berdir |
#16 | non_persistent_fields-2884202-16-test-only.patch | 6.44 KB | Berdir |
Comments
Comment #2
BerdirAs discussed last night, I think as a first step, \Drupal\field\FieldUninstallValidator::validate() should be extended to also check for fields of entity types that would be removed by the uninstall.
Possibly only deleted fields, as non-deleted fields can just be removed if there is no data.
I'm wondering when exactly the field does get set to being purged status. if that only happens on uninstall due to the media bundles being deleted, then we might hit a weird state where the uninstall validator already did run?
In that case, a possibly alternative fix would be to check for a field being empty when deleting it and then immediately drop it, without going through the purge process.
Comment #3
Wim LeersQuoting @catch at #2831936-188: Add "File" MediaSource plugin:
Emphasis mine.
Comment #4
xjmYeah, data integrity sadness. Let's promote this to at least review the potential criticality.
Comment #5
amateescu CreditAttribution: amateescu for Pfizer, Inc. commentedHere's a test-only patch for whoever wants to look into this bug without spending their lifetime running/waiting for
\Drupal\system\Tests\Module\InstallUninstallTest
:)Comment #6
BerdirThanks @amateescu! :)
So I think I was actually wrong in #2, at least the first part. The interesting thing is that we *explicitly* exclude entity types that are being uninstalled as well, with the argument that those need to have no data to be deleted anyway.
Which makes sense. Problem is that we also send empty fields through the purge process and therefore condem them to this zombie-state for eternity.
But what I wrote in the last paragraph should work. If a field is empty, there's no point in trying to purge something, so lets just remove it.
At least it makes this test path, but I had to stop using $this->container, because that module installer still had stale services injected. See #2066993: Use \Drupal consistently in tests.
Now the question is how many tests explicitly or implicitly expect that empty fields get purged.
Comment #7
BerdirAlso, I would suggest we use something like "Problem is that we also send empty fields through the purge process and therefore condem them to this zombie-state for eternity." for the issue title ;)
Comment #8
phenaproximaWish granted. :)
Comment #10
BerdirThis should fix those test fails. Updated some of the existing tests to specifically test deleting fields/storages without data because we have a whole separate test (BulkDeleteTest) for deleting and purging. I also updated some *very* old @todo's to point to the test that is actually testing the purging.
I think that gives us enough test coverage.
Comment #12
amateescu CreditAttribution: amateescu for Pfizer, Inc. commentedThe new logic "delete the field directly if it doesn't have any data" makes perfect sense to me. Here's a few points that could be fixed:
Unrelated change?
Really minor point, but I usually prefer to describe the array itself, e.g.
/** @var \Drupal\field\FieldConfigInterface[] $fields */
. Feel free to ignore this one :)Let's get the entity type manager outside the foreach, maybe at the top of the method like we do for
$state
.Assuming that we want to keep this test, we need a description for the method and a couple of code comments inside.
Comment #13
BerdirThanks!
1. Removed change.
2. Hm, I prefer it on the variable in the loop :)
3. Changed.
4. What about converting it to a kernel test? Verified it still fails.
Comment #14
amateescu CreditAttribution: amateescu for Pfizer, Inc. commentedAwesome!
Comment #16
BerdirWould be more awesome if I would not have used the wrong KernelTestBase class :)
Comment #20
BerdirAnd now with the right namespace, clearly it was too late.
Comment #21
amateescu CreditAttribution: amateescu for Pfizer, Inc. commentedYeah, namespaces are tricky :)
Comment #22
xjm@Cottser, @cilefen, @effulgentsia, and I discussed this issue and agreed that it' a critical bug as a data integrity problem that prevents un/reinstallation of modules without workaround, as well as interfering with cleanup/removal of data.
Comment #23
catchIs there a reason this hunk couldn't use $field->getFieldStorageDefinition()->hasData()?
Otherwise looks good, nice find, and maybe the first nail in the coffin of field purging.
Comment #24
BerdirNot really a reason, could change that. Only problem is that hasData is on FieldStorageConfig and not FieldStorageDefinitionInterface, so the IDE wouldn't be happy about it but we know (I think?) that it is a FieldStorageConfig.
Comment #27
catchSince it's functionally the same, I think it's OK to leave it like it is.
Comment #28
phenaproximaI'm just so pleased that the word "zombies" is now in core's commit log. On a critical, no less.
Thanks, everybody!
Comment #29
xjmComment #30
Gábor HojtsySo I was/am looking at this for #2895685: Create release notes (a.k.a CHANGELOG) for Drupal 8.4.0-alpha1 and Drupal 8.4.0. Is this only a problem for empty fields? That seems to be the solution committed. It seems like so as per #7. Unfortunately the proposed solution did nto make it to the issue summary so trying to summarize for #2895685: Create release notes (a.k.a CHANGELOG) for Drupal 8.4.0-alpha1 and Drupal 8.4.0 myself.
Comment #31
BerdirYes, this special case only occurred with empty fields, where it allowed to uninstall because it is empty but then ended up in limbo. Non-empty fields would need to be deleted first before you can uninstall.
This change however isn't just a bugfix for that, it also leads to a neat improvement during site building. I'm sure everyone had the situation where they picked the wrong field type or made a typo in the field name that they wanted to fix and then delete and re-create the field. In 8.3, this resulted in temporary purge tables and they were only really removed on cron, so especially when not running cron locally, you might end up with a considerable amount of those being-purged tables. Now they are immediately dropped as they are still empty.