Page MenuHomePhabricator

Hooking into MediaWikiServices with a 1.35-style HookHandler breaks maintenance scripts
Closed, ResolvedPublic

Description

Getting the following error when running maintenance scripts such as update.php:

PHP Fatal error:  Uncaught MWException: salvage() must be called immediately after construction in includes/HookContainer/HookContainer.php:94
Stack trace:
#0 includes/MediaWikiServices.php(321): MediaWiki\HookContainer\HookContainer->salvage(Object(MediaWiki\HookContainer\HookContainer))
#1 includes/MediaWikiServices.php(292): MediaWiki\MediaWikiServices->salvage(Object(MediaWiki\MediaWikiServices))
#2 includes/Setup.php(535): MediaWiki\MediaWikiServices::resetGlobalInstance(Object(GlobalVarConfig), 'quick')
#3 maintenance/doMaintenance.php(91): require_once('...')
#4 maintenance/update.php(253): require_once('...')
#5 {main}
  thrown in includes/HookContainer/HookContainer.php on line 94

As far as I can tell, this is caused by an extension registering a hook for MediaWikiServices using the new 1.35-style HookHandler classes. An extension.json snippet that reproduces the issue is below:

{
    "AutoloadClasses": {
        "MyExtensionHooks": "MyExtensionHooks.php"
    },
    "Hooks": {
        "MediaWikiServices": "service"
    },
    "HookHandlers": {
        "service": {
            "class": "MyExtensionHooks"
        }
    }
}

along with e.g. MyExtensionHooks.php:

<?php
class MyExtensionHooks implements MediaWikiServicesHook {
    public function onMediaWikiServices( $services ) { }
}

This seems to only break in maintenance scripts. It works fine for normal web requests and the action API. The culprit seems to be Maintenance::finalSetup() which grabs an instance of MediaWikiServices before services are finished bootstrapping in Setup.php. That function is run as a MW_SETUP_CALLBACK which happens very early on in init.

I'm not sure what the proper fix here is, hence why no gerrit patches as of yet. I'm thinking though that probably we just want HookContainer::salvage to ignore whether or not HookContainer::$handlersByName is already set. Since we can easily re-instantiate any of those handlers whenever we attempt to fetch the appropriate hooks, it should be safe to just erase any existing handlers and re-construct them later as they're needed. I think we still want to throw if any dynamic handlers are registered before salvage() is called since those can't be easily recovered.

This fix should be backported to 1.35 as well once merged.

Event Timeline

Change 631546 had a related patch set uploaded (by Skizzerz; owner: Skizzerz):
[mediawiki/core@master] Make HookContainer::salvage ignore named handlers

https://gerrit.wikimedia.org/r/631546

As I said in the exception message, I expected that salvage() would be called immediately after construction, not after a hook is executed. I don't think Maintenance::finalSetup() is at fault, the context there is the same as LocalSettings.php, and it should be valid to use the hook container during LocalSettings.php. I think MediaWikiServices::resetGlobalInstance() is at fault for using the partially-initialised HookContainer to run the MediaWikiServices hook. My idea for a fix is to use $oldInstance->getHookContainer() to run the hook. I've confirmed that if this is done, no exception is thrown with an empty onMediaWikiServices(). I haven't confirmed that a more complex handler would be able to do what it needs to do, but I imagine it would work. I'll upload my patch in a couple of minutes.

Change 644079 had a related patch set uploaded (by Tim Starling; owner: Tim Starling):
[mediawiki/core@master] Use the old HookContainer to set up the post-reset services

https://gerrit.wikimedia.org/r/644079

Note that for the reproduction procedure to work, $wgDBadminuser must be set. Maintenance::finalSetup() uses it to decide whether to reconfigure the LBFactory.

Change 631546 abandoned by Skizzerz:
[mediawiki/core@master] Make HookContainer::salvage ignore named handlers

Reason:
Tim's change looks like a better approach

https://gerrit.wikimedia.org/r/631546

Change 644079 merged by jenkins-bot:
[mediawiki/core@master] Use the old HookContainer to set up the post-reset services

https://gerrit.wikimedia.org/r/644079

@tstarling any chance we can get this backported to 1.35 as well? I'm developing an extension making use of that hook and wish to support 1.35 with it (on account of it being LTS) and right now my options are to either only target 1.36+ or to use the legacy hooking system (losing all of the benefits of implementing the interface and getting guarantees on what the signature looks like)

Change 644646 had a related patch set uploaded (by Reedy; owner: Tim Starling):
[mediawiki/core@REL1_35] Use the old HookContainer to set up the post-reset services

https://gerrit.wikimedia.org/r/644646

Change 644646 merged by jenkins-bot:
[mediawiki/core@REL1_35] Use the old HookContainer to set up the post-reset services

https://gerrit.wikimedia.org/r/644646

Clarakosi assigned this task to tstarling.
Clarakosi subscribed.

Marking as resolved but please feel free to reopen if the problem continues