Problem/Motivation
PurgersService::initializePurgers() catches ServiceNotFoundException when a purger cannot be created (the comment says this happens "when uninstalling modules providing purger plugins"), logs "Error loading purger" and skips it. The id stays in $this->pluginsEnabled, though, and getLabels() (also getTypesByPurger() and the delete loop in setPluginsEnabled()) uses $this->purgers[$id] for every enabled id.
It happens on a configuration import that uninstalls a module providing a purger and removes its purger from purge.plugins, here cloudflare_purger 1.0.0. The import itself reports success, then at kernel.terminate LateRuntimeProcessor::onKernelTerminate() → invalidate() → isSystemOnFire() → PurgersAvailableDiagnosticCheck → getLabels() dies with "Error: Call to a member function getLabel() on null". drush cim exits 1, and drush deploy stops before cache:rebuild and deploy:hook. The isSyncing() guard from #3264141 does not cover it, because it is false at kernel.terminate, after the import.
Why the purger is skipped: during the uninstall the container is rebuilt without cloudflare_purger while its purger definitions are still in cache_discovery. The core tags queuer then calls getTypes() → initializePurgers() in the new container, and CloudflarePurger::create() asks for cloudflare_purger.tags_converter, which is gone.
Steps to reproduce
Clean install, PHP with zend.exception_ignore_args=On (as in production; see the related problem below for Off):
composer create-project drupal/recommended-project:^11.4 site && cd site
composer require drush/drush 'drupal/purge:3.x-dev' 'drupal/cloudflare_purger:^1.0'
vendor/bin/drush si minimal --db-url=sqlite://sites/default/files/.ht.sqlite -y
vendor/bin/drush en purge purge_processor_lateruntime purge_queuer_coretags cloudflare_purger -y
vendor/bin/drush p:purger-add cloudflare
vendor/bin/drush cex -y
# In the exported configuration: remove "cloudflare_purger: 0" from core.extension.yml,
# and set purge.plugins.yml to "purgers: { }".
vendor/bin/drush cim -y; echo "EXIT=$?"
Result, on 8.x-3.x at ac9fc9e and at 7dd41380 (Drupal 11.4.8, Drush 13.8.0, PHP 8.5):
[error] purgers: Error loading purger d7edf859e5 (cloudflare): Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException: You have requested a non-existent service "cloudflare_purger.tags_converter".
[success] The configuration was imported successfully.
[warning] Undefined array key "d7edf859e5" PurgersService.php:224
[error] Error: Call to a member function getLabel() on null in Drupal\purge\Plugin\Purge\Purger\PurgersService->getLabels() (line 224 of .../PurgersService.php)
EXIT=1
Proposed resolution
When a purger is skipped, drop it from the enabled list too, so that every caller sees the same set:
--- a/src/Plugin/Purge/Purger/PurgersService.php
+++ b/src/Plugin/Purge/Purger/PurgersService.php
@@ -411,6 +411,9 @@ class PurgersService extends ServiceBase implements PurgersServiceInterface {
catch (ServiceNotFoundException $e) {
$this->logger->error("Error loading purger @id (@plugin_id): @error",
['@id' => $id, '@plugin_id' => $plugin_id, '@error' => $e]);
+ // Keep the enabled list in sync with the loaded instances: callers
+ // such as ::getLabels() expect an instance for every enabled purger.
+ unset($this->pluginsEnabled[$id]);
}
}
With it, "Error loading purger" is still logged once, the import succeeds, the late runtime processor logs "There is no purger loaded", and drush cim exits 0; drush deploy runs cache:rebuild and deploy:hook. The unit tests (119) and LateRuntimeProcessorTest pass; the one kernel failure, Queue\ServiceTest::testStateConsistency, fails the same way without the change.
Related, in the same catch block: it logs the exception object ('@error' => $e). With zend.exception_ignore_args=Off, PHP's default and development setting, the exception carries the container, and serializing the log context throws "AssertionError: The container was serialized." The import then stops halfway: the module is uninstalled but purge.plugins still lists its purger. Logging $e->getMessage() avoids it.
Found while upgrading a site to Drupal 11; tested on a clean install as above.
Drafted with the help of an AI assistant; tested and reviewed by me.
Remaining tasks
Review of the merge request.
User interface changes
None.
API changes
None.
Data model changes
None.
Issue fork purge-3626509
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
Comments
Comment #3
lucaspretti commentedMerge request !79 against 8.x-3.x, ready for review: the
unset()from the summary, and a second commit that logs$e->getMessage()instead of the exception object.Comment #6
japerryConfirmed the bug on 8.x-3.x and D11. MR !79 looks correct to me. Added one more test, and fixed!