jenkins-bot has submitted this change and it was merged.

Change subject: Allow reset of global services (redux).
......................................................................


Allow reset of global services (redux).

(This is part of I6ec374ac9 wich was a re-submit of Ie98bf5af5
which got reverted by Ide7ab563)

This change provides a mechanism to reset global service instances
in an orderly manner. There are three use cases for this:

* the installation process
* integration tests (which most of the existing phpunit tests are)

In contrast to I6ec374ac9, this change does not cause singeltons
of legacy services to be reset. It is assumed that legacy services
use global state to access services and configuration, so any
change in confuguration would affect them immediately.

NOTE: the original I6ec374ac9 would cause session information to
get lost if the user session was creatsed before initialization
was complete. This was apparently triggered by the MobileFrontend
extension under some circumstances. Check with Addshore and Catrope.

Change-Id: Ie06782ffb96e675c0aa55dc26fb8f22037e8517d
---
M autoload.php
M docs/hooks.txt
M docs/injection.txt
M includes/GlobalFunctions.php
M includes/MediaWikiServices.php
M includes/ServiceWiring.php
A includes/Services/CannotReplaceActiveServiceException.php
A includes/Services/ContainerDisabledException.php
A includes/Services/DestructibleService.php
A includes/Services/NoSuchServiceException.php
A includes/Services/ServiceAlreadyDefinedException.php
M includes/Services/ServiceContainer.php
A includes/Services/ServiceDisabledException.php
M includes/Setup.php
M includes/config/ConfigFactory.php
M includes/db/loadbalancer/LBFactory.php
M includes/db/loadbalancer/LoadBalancer.php
M includes/installer/DatabaseInstaller.php
M includes/installer/Installer.php
M tests/phpunit/MediaWikiTestCase.php
M tests/phpunit/includes/MediaWikiServicesTest.php
M tests/phpunit/includes/Services/ServiceContainerTest.php
M tests/phpunit/includes/config/ConfigFactoryTest.php
M tests/phpunit/phpunit.php
24 files changed, 1,548 insertions(+), 139 deletions(-)

Approvals:
  Legoktm: Looks good to me, approved
  Addshore: Checked; Looks good to me, but someone else must approve
  jenkins-bot: Verified



diff --git a/autoload.php b/autoload.php
index 3b17215..c8277f5 100644
--- a/autoload.php
+++ b/autoload.php
@@ -629,7 +629,6 @@
        'KkConverter' => __DIR__ . '/languages/classes/LanguageKk.php',
        'KuConverter' => __DIR__ . '/languages/classes/LanguageKu.php',
        'LBFactory' => __DIR__ . '/includes/db/loadbalancer/LBFactory.php',
-       'LBFactoryFake' => __DIR__ . 
'/includes/db/loadbalancer/LBFactoryFake.php',
        'LBFactoryMulti' => __DIR__ . 
'/includes/db/loadbalancer/LBFactoryMulti.php',
        'LBFactorySimple' => __DIR__ . 
'/includes/db/loadbalancer/LBFactorySimple.php',
        'LBFactorySingle' => __DIR__ . 
'/includes/db/loadbalancer/LBFactorySingle.php',
@@ -778,6 +777,7 @@
        'MediaWikiSite' => __DIR__ . '/includes/site/MediaWikiSite.php',
        'MediaWikiTitleCodec' => __DIR__ . 
'/includes/title/MediaWikiTitleCodec.php',
        'MediaWikiVersionFetcher' => __DIR__ . 
'/includes/MediaWikiVersionFetcher.php',
+       'MediaWiki\\MediaWikiServices' => __DIR__ . 
'/includes/MediaWikiServices.php',
        'MediaWiki\\Languages\\Data\\Names' => __DIR__ . 
'/languages/data/Names.php',
        'MediaWiki\\Languages\\Data\\ZhConversion' => __DIR__ . 
'/languages/data/ZhConversion.php',
        'MediaWiki\\Linker\\LinkTarget' => __DIR__ . 
'/includes/linker/LinkTarget.php',
@@ -795,8 +795,13 @@
        'MediaWiki\\Logger\\Monolog\\WikiProcessor' => __DIR__ . 
'/includes/debug/logger/monolog/WikiProcessor.php',
        'MediaWiki\\Logger\\NullSpi' => __DIR__ . 
'/includes/debug/logger/NullSpi.php',
        'MediaWiki\\Logger\\Spi' => __DIR__ . '/includes/debug/logger/Spi.php',
-       'MediaWiki\\MediaWikiServices' => __DIR__ . 
'/includes/MediaWikiServices.php',
+       'MediaWiki\\Services\\DestructibleService' => __DIR__ . 
'/includes/Services/DestructibleService.php',
        'MediaWiki\\Services\\ServiceContainer' => __DIR__ . 
'/includes/Services/ServiceContainer.php',
+       'MediaWiki\\Services\\NoSuchServiceException' => __DIR__ . 
'/includes/Services/NoSuchServiceException.php',
+       'MediaWiki\\Services\\CannotReplaceActiveServiceException' => __DIR__ . 
'/includes/Services/CannotReplaceActiveServiceException.php',
+       'MediaWiki\\Services\\ServiceAlreadyDefinedException' => __DIR__ . 
'/includes/Services/ServiceAlreadyDefinedException.php',
+       'MediaWiki\\Services\\ServiceDisabledException' => __DIR__ . 
'/includes/Services/ServiceDisabledException.php',
+       'MediaWiki\\Services\\ContainerDisabledException' => __DIR__ . 
'/includes/Services/ContainerDisabledException.php',
        'MediaWiki\\Session\\BotPasswordSessionProvider' => __DIR__ . 
'/includes/session/BotPasswordSessionProvider.php',
        'MediaWiki\\Session\\CookieSessionProvider' => __DIR__ . 
'/includes/session/CookieSessionProvider.php',
        'MediaWiki\\Session\\ImmutableSessionProviderWithCookie' => __DIR__ . 
'/includes/session/ImmutableSessionProviderWithCookie.php',
diff --git a/docs/hooks.txt b/docs/hooks.txt
index 31e9d92..a7092ec 100644
--- a/docs/hooks.txt
+++ b/docs/hooks.txt
@@ -1997,9 +1997,10 @@
 $request: $wgRequest
 $mediaWiki: The $mediawiki object
 
-'MediaWikiServices': Override services in the default MediaWikiServices 
instance.
-Extensions may use this to define, replace, or wrap existing services.
-However, the preferred way to define a new service is the 
$wgServiceWiringFiles array.
+'MediaWikiServices': Called when a global MediaWikiServices instance is
+initialized. Extensions may use this to define, replace, or wrap services.
+However, the preferred way to define a new service is
+the $wgServiceWiringFiles array.
 $services: MediaWikiServices
 
 'MessageCache::get': When fetching a message. Can be used to override the key
diff --git a/docs/injection.txt b/docs/injection.txt
index e0466c4..2badea9 100644
--- a/docs/injection.txt
+++ b/docs/injection.txt
@@ -60,6 +60,27 @@
 entry points" such as hook handler functions. See "Migration" below.
 
 
+== Service Reset ==
+
+Services get their configuration injected, and changes to global
+configuration variables will not have any effect on services that were already
+instantiated. This would typically be the case for low level services like
+the ConfigFactory or the ObjectCacheManager, which are used during extension
+registration. To address this issue, Setup.php resets the global service
+locator instance by calling MediaWikiServices::resetGlobalInstance() once
+configuration and extension registration is complete.
+
+Note that "unmanaged" legacy services services that manage their own singleton
+must not keep references to services managed by MediaWikiServices, to allow a
+clean reset. After the global MediaWikiServices instance got reset, any such
+references would be stale, and using a stale service will result in an error.
+
+Services should either have all dependencies injected and be themselves managed
+by MediaWikiServices, or they should use the Service Locator pattern, accessing
+service instances via the global MediaWikiServices instance state when needed.
+This ensures that no stale service references remain after a reset.
+
+
 == Configuration ==
 
 When the default MediaWikiServices instance is created, a Config object is
diff --git a/includes/GlobalFunctions.php b/includes/GlobalFunctions.php
index 5c42bc2..8c55d9a 100644
--- a/includes/GlobalFunctions.php
+++ b/includes/GlobalFunctions.php
@@ -3109,6 +3109,9 @@
  * Note 2: use $this->getDB() in maintenance scripts that may be invoked by
  * updater to ensure that a proper database is being updated.
  *
+ * @todo Replace calls to wfGetDB with calls to LoadBalancer::getConnection()
+ *       on an injected instance of LoadBalancer.
+ *
  * @return DatabaseBase
  */
 function wfGetDB( $db, $groups = [], $wiki = false ) {
@@ -3118,20 +3121,30 @@
 /**
  * Get a load balancer object.
  *
+ * @deprecated since 1.27, use MediaWikiServices::getDBLoadBalancer()
+ *              or MediaWikiServices::getDBLoadBalancerFactory() instead.
+ *
  * @param string|bool $wiki Wiki ID, or false for the current wiki
  * @return LoadBalancer
  */
 function wfGetLB( $wiki = false ) {
-       return wfGetLBFactory()->getMainLB( $wiki );
+       if ( $wiki === false ) {
+               return 
\MediaWiki\MediaWikiServices::getInstance()->getDBLoadBalancer();
+       } else {
+               $factory = 
\MediaWiki\MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
+               return $factory->getMainLB( $wiki );
+       }
 }
 
 /**
  * Get the load balancer factory object
  *
+ * @deprecated since 1.27, use MediaWikiServices::getDBLoadBalancerFactory() 
instead.
+ *
  * @return LBFactory
  */
 function wfGetLBFactory() {
-       return LBFactory::singleton();
+       return 
\MediaWiki\MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
 }
 
 /**
diff --git a/includes/MediaWikiServices.php b/includes/MediaWikiServices.php
index 1f3d81c..e48ea79 100644
--- a/includes/MediaWikiServices.php
+++ b/includes/MediaWikiServices.php
@@ -1,13 +1,17 @@
 <?php
 namespace MediaWiki;
 
+use Config;
 use ConfigFactory;
 use EventRelayerGroup;
 use GlobalVarConfig;
-use Config;
 use Hooks;
+use LBFactory;
 use Liuggio\StatsdClient\Factory\StatsdDataFactory;
+use LoadBalancer;
 use MediaWiki\Services\ServiceContainer;
+use MWException;
+use ResourceLoader;
 use SearchEngine;
 use SearchEngineConfig;
 use SearchEngineFactory;
@@ -54,6 +58,11 @@
 class MediaWikiServices extends ServiceContainer {
 
        /**
+        * @var MediaWikiServices|null
+        */
+       private static $instance = null;
+
+       /**
         * Returns the global default instance of the top level service locator.
         *
         * The default instance is initialized using the service instantiator 
functions
@@ -66,25 +75,223 @@
         * @return MediaWikiServices
         */
        public static function getInstance() {
-               static $instance = null;
-
-               if ( $instance === null ) {
+               if ( self::$instance === null ) {
                        // NOTE: constructing GlobalVarConfig here is not 
particularly pretty,
                        // but some information from the global scope has to be 
injected here,
                        // even if it's just a file name or database 
credentials to load
                        // configuration from.
-                       $config = new GlobalVarConfig();
-                       $instance = new self( $config );
-
-                       // Load the default wiring from the specified files.
-                       $wiringFiles = $config->get( 'ServiceWiringFiles' );
-                       $instance->loadWiringFiles( $wiringFiles );
-
-                       // Provide a traditional hook point to allow extensions 
to configure services.
-                       Hooks::run( 'MediaWikiServices', [ $instance ] );
+                       $bootstrapConfig = new GlobalVarConfig();
+                       self::$instance = self::newInstance( $bootstrapConfig );
                }
 
+               return self::$instance;
+       }
+
+       /**
+        * Replaces the global MediaWikiServices instance.
+        *
+        * @note This is for use in PHPUnit tests only!
+        *
+        * @throws MWException if called outside of PHPUnit tests.
+        *
+        * @param MediaWikiServices $services The new MediaWikiServices object.
+        *
+        * @return MediaWikiServices The old MediaWikiServices object, so it 
can be restored later.
+        */
+       public static function forceGlobalInstance( MediaWikiServices $services 
) {
+               if ( !defined( 'MW_PHPUNIT_TEST' ) ) {
+                       throw new MWException( __METHOD__ . ' must not be used 
outside unit tests.' );
+               }
+
+               $old = self::getInstance();
+               self::$instance = $services;
+
+               return $old;
+       }
+
+       /**
+        * Creates a new instance of MediaWikiServices and sets it as the 
global default
+        * instance. getInstance() will return a different MediaWikiServices 
object
+        * after every call to resetGlobalServiceLocator().
+        *
+        * @warning This should not be used during normal operation. It is 
intended for use
+        * when the configuration has changed significantly since bootstrap 
time, e.g.
+        * during the installation process or during testing.
+        *
+        * @warning Calling resetGlobalServiceLocator() may leave the 
application in an inconsistent
+        * state. Calling this is only safe under the ASSUMPTION that NO 
REFERENCE to
+        * any of the services managed by MediaWikiServices exist. If any 
service objects
+        * managed by the old MediaWikiServices instance remain in use, they 
may INTERFERE
+        * with the operation of the services managed by the new 
MediaWikiServices.
+        * Operating with a mix of services created by the old and the new
+        * MediaWikiServices instance may lead to INCONSISTENCIES and even DATA 
LOSS!
+        * Any class implementing LAZY LOADING is especially prone to this 
problem,
+        * since instances would typically retain a reference to a storage 
layer service.
+        *
+        * @see forceGlobalInstance()
+        * @see resetGlobalInstance()
+        * @see resetBetweenTest()
+        *
+        * @param Config|null $bootstrapConfig The Config object to be 
registered as the
+        *        'BootstrapConfig' service. This has to contain at least the 
information
+        *        needed to set up the 'ConfigFactory' service. If not given, 
the bootstrap
+        *        config of the old instance of MediaWikiServices will be 
re-used. If there
+        *        was no previous instance, a new GlobalVarConfig object will 
be used to
+        *        bootstrap the services.
+        *
+        * @throws MWException If called after MW_SERVICE_BOOTSTRAP_COMPLETE 
has been defined in
+        *         Setup.php (unless MW_PHPUNIT_TEST or MEDIAWIKI_INSTALL or 
RUN_MAINTENANCE_IF_MAIN
+        *          is defined).
+        */
+       public static function resetGlobalInstance( Config $bootstrapConfig = 
null ) {
+               if ( self::$instance === null ) {
+                       // no global instance yet, nothing to reset
+                       return;
+               }
+
+               self::failIfResetNotAllowed( __METHOD__ );
+
+               if ( $bootstrapConfig === null ) {
+                       $bootstrapConfig = 
self::$instance->getBootstrapConfig();
+               }
+
+               self::$instance->destroy();
+
+               self::$instance = self::newInstance( $bootstrapConfig );
+       }
+
+       /**
+        * Creates a new MediaWikiServices instance and initializes it 
according to the
+        * given $bootstrapConfig. In particular, all wiring files defined in 
the
+        * ServiceWiringFiles setting are loaded, and the MediaWikiServices 
hook is called.
+        *
+        * @param Config|null $bootstrapConfig The Config object to be 
registered as the
+        *        'BootstrapConfig' service. This has to contain at least the 
information
+        *        needed to set up the 'ConfigFactory' service. If not 
provided, any call
+        *        to getBootstrapConfig(), getConfigFactory, or getMainConfig 
will fail.
+        *        A MediaWikiServices instance without access to configuration 
is called
+        *        "primordial".
+        *
+        * @return MediaWikiServices
+        * @throws MWException
+        */
+       private static function newInstance( Config $bootstrapConfig ) {
+               $instance = new self( $bootstrapConfig );
+
+               // Load the default wiring from the specified files.
+               $wiringFiles = $bootstrapConfig->get( 'ServiceWiringFiles' );
+               $instance->loadWiringFiles( $wiringFiles );
+
+               // Provide a traditional hook point to allow extensions to 
configure services.
+               Hooks::run( 'MediaWikiServices', [ $instance ] );
+
                return $instance;
+       }
+
+       /**
+        * Disables all storage layer services. After calling this, any attempt 
to access the
+        * storage layer will result in an error. Use resetGlobalInstance() to 
restore normal
+        * operation.
+        *
+        * @warning This is intended for extreme situations only and should 
never be used
+        * while serving normal web requests. Legitimate use cases for this 
method include
+        * the installation process. Test fixtures may also use this, if the 
fixture relies
+        * on globalState.
+        *
+        * @see resetGlobalInstance()
+        * @see resetChildProcessServices()
+        */
+       public static function disableStorageBackend() {
+               // TODO: also disable some Caches, JobQueues, etc
+               $destroy = [ 'DBLoadBalancer', 'DBLoadBalancerFactory' ];
+               $services = self::getInstance();
+
+               foreach ( $destroy as $name ) {
+                       $services->disableService( $name );
+               }
+       }
+
+       /**
+        * Resets any services that may have become stale after a child process
+        * returns from after pcntl_fork(). It's also safe, but generally 
unnecessary,
+        * to call this method from the parent process.
+        *
+        * @note This is intended for use in the context of process forking 
only!
+        *
+        * @see resetGlobalInstance()
+        * @see disableStorageBackend()
+        */
+       public static function resetChildProcessServices() {
+               // NOTE: for now, just reset everything. Since we don't know 
the interdependencies
+               // between services, we can't do this more selectively at this 
time.
+               self::resetGlobalInstance();
+
+               // Child, reseed because there is no bug in PHP:
+               // http://bugs.php.net/bug.php?id=42465
+               mt_srand( getmypid() );
+       }
+
+       /**
+        * Resets the given service for testing purposes.
+        *
+        * @warning This is generally unsafe! Other services may still retain 
references
+        * to the stale service instance, leading to failures and 
inconsistencies. Subclasses
+        * may use this method to reset specific services under specific 
instances, but
+        * it should not be exposed to application logic.
+        *
+        * @note With proper dependency injection used throughout the codebase, 
this method
+        * should not be needed. It is provided to allow tests that pollute 
global service
+        * instances to clean up.
+        *
+        * @param string $name
+        * @param string $destroy Whether the service instance should be 
destroyed if it exists.
+        *        When set to false, any existing service instance will 
effectively be detached
+        *        from the container.
+        *
+        * @throws MWException if called outside of PHPUnit tests.
+        */
+       public function resetServiceForTesting( $name, $destroy = true ) {
+               if ( !defined( 'MW_PHPUNIT_TEST' ) && !defined( 
'MW_PARSER_TEST' ) ) {
+                       throw new MWException( 'resetServiceForTesting() must 
not be used outside unit tests.' );
+               }
+
+               $this->resetService( $name, $destroy );
+       }
+
+       /**
+        * Convenience method that throws an exception unless it is called 
during a phase in which
+        * resetting of global services is allowed. In general, services should 
not be reset
+        * individually, since that may introduce inconsistencies.
+        *
+        * This method will throw an exception if:
+        *
+        * - self::$resetInProgress is false (to allow all services to be reset 
together
+        *   via resetGlobalInstance)
+        * - and MEDIAWIKI_INSTALL is not defined (to allow services to be 
reset during installation)
+        * - and MW_PHPUNIT_TEST is not defined (to allow services to be reset 
during testing)
+        *
+        * This method is intended to be used to safeguard against accidentally 
resetting
+        * global service instances that are not yet managed by 
MediaWikiServices. It is
+        * defined here in the MediaWikiServices services class to have a 
central place
+        * for managing service bootstrapping and resetting.
+        *
+        * @param string $method the name of the caller method, as given by 
__METHOD__.
+        *
+        * @throws MWException if called outside bootstrap mode.
+        *
+        * @see resetGlobalInstance()
+        * @see forceGlobalInstance()
+        * @see disableStorageBackend()
+        */
+       public static function failIfResetNotAllowed( $method ) {
+               if ( !defined( 'MW_PHPUNIT_TEST' )
+                       && !defined( 'MW_PARSER_TEST' )
+                       && !defined( 'MEDIAWIKI_INSTALL' )
+                       && !defined( 'RUN_MAINTENANCE_IF_MAIN' )
+                       && defined( 'MW_SERVICE_BOOTSTRAP_COMPLETE' )
+               ) {
+                       throw new MWException( $method . ' may only be called 
during bootstrapping and unit tests!' );
+               }
        }
 
        /**
@@ -95,11 +302,13 @@
        public function __construct( Config $config ) {
                parent::__construct();
 
-               // register the given Config object as the bootstrap config 
service.
+               // Register the given Config object as the bootstrap config 
service.
                $this->defineService( 'BootstrapConfig', function() use ( 
$config ) {
                        return $config;
                } );
        }
+
+       // CONVENIENCE GETTERS 
////////////////////////////////////////////////////
 
        /**
         * Returns the Config object containing the bootstrap configuration.
@@ -191,6 +400,20 @@
                return $this->getService( 'SkinFactory' );
        }
 
+       /**
+        * @return LBFactory
+        */
+       public function getDBLoadBalancerFactory() {
+               return $this->getService( 'DBLoadBalancerFactory' );
+       }
+
+       /**
+        * @return LoadBalancer The main DB load balancer for the local wiki.
+        */
+       public function getDBLoadBalancer() {
+               return $this->getService( 'DBLoadBalancer' );
+       }
+
        
///////////////////////////////////////////////////////////////////////////
        // NOTE: When adding a service getter here, don't forget to add a test
        // case for it in MediaWikiServicesTest::provideGetters() and in
diff --git a/includes/ServiceWiring.php b/includes/ServiceWiring.php
index b3de172..1f26b58 100644
--- a/includes/ServiceWiring.php
+++ b/includes/ServiceWiring.php
@@ -40,9 +40,25 @@
 use MediaWiki\MediaWikiServices;
 
 return [
+       'DBLoadBalancerFactory' => function( MediaWikiServices $services ) {
+               $config = $services->getMainConfig()->get( 'LBFactoryConf' );
+
+               $class = LBFactory::getLBFactoryClass( $config );
+               if ( !isset( $config['readOnlyReason'] ) ) {
+                       // TODO: replace the global 
wfConfiguredReadOnlyReason() with a service.
+                       $config['readOnlyReason'] = 
wfConfiguredReadOnlyReason();
+               }
+
+               return new $class( $config );
+       },
+
+       'DBLoadBalancer' => function( MediaWikiServices $services ) {
+               // just return the default LB from the DBLoadBalancerFactory 
service
+               return $services->getDBLoadBalancerFactory()->getMainLB();
+       },
+
        'SiteStore' => function( MediaWikiServices $services ) {
-               $loadBalancer = wfGetLB(); // TODO: use LB from 
MediaWikiServices
-               $rawSiteStore = new DBSiteStore( $loadBalancer );
+               $rawSiteStore = new DBSiteStore( $services->getDBLoadBalancer() 
);
 
                // TODO: replace wfGetCache with a CacheFactory service.
                // TODO: replace wfIsHHVM with a capabilities service.
@@ -92,7 +108,26 @@
        },
 
        'SkinFactory' => function( MediaWikiServices $services ) {
-               return new SkinFactory();
+               $factory = new SkinFactory();
+
+               $names = $services->getMainConfig()->get( 'ValidSkinNames' );
+
+               foreach ( $names as $name => $skin ) {
+                       $factory->register( $name, $skin, function () use ( 
$name, $skin ) {
+                               $class = "Skin$skin";
+                               return new $class( $name );
+                       } );
+               }
+               // Register a hidden "fallback" skin
+               $factory->register( 'fallback', 'Fallback', function () {
+                       return new SkinFallback;
+               } );
+               // Register a hidden skin for api output
+               $factory->register( 'apioutput', 'ApiOutput', function () {
+                       return new SkinApi;
+               } );
+
+               return $factory;
        },
 
        
///////////////////////////////////////////////////////////////////////////
diff --git a/includes/Services/CannotReplaceActiveServiceException.php 
b/includes/Services/CannotReplaceActiveServiceException.php
new file mode 100644
index 0000000..4993073
--- /dev/null
+++ b/includes/Services/CannotReplaceActiveServiceException.php
@@ -0,0 +1,43 @@
+<?php
+namespace MediaWiki\Services;
+
+use Exception;
+use RuntimeException;
+
+/**
+ * Exception thrown when trying to replace an already active service.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ *
+ * @since 1.27
+ */
+
+/**
+ * Exception thrown when trying to replace an already active service.
+ */
+class CannotReplaceActiveServiceException extends RuntimeException {
+
+       /**
+        * @param string $serviceName
+        * @param Exception|null $previous
+        */
+       public function __construct( $serviceName, Exception $previous = null ) 
{
+               parent::__construct( "Cannot replace an active service: 
$serviceName", 0, $previous );
+       }
+
+}
diff --git a/includes/Services/ContainerDisabledException.php 
b/includes/Services/ContainerDisabledException.php
new file mode 100644
index 0000000..ede076d
--- /dev/null
+++ b/includes/Services/ContainerDisabledException.php
@@ -0,0 +1,42 @@
+<?php
+namespace MediaWiki\Services;
+
+use Exception;
+use RuntimeException;
+
+/**
+ * Exception thrown when trying to access a service on a disabled container or 
factory.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ *
+ * @since 1.27
+ */
+
+/**
+ * Exception thrown when trying to access a service on a disabled container or 
factory.
+ */
+class ContainerDisabledException extends RuntimeException {
+
+       /**
+        * @param Exception|null $previous
+        */
+       public function __construct( Exception $previous = null ) {
+               parent::__construct( 'Container disabled!', 0, $previous );
+       }
+
+}
diff --git a/includes/Services/DestructibleService.php 
b/includes/Services/DestructibleService.php
new file mode 100644
index 0000000..6ce9af2
--- /dev/null
+++ b/includes/Services/DestructibleService.php
@@ -0,0 +1,45 @@
+<?php
+namespace MediaWiki\Services;
+
+/**
+ * Interface for destructible services.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ *
+ * @since 1.27
+ */
+
+/**
+ * DestructibleService defines a standard interface for shutting down a 
service instance.
+ * The intended use is for a service container to be able to shut down 
services that should
+ * no longer be used, and allow such services to release any system resources.
+ *
+ * @note There is no expectation that services will be destroyed when the 
process (or web request)
+ * terminates.
+ */
+interface DestructibleService {
+
+       /**
+        * Notifies the service object that it should expect to no longer be 
used, and should release
+        * any system resources it may own. The behavior of all service methods 
becomes undefined after
+        * destroy() has been called. It is recommended that implementing 
classes should throw an
+        * exception when service methods are accessed after destroy() has been 
called.
+        */
+       public function destroy();
+
+}
diff --git a/includes/Services/NoSuchServiceException.php 
b/includes/Services/NoSuchServiceException.php
new file mode 100644
index 0000000..36e50d2
--- /dev/null
+++ b/includes/Services/NoSuchServiceException.php
@@ -0,0 +1,43 @@
+<?php
+namespace MediaWiki\Services;
+
+use Exception;
+use RuntimeException;
+
+/**
+ * Exception thrown when the requested service is not known.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ *
+ * @since 1.27
+ */
+
+/**
+ * Exception thrown when the requested service is not known.
+ */
+class NoSuchServiceException extends RuntimeException {
+
+       /**
+        * @param string $serviceName
+        * @param Exception|null $previous
+        */
+       public function __construct( $serviceName, Exception $previous = null ) 
{
+               parent::__construct( "No such service: $serviceName", 0, 
$previous );
+       }
+
+}
diff --git a/includes/Services/ServiceAlreadyDefinedException.php 
b/includes/Services/ServiceAlreadyDefinedException.php
new file mode 100644
index 0000000..c6344d3
--- /dev/null
+++ b/includes/Services/ServiceAlreadyDefinedException.php
@@ -0,0 +1,45 @@
+<?php
+namespace MediaWiki\Services;
+
+use Exception;
+use RuntimeException;
+
+/**
+ * Exception thrown when a service was already defined, but the
+ * caller expected it to not exist.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ *
+ * @since 1.27
+ */
+
+/**
+ * Exception thrown when a service was already defined, but the
+ * caller expected it to not exist.
+ */
+class ServiceAlreadyDefinedException extends RuntimeException {
+
+       /**
+        * @param string $serviceName
+        * @param Exception|null $previous
+        */
+       public function __construct( $serviceName, Exception $previous = null ) 
{
+               parent::__construct( "Service already defined: $serviceName", 
0, $previous );
+       }
+
+}
diff --git a/includes/Services/ServiceContainer.php 
b/includes/Services/ServiceContainer.php
index e3cda2e..66ee918 100644
--- a/includes/Services/ServiceContainer.php
+++ b/includes/Services/ServiceContainer.php
@@ -43,7 +43,7 @@
  * @see docs/injection.txt for an overview of using dependency injection in the
  *      MediaWiki code base.
  */
-class ServiceContainer {
+class ServiceContainer implements DestructibleService {
 
        /**
         * @var object[]
@@ -61,12 +61,36 @@
        private $extraInstantiationParams;
 
        /**
+        * @var boolean
+        */
+       private $destroyed = false;
+
+       /**
         * @param array $extraInstantiationParams Any additional parameters to 
be passed to the
         * instantiator function when creating a service. This is typically 
used to provide
         * access to additional ServiceContainers or Config objects.
         */
        public function __construct( array $extraInstantiationParams = [] ) {
                $this->extraInstantiationParams = $extraInstantiationParams;
+       }
+
+       /**
+        * Destroys all contained service instances that implement the 
DestructibleService
+        * interface. This will render all services obtained from this 
MediaWikiServices
+        * instance unusable. In particular, this will disable access to the 
storage backend
+        * via any of these services. Any future call to getService() will 
throw an exception.
+        *
+        * @see resetGlobalInstance()
+        */
+       public function destroy() {
+               foreach ( $this->getServiceNames() as $name ) {
+                       $service = $this->peekService( $name );
+                       if ( $service !== null && $service instanceof 
DestructibleService ) {
+                               $service->destroy();
+                       }
+               }
+
+               $this->destroyed = true;
        }
 
        /**
@@ -115,6 +139,29 @@
        }
 
        /**
+        * Returns the service instance for $name only if that service has 
already been instantiated.
+        * This is intended for situations where services get destroyed/cleaned 
up, so we can
+        * avoid creating a service just to destroy it again.
+        *
+        * @note This is intended for internal use and for test fixtures.
+        * Application logic should use getService() instead.
+        *
+        * @see getService().
+        *
+        * @param string $name
+        *
+        * @return object|null The service instance, or null if the service has 
not yet been instantiated.
+        * @throws RuntimeException if $name does not refer to a known service.
+        */
+       public function peekService( $name ) {
+               if ( !$this->hasService( $name ) ) {
+                       throw new NoSuchServiceException( $name );
+               }
+
+               return isset( $this->services[$name] ) ? $this->services[$name] 
: null;
+       }
+
+       /**
         * @return string[]
         */
        public function getServiceNames() {
@@ -139,7 +186,7 @@
                Assert::parameterType( 'string', $name, '$name' );
 
                if ( $this->hasService( $name ) ) {
-                       throw new RuntimeException( 'Service already defined: ' 
. $name );
+                       throw new ServiceAlreadyDefinedException( $name );
                }
 
                $this->serviceInstantiators[$name] = $instantiator;
@@ -165,14 +212,76 @@
                Assert::parameterType( 'string', $name, '$name' );
 
                if ( !$this->hasService( $name ) ) {
-                       throw new RuntimeException( 'Service not defined: ' . 
$name );
+                       throw new NoSuchServiceException( $name );
                }
 
                if ( isset( $this->services[$name] ) ) {
-                       throw new RuntimeException( 'Cannot redefine a service 
that is already in use: ' . $name );
+                       throw new CannotReplaceActiveServiceException( $name );
                }
 
                $this->serviceInstantiators[$name] = $instantiator;
+       }
+
+       /**
+        * Disables a service.
+        *
+        * @note Attempts to call getService() for a disabled service will 
result
+        * in a DisabledServiceException. Calling peekService for a disabled 
service will
+        * return null. Disabled services are listed by getServiceNames(). A 
disabled service
+        * can be enabled again using redefineService().
+        *
+        * @note If the service was already active (that is, instantiated) when 
getting disabled,
+        * and the service instance implements DestructibleService, destroy() 
is called on the
+        * service instance.
+        *
+        * @see redefineService()
+        * @see resetService()
+        *
+        * @param string $name The name of the service to disable.
+        *
+        * @throws RuntimeException if $name is not a known service.
+        */
+       public function disableService( $name ) {
+               $this->resetService( $name );
+
+               $this->redefineService( $name, function() use ( $name ) {
+                       throw new ServiceDisabledException( $name );
+               } );
+       }
+
+       /**
+        * Resets a service by dropping the service instance.
+        * If the service instances implements DestructibleService, destroy()
+        * is called on the service instance.
+        *
+        * @warning This is generally unsafe! Other services may still retain 
references
+        * to the stale service instance, leading to failures and 
inconsistencies. Subclasses
+        * may use this method to reset specific services under specific 
instances, but
+        * it should not be exposed to application logic.
+        *
+        * @note This is declared final so subclasses can not interfere with 
the expectations
+        * disableService() has when calling resetService().
+        *
+        * @see redefineService()
+        * @see disableService().
+        *
+        * @param string $name The name of the service to reset.
+        * @param bool $destroy Whether the service instance should be 
destroyed if it exists.
+        *        When set to false, any existing service instance will 
effectively be detached
+        *        from the container.
+        *
+        * @throws RuntimeException if $name is not a known service.
+        */
+       final protected function resetService( $name, $destroy = true ) {
+               Assert::parameterType( 'string', $name, '$name' );
+
+               $instance = $this->peekService( $name );
+
+               if ( $destroy && $instance instanceof DestructibleService )  {
+                       $instance->destroy();
+               }
+
+               unset( $this->services[$name] );
        }
 
        /**
@@ -189,10 +298,16 @@
         *
         * @param string $name The service name
         *
-        * @throws InvalidArgumentException if $name is not a known service.
+        * @throws NoSuchServiceException if $name is not a known service.
+        * @throws ServiceDisabledException if this container has already been 
destroyed.
+        *
         * @return object The service instance
         */
        public function getService( $name ) {
+               if ( $this->destroyed ) {
+                       throw new ContainerDisabledException();
+               }
+
                if ( !isset( $this->services[$name] ) ) {
                        $this->services[$name] = $this->createService( $name );
                }
@@ -213,7 +328,7 @@
                                array_merge( [ $this ], 
$this->extraInstantiationParams )
                        );
                } else {
-                       throw new InvalidArgumentException( 'Unknown service: ' 
. $name );
+                       throw new NoSuchServiceException( $name );
                }
 
                return $service;
diff --git a/includes/Services/ServiceDisabledException.php 
b/includes/Services/ServiceDisabledException.php
new file mode 100644
index 0000000..ae15b7c
--- /dev/null
+++ b/includes/Services/ServiceDisabledException.php
@@ -0,0 +1,43 @@
+<?php
+namespace MediaWiki\Services;
+
+use Exception;
+use RuntimeException;
+
+/**
+ * Exception thrown when trying to access a disabled service.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ *
+ * @since 1.27
+ */
+
+/**
+ * Exception thrown when trying to access a disabled service.
+ */
+class ServiceDisabledException extends RuntimeException {
+
+       /**
+        * @param string $serviceName
+        * @param Exception|null $previous
+        */
+       public function __construct( $serviceName, Exception $previous = null ) 
{
+               parent::__construct( "Service disabled: $serviceName", 0, 
$previous );
+       }
+
+}
diff --git a/includes/Setup.php b/includes/Setup.php
index 9898b84..9db997a 100644
--- a/includes/Setup.php
+++ b/includes/Setup.php
@@ -23,6 +23,7 @@
  *
  * @file
  */
+use MediaWiki\MediaWikiServices;
 
 /**
  * This file is not a valid entry point, perform no further processing unless
@@ -290,25 +291,6 @@
        $wgSkipSkins[] = $wgSkipSkin;
 }
 
-// Register skins
-// Use a closure to avoid leaking into global state
-call_user_func( function () use ( $wgValidSkinNames ) {
-       $factory = SkinFactory::getDefaultInstance();
-       foreach ( $wgValidSkinNames as $name => $skin ) {
-               $factory->register( $name, $skin, function () use ( $name, 
$skin ) {
-                       $class = "Skin$skin";
-                       return new $class( $name );
-               } );
-       }
-       // Register a hidden "fallback" skin
-       $factory->register( 'fallback', 'Fallback', function () {
-               return new SkinFallback;
-       } );
-       // Register a hidden skin for api output
-       $factory->register( 'apioutput', 'ApiOutput', function () {
-               return new SkinApi;
-       } );
-} );
 $wgSkipSkins[] = 'fallback';
 $wgSkipSkins[] = 'apioutput';
 
@@ -517,6 +499,14 @@
        require_once "$IP/includes/AutoLoader.php";
 }
 
+// Reset the global service locator, so any services that have already been 
created will be
+// re-created while taking into account any custom settings and extensions.
+MediaWikiServices::resetGlobalInstance( new GlobalVarConfig() );
+
+// Define a constant that indicates that the bootstrapping of the service 
locator
+// is complete.
+define( 'MW_SERVICE_BOOTSTRAP_COMPLETE', 1 );
+
 // Install a header callback to prevent caching of responses with cookies 
(T127993)
 if ( !$wgCommandLineMode ) {
        header_register_callback( function () {
diff --git a/includes/config/ConfigFactory.php 
b/includes/config/ConfigFactory.php
index 4b803d8..09b0baa 100644
--- a/includes/config/ConfigFactory.php
+++ b/includes/config/ConfigFactory.php
@@ -51,16 +51,39 @@
        }
 
        /**
-        * Register a new config factory function
-        * Will override if it's already registered
+        * @return string[]
+        */
+       public function getConfigNames() {
+               return array_keys( $this->factoryFunctions );
+       }
+
+       /**
+        * Register a new config factory function.
+        * Will override if it's already registered.
+        * Use "*" for $name to provide a fallback config for all unknown names.
         * @param string $name
-        * @param callable $callback That takes this ConfigFactory as an 
argument
+        * @param callable|Config $callback A factory callabck that takes this 
ConfigFactory
+        *        as an argument and returns a Config instance, or an existing 
Config instance.
         * @throws InvalidArgumentException If an invalid callback is provided
         */
        public function register( $name, $callback ) {
+               if ( $callback instanceof Config ) {
+                       $instance = $callback;
+
+                       // Register a callback anyway, for consistency. Note 
that getConfigNames()
+                       // relies on $factoryFunctions to have all config names.
+                       $callback = function() use ( $instance ) {
+                               return $instance;
+                       };
+               } else {
+                       $instance = null;
+               }
+
                if ( !is_callable( $callback ) ) {
                        throw new InvalidArgumentException( 'Invalid callback 
provided' );
                }
+
+               $this->configs[$name] = $instance;
                $this->factoryFunctions[$name] = $callback;
        }
 
@@ -75,10 +98,14 @@
         */
        public function makeConfig( $name ) {
                if ( !isset( $this->configs[$name] ) ) {
-                       if ( !isset( $this->factoryFunctions[$name] ) ) {
+                       $key = $name;
+                       if ( !isset( $this->factoryFunctions[$key] ) ) {
+                               $key = '*';
+                       }
+                       if ( !isset( $this->factoryFunctions[$key] ) ) {
                                throw new ConfigException( "No registered 
builder available for $name." );
                        }
-                       $conf = call_user_func( $this->factoryFunctions[$name], 
$this );
+                       $conf = call_user_func( $this->factoryFunctions[$key], 
$this );
                        if ( $conf instanceof Config ) {
                                $this->configs[$name] = $conf;
                        } else {
@@ -88,4 +115,5 @@
 
                return $this->configs[$name];
        }
+
 }
diff --git a/includes/db/loadbalancer/LBFactory.php 
b/includes/db/loadbalancer/LBFactory.php
index f39596b..b78793f 100644
--- a/includes/db/loadbalancer/LBFactory.php
+++ b/includes/db/loadbalancer/LBFactory.php
@@ -21,6 +21,8 @@
  * @ingroup Database
  */
 
+use MediaWiki\MediaWikiServices;
+use MediaWiki\Services\DestructibleService;
 use Psr\Log\LoggerInterface;
 use MediaWiki\Logger\LoggerFactory;
 
@@ -28,7 +30,8 @@
  * An interface for generating database load balancers
  * @ingroup Database
  */
-abstract class LBFactory {
+abstract class LBFactory implements DestructibleService {
+
        /** @var ChronologyProtector */
        protected $chronProt;
 
@@ -37,9 +40,6 @@
 
        /** @var LoggerInterface */
        protected $logger;
-
-       /** @var LBFactory */
-       private static $instance;
 
        /** @var string|bool Reason all LBs are read-only or false if not */
        protected $readOnlyReason = false;
@@ -61,36 +61,38 @@
        }
 
        /**
+        * Disables all load balancers. All connections are closed, and any 
attempt to
+        * open a new connection will result in a DBAccessError.
+        * @see LoadBalancer::disable()
+        */
+       public function destroy() {
+               $this->shutdown();
+               $this->forEachLBCallMethod( 'disable' );
+       }
+
+       /**
         * Disables all access to the load balancer, will cause all database 
access
         * to throw a DBAccessError
         */
        public static function disableBackend() {
-               global $wgLBFactoryConf;
-               self::$instance = new LBFactoryFake( $wgLBFactoryConf );
+               MediaWikiServices::disableStorageBackend();
        }
 
        /**
         * Get an LBFactory instance
         *
+        * @deprecated since 1.27, use 
MediaWikiServices::getDBLoadBalancerFactory() instead.
+        *
         * @return LBFactory
         */
        public static function singleton() {
-               global $wgLBFactoryConf;
-
-               if ( is_null( self::$instance ) ) {
-                       $class = self::getLBFactoryClass( $wgLBFactoryConf );
-                       $config = $wgLBFactoryConf;
-                       if ( !isset( $config['readOnlyReason'] ) ) {
-                               $config['readOnlyReason'] = 
wfConfiguredReadOnlyReason();
-                       }
-                       self::$instance = new $class( $config );
-               }
-
-               return self::$instance;
+               return 
MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
        }
 
        /**
         * Returns the LBFactory class to use and the load balancer 
configuration.
+        *
+        * @todo instead of this, use a ServiceContainer for managing the 
different implementations.
         *
         * @param array $config (e.g. $wgLBFactoryConf)
         * @return string Class name
@@ -120,23 +122,11 @@
 
        /**
         * Shut down, close connections and destroy the cached instance.
+        *
+        * @deprecated since 1.27, use LBFactory::destroy()
         */
        public static function destroyInstance() {
-               if ( self::$instance ) {
-                       self::$instance->shutdown();
-                       self::$instance->forEachLBCallMethod( 'closeAll' );
-                       self::$instance = null;
-               }
-       }
-
-       /**
-        * Set the instance to be the given object
-        *
-        * @param LBFactory $instance
-        */
-       public static function setInstance( $instance ) {
-               self::destroyInstance();
-               self::$instance = $instance;
+               self::singleton()->destroy();
        }
 
        /**
@@ -470,7 +460,7 @@
 class DBAccessError extends MWException {
        public function __construct() {
                parent::__construct( "Mediawiki tried to access the database 
via wfGetDB(). " .
-                       "This is not allowed." );
+                       "This is not allowed, because database access has been 
disabled." );
        }
 }
 
diff --git a/includes/db/loadbalancer/LoadBalancer.php 
b/includes/db/loadbalancer/LoadBalancer.php
index 741999c..5578099 100644
--- a/includes/db/loadbalancer/LoadBalancer.php
+++ b/includes/db/loadbalancer/LoadBalancer.php
@@ -78,6 +78,11 @@
        const POS_WAIT_TIMEOUT = 10;
 
        /**
+        * @var boolean
+        */
+       private $disabled = false;
+
+       /**
         * @param array $params Array with keys:
         *  - servers : Required. Array of server info structures.
         *  - loadMonitor : Name of a class used to fetch server lag and load.
@@ -666,6 +671,8 @@
         * On error, returns false, and the connection which caused the
         * error will be available via $this->mErrorConnection.
         *
+        * @note If disable() was called on this LoadBalancer, this method will 
throw a DBAccessError.
+        *
         * @param int $i Server index
         * @param string|bool $wiki Wiki ID, or false for the current wiki
         * @return DatabaseBase|bool Returns false on errors
@@ -715,6 +722,8 @@
         *
         * On error, returns false, and the connection which caused the
         * error will be available via $this->mErrorConnection.
+        *
+        * @note If disable() was called on this LoadBalancer, this method will 
throw a DBAccessError.
         *
         * @param int $i Server index
         * @param string $wiki Wiki ID to open
@@ -803,6 +812,10 @@
         * @return DatabaseBase
         */
        protected function reallyOpenConnection( $server, $dbNameOverride = 
false ) {
+               if ( $this->disabled ) {
+                       throw new DBAccessError();
+               }
+
                if ( !is_array( $server ) ) {
                        throw new MWException( 'You must update your 
load-balancing configuration. ' .
                                'See DefaultSettings.php entry for 
$wgDBservers.' );
@@ -977,6 +990,17 @@
        }
 
        /**
+        * Disable this load balancer. All connections are closed, and any 
attempt to
+        * open a new connection will result in a DBAccessError.
+        *
+        * @since 1.27
+        */
+       public function disable() {
+               $this->closeAll();
+               $this->disabled = true;
+       }
+
+       /**
         * Close all open connections
         */
        public function closeAll() {
diff --git a/includes/installer/DatabaseInstaller.php 
b/includes/installer/DatabaseInstaller.php
index 79bd961..701403e 100644
--- a/includes/installer/DatabaseInstaller.php
+++ b/includes/installer/DatabaseInstaller.php
@@ -287,8 +287,16 @@
                if ( !$status->isOK() ) {
                        throw new MWException( __METHOD__ . ': unexpected DB 
connection error' );
                }
-               LBFactory::setInstance( new LBFactorySingle( [
-                       'connection' => $status->value ] ) );
+
+               \MediaWiki\MediaWikiServices::resetGlobalInstance();
+               $services = \MediaWiki\MediaWikiServices::getInstance();
+
+               $connection = $status->value;
+               $services->redefineService( 'DBLoadBalancerFactory', function() 
use ( $connection ) {
+                       return new LBFactorySingle( [
+                               'connection' => $connection ] );
+               } );
+
        }
 
        /**
diff --git a/includes/installer/Installer.php b/includes/installer/Installer.php
index 3d1c860..97214da 100644
--- a/includes/installer/Installer.php
+++ b/includes/installer/Installer.php
@@ -352,36 +352,66 @@
        abstract public function showStatusMessage( Status $status );
 
        /**
+        * Constructs a Config object that contains configuration settings that 
should be
+        * overwritten for the installation process.
+        *
+        * @since 1.27
+        *
+        * @param Config $baseConfig
+        *
+        * @return Config The config to use during installation.
+        */
+       public static function getInstallerConfig( Config $baseConfig ) {
+               $configOverrides = new HashConfig();
+
+               // disable (problematic) object cache types explicitly, 
preserving all other (working) ones
+               // bug T113843
+               $emptyCache = [ 'class' => 'EmptyBagOStuff' ];
+
+               $objectCaches = [
+                               CACHE_NONE => $emptyCache,
+                               CACHE_DB => $emptyCache,
+                               CACHE_ANYTHING => $emptyCache,
+                               CACHE_MEMCACHED => $emptyCache,
+                       ] + $baseConfig->get( 'ObjectCaches' );
+
+               $configOverrides->set( 'ObjectCaches', $objectCaches );
+
+               // Load the installer's i18n.
+               $messageDirs = $baseConfig->get( 'MessagesDirs' );
+               $messageDirs['MediawikiInstaller'] = __DIR__ . '/i18n';
+
+               $configOverrides->set( 'MessagesDirs', $messageDirs );
+
+               return new MultiConfig( [ $configOverrides, $baseConfig ] );
+       }
+
+       /**
         * Constructor, always call this from child classes.
         */
        public function __construct() {
-               global $wgMessagesDirs, $wgUser;
+               global $wgMemc, $wgUser;
+
+               $defaultConfig = new GlobalVarConfig(); // all the stuff from 
DefaultSettings.php
+               $installerConfig = self::getInstallerConfig( $defaultConfig );
+
+               // Reset all services and inject config overrides
+               MediaWiki\MediaWikiServices::resetGlobalInstance( 
$installerConfig );
 
                // Don't attempt to load user language options (T126177)
                // This will be overridden in the web installer with the 
user-specified language
                RequestContext::getMain()->setLanguage( 'en' );
 
                // Disable the i18n cache
+               // TODO: manage LocalisationCache singleton in MediaWikiServices
                Language::getLocalisationCache()->disableBackend();
-               // Disable LoadBalancer and wfGetDB etc.
-               LBFactory::disableBackend();
+
+               // Disable all global services, since we don't have any 
configuration yet!
+               MediaWiki\MediaWikiServices::disableStorageBackend();
 
                // Disable object cache (otherwise CACHE_ANYTHING will try 
CACHE_DB and
                // SqlBagOStuff will then throw since we just disabled wfGetDB)
-               $GLOBALS['wgMemc'] = new EmptyBagOStuff;
-               ObjectCache::clear();
-               $emptyCache = [ 'class' => 'EmptyBagOStuff' ];
-               // disable (problematic) object cache types explicitly, 
preserving all other (working) ones
-               // bug T113843
-               $GLOBALS['wgObjectCaches'] = [
-                       CACHE_NONE => $emptyCache,
-                       CACHE_DB => $emptyCache,
-                       CACHE_ANYTHING => $emptyCache,
-                       CACHE_MEMCACHED => $emptyCache,
-               ] + $GLOBALS['wgObjectCaches'];
-
-               // Load the installer's i18n.
-               $wgMessagesDirs['MediawikiInstaller'] = __DIR__ . '/i18n';
+               $wgMemc = ObjectCache::getInstance( CACHE_NONE );
 
                // Having a user with id = 0 safeguards us from DB access via 
User::loadOptions().
                $wgUser = User::newFromId( 0 );
diff --git a/tests/phpunit/MediaWikiTestCase.php 
b/tests/phpunit/MediaWikiTestCase.php
index d846b57..25e0e31 100644
--- a/tests/phpunit/MediaWikiTestCase.php
+++ b/tests/phpunit/MediaWikiTestCase.php
@@ -2,12 +2,23 @@
 use MediaWiki\Logger\LegacySpi;
 use MediaWiki\Logger\LoggerFactory;
 use MediaWiki\Logger\MonologSpi;
+use MediaWiki\MediaWikiServices;
 use Psr\Log\LoggerInterface;
 
 /**
  * @since 1.18
  */
 abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase {
+
+       /**
+        * The service locator created by prepareServices(). This service 
locator will
+        * be restored after each test. Tests that pollute the global service 
locator
+        * instance should use overrideMwServices() to isolate the test.
+        *
+        * @var MediaWikiServices|null
+        */
+       private static $serviceLocator = null;
+
        /**
         * $called tracks whether the setUp and tearDown method has been called.
         * class extending MediaWikiTestCase usually override setUp and tearDown
@@ -108,18 +119,202 @@
                }
        }
 
-       public function run( PHPUnit_Framework_TestResult $result = null ) {
+       public static function setUpBeforeClass() {
+               parent::setUpBeforeClass();
+
+               // NOTE: Usually, PHPUnitMaintClass::finalSetup already called 
this,
+               // but let's make doubly sure.
+               self::prepareServices( new GlobalVarConfig() );
+       }
+
+       /**
+        * Prepare service configuration for unit testing.
+        *
+        * This calls MediaWikiServices::resetGlobalInstance() to allow some 
critical services
+        * to be overridden for testing.
+        *
+        * prepareServices() only needs to be called once, but should be called 
as early as possible,
+        * before any class has a chance to grab a reference to any of the 
global services
+        * instances that get discarded by prepareServices(). Only the first 
call has any effect,
+        * later calls are ignored.
+        *
+        * @note This is called by PHPUnitMaintClass::finalSetup.
+        *
+        * @see MediaWikiServices::resetGlobalInstance()
+        *
+        * @param Config $bootstrapConfig The bootstrap config to use with the 
new
+        *        MediaWikiServices. Only used for the first call to this 
method.
+        */
+       public static function prepareServices( Config $bootstrapConfig ) {
+               static $servicesPrepared = false;
+
+               if ( $servicesPrepared ) {
+                       return;
+               } else {
+                       $servicesPrepared = true;
+               }
+
+               self::resetGlobalServices( $bootstrapConfig );
+       }
+
+       /**
+        * Reset global services, and install testing environment.
+        * This is the testing equivalent of 
MediaWikiServices::resetGlobalInstance().
+        * This should only be used to set up the testing environment, not when
+        * running unit tests. Use overrideMwServices() for that.
+        *
+        * @see MediaWikiServices::resetGlobalInstance()
+        * @see prepareServices()
+        * @see overrideMwServices()
+        *
+        * @param Config|null $bootstrapConfig The bootstrap config to use with 
the new
+        *        MediaWikiServices.
+        */
+       protected static function resetGlobalServices( Config $bootstrapConfig 
= null ) {
+               $oldServices = MediaWikiServices::getInstance();
+               $oldConfigFactory = $oldServices->getConfigFactory();
+
+               $testConfig = self::makeTestConfig( $bootstrapConfig );
+
+               MediaWikiServices::resetGlobalInstance( $testConfig );
+
+               self::$serviceLocator = MediaWikiServices::getInstance();
+               self::installTestServices(
+                       $oldConfigFactory,
+                       self::$serviceLocator
+               );
+       }
+
+       /**
+        * Create a config suitable for testing, based on a base config, 
default overrides,
+        * and custom overrides.
+        *
+        * @param Config|null $baseConfig
+        * @param Config|null $customOverrides
+        *
+        * @return Config
+        */
+       private static function makeTestConfig(
+               Config $baseConfig = null,
+               Config $customOverrides = null
+       ) {
+               $defaultOverrides = new HashConfig();
+
+               if ( !$baseConfig ) {
+                       $baseConfig = 
MediaWikiServices::getInstance()->getBootstrapConfig();
+               }
+
                /* Some functions require some kind of caching, and will end up 
using the db,
                 * which we can't allow, as that would open a new connection 
for mysql.
                 * Replace with a HashBag. They would not be going to persist 
anyway.
                 */
-               ObjectCache::$instances[CACHE_DB] = new HashBagOStuff;
+               $hashCache = [ 'class' => 'HashBagOStuff' ];
+               $objectCaches = [
+                               CACHE_DB => $hashCache,
+                               CACHE_ACCEL => $hashCache,
+                               CACHE_MEMCACHED => $hashCache,
+                               'apc' => $hashCache,
+                               'xcache' => $hashCache,
+                               'wincache' => $hashCache,
+                       ] + $baseConfig->get( 'ObjectCaches' );
 
-               // Sandbox APC by replacing with in-process hash instead.
-               // Ensures values are removed between tests.
-               ObjectCache::$instances['apc'] =
-               ObjectCache::$instances['xcache'] =
-               ObjectCache::$instances['wincache'] = new HashBagOStuff;
+               $defaultOverrides->set( 'ObjectCaches', $objectCaches );
+               $defaultOverrides->set( 'MainCacheType', CACHE_NONE );
+
+               $testConfig = $customOverrides
+                       ? new MultiConfig( [ $customOverrides, 
$defaultOverrides, $baseConfig ] )
+                       : new MultiConfig( [ $defaultOverrides, $baseConfig ] );
+
+               return $testConfig;
+       }
+
+       /**
+        * @param ConfigFactory $oldConfigFactory
+        * @param MediaWikiServices $newServices
+        *
+        * @throws MWException
+        */
+       private static function installTestServices(
+               ConfigFactory $oldConfigFactory,
+               MediaWikiServices $newServices
+       ) {
+               // Use bootstrap config for all configuration.
+               // This allows config overrides via global variables to take 
effect.
+               $bootstrapConfig = $newServices->getBootstrapConfig();
+               $newServices->resetServiceForTesting( 'ConfigFactory' );
+               $newServices->redefineService(
+                       'ConfigFactory',
+                       self::makeTestConfigFactoryInstantiator(
+                               $oldConfigFactory,
+                               [ 'main' =>  $bootstrapConfig ]
+                       )
+               );
+       }
+
+       /**
+        * @param ConfigFactory $oldFactory
+        * @param Config[] $configurations
+        *
+        * @return Closure
+        */
+       private static function makeTestConfigFactoryInstantiator(
+               ConfigFactory $oldFactory,
+               array $configurations
+       ) {
+               return function( MediaWikiServices $services ) use ( 
$oldFactory, $configurations ) {
+                       $factory = new ConfigFactory();
+
+                       // clone configurations from $oldFactory that are not 
overwritten by $configurations
+                       $namesToClone = array_diff(
+                               $oldFactory->getConfigNames(),
+                               array_keys( $configurations )
+                       );
+
+                       foreach ( $namesToClone as $name ) {
+                               $factory->register( $name, 
$oldFactory->makeConfig( $name ) );
+                       }
+
+                       foreach ( $configurations as $name => $config ) {
+                               $factory->register( $name, $config );
+                       }
+
+                       return $factory;
+               };
+       }
+
+       /**
+        * Resets some well known services that typically have state that may 
interfere with unit tests.
+        * This is a lightweight alternative to resetGlobalServices().
+        *
+        * @note There is no guarantee that no references remain to stale 
service instances destroyed
+        * by a call to doLightweightServiceReset().
+        *
+        * @throws MWException if called outside of PHPUnit tests.
+        *
+        * @see resetGlobalServices()
+        */
+       private function doLightweightServiceReset() {
+               global $wgRequest;
+
+               JobQueueGroup::destroySingletons();
+               ObjectCache::clear();
+               FileBackendGroup::destroySingleton();
+
+               // TODO: move global state into MediaWikiServices
+               RequestContext::resetMain();
+               MediaHandler::resetCache();
+               if ( session_id() !== '' ) {
+                       session_write_close();
+                       session_id( '' );
+               }
+
+               $wgRequest = new FauxRequest();
+               MediaWiki\Session\SessionManager::resetCache();
+       }
+
+       public function run( PHPUnit_Framework_TestResult $result = null ) {
+               // Reset all caches between tests.
+               $this->doLightweightServiceReset();
 
                $needsResetDB = false;
 
@@ -289,6 +484,12 @@
                }
                $this->mwGlobals = [];
                $this->restoreLoggers();
+
+               if ( self::$serviceLocator && MediaWikiServices::getInstance() 
!== self::$serviceLocator ) {
+                       MediaWikiServices::forceGlobalInstance( 
self::$serviceLocator );
+               }
+
+               // TODO: move global state into MediaWikiServices
                RequestContext::resetMain();
                MediaHandler::resetCache();
                if ( session_id() !== '' ) {
@@ -325,6 +526,30 @@
        }
 
        /**
+        * Sets a service, maintaining a stashed version of the previous 
service to be
+        * restored in tearDown
+        *
+        * @since 1.27
+        *
+        * @param string $name
+        * @param object $object
+        */
+       protected function setService( $name, $object ) {
+               // If we did not yet override the service locator, so so now.
+               if ( MediaWikiServices::getInstance() === self::$serviceLocator 
) {
+                       $this->overrideMwServices();
+               }
+
+               MediaWikiServices::getInstance()->disableService( $name );
+               MediaWikiServices::getInstance()->redefineService(
+                       $name,
+                       function () use ( $object ) {
+                               return $object;
+                       }
+               );
+       }
+
+       /**
         * Sets a global, maintaining a stashed version of the previous global 
to be
         * restored in tearDown
         *
@@ -354,6 +579,9 @@
         * @param mixed $value Value to set the global to (ignored
         *  if an array is given as first argument).
         *
+        * @note To allow changes to global variables to take effect on global 
service instances,
+        *       call overrideMwServices().
+        *
         * @since 1.21
         */
        protected function setMwGlobals( $pairs, $value = null ) {
@@ -381,6 +609,10 @@
         * @param array|string $globalKeys Key to the global variable, or an 
array of keys.
         *
         * @throws Exception When trying to stash an unset global
+        *
+        * @note To allow changes to global variables to take effect on global 
service instances,
+        *       call overrideMwServices().
+        *
         * @since 1.23
         */
        protected function stashMwGlobals( $globalKeys ) {
@@ -421,6 +653,9 @@
         *
         * @throws MWException If the designated global is not an array.
         *
+        * @note To allow changes to global variables to take effect on global 
service instances,
+        *       call overrideMwServices().
+        *
         * @since 1.21
         */
        protected function mergeMwGlobalArrayValue( $name, $values ) {
@@ -439,6 +674,52 @@
                }
 
                $this->setMwGlobals( $name, $merged );
+       }
+
+       /**
+        * Stashes the global instance of MediaWikiServices, and installs a new 
one,
+        * allowing test cases to override settings and services.
+        * The previous instance of MediaWikiServices will be restored on 
tearDown.
+        *
+        * @since 1.27
+        *
+        * @param Config $configOverrides Configuration overrides for the new 
MediaWikiServices instance.
+        * @param callable[] $services An associative array of services to 
re-define. Keys are service
+        *        names, values are callables.
+        *
+        * @return MediaWikiServices
+        * @throws MWException
+        */
+       protected function overrideMwServices( Config $configOverrides = null, 
array $services = [] ) {
+               if ( !$configOverrides ) {
+                       $configOverrides = new HashConfig();
+               }
+
+               $oldInstance = MediaWikiServices::getInstance();
+               $oldConfigFactory = $oldInstance->getConfigFactory();
+
+               $testConfig = self::makeTestConfig( null, $configOverrides );
+               $newInstance = new MediaWikiServices( $testConfig );
+
+               // Load the default wiring from the specified files.
+               // NOTE: this logic mirrors the logic in 
MediaWikiServices::newInstance.
+               $wiringFiles = $testConfig->get( 'ServiceWiringFiles' );
+               $newInstance->loadWiringFiles( $wiringFiles );
+
+               // Provide a traditional hook point to allow extensions to 
configure services.
+               Hooks::run( 'MediaWikiServices', [ $newInstance ] );
+
+               foreach ( $services as $name => $callback ) {
+                       $newInstance->redefineService( $name, $callback );
+               }
+
+               self::installTestServices(
+                       $oldConfigFactory,
+                       $newInstance
+               );
+               MediaWikiServices::forceGlobalInstance( $newInstance );
+
+               return $newInstance;
        }
 
        /**
@@ -475,6 +756,9 @@
         * @param LoggerInterface $logger
         */
        protected function setLogger( $channel, LoggerInterface $logger ) {
+               // TODO: Once loggers are managed by MediaWikiServices, use
+               //       overrideMwServices() to set loggers.
+
                $provider = LoggerFactory::getProvider();
                $wrappedProvider = TestingAccessWrapper::newFromObject( 
$provider );
                $singletons = $wrappedProvider->singletons;
@@ -565,6 +849,10 @@
 
                $user = User::newFromName( 'UTSysop' );
                $comment = __METHOD__ . ': Sample page for unit test.';
+
+               // Avoid memory leak...?
+               // LinkCache::singleton()->clear();
+               // Maybe.  But doing this absolutely breaks 
$title->isRedirect() when called during unit tests....
 
                $page = WikiPage::factory( $title );
                $page->doEditContent( ContentHandler::makeContent( $text, 
$title ), $comment, 0, false, $user );
@@ -763,6 +1051,9 @@
                        return;
                }
 
+               // TODO: the below should be re-written as soon as LBFactory, 
LoadBalancer,
+               // and DatabaseBase no longer use global state.
+
                self::$dbSetup = true;
 
                if ( !self::setupDatabaseWithTestPrefix( $db, $prefix ) ) {
diff --git a/tests/phpunit/includes/MediaWikiServicesTest.php 
b/tests/phpunit/includes/MediaWikiServicesTest.php
index f5c215b..df0ce8e 100644
--- a/tests/phpunit/includes/MediaWikiServicesTest.php
+++ b/tests/phpunit/includes/MediaWikiServicesTest.php
@@ -1,6 +1,7 @@
 <?php
 use Liuggio\StatsdClient\Factory\StatsdDataFactory;
 use MediaWiki\MediaWikiServices;
+use MediaWiki\Services\ServiceDisabledException;
 
 /**
  * @covers MediaWiki\MediaWikiServices
@@ -9,28 +10,204 @@
  */
 class MediaWikiServicesTest extends PHPUnit_Framework_TestCase {
 
+       /**
+        * @return Config
+        */
+       private function newTestConfig() {
+               $globalConfig = new GlobalVarConfig();
+
+               $testConfig = new HashConfig();
+               $testConfig->set( 'ServiceWiringFiles', $globalConfig->get( 
'ServiceWiringFiles' ) );
+               $testConfig->set( 'ConfigRegistry', $globalConfig->get( 
'ConfigRegistry' ) );
+
+               return $testConfig;
+       }
+
+       /**
+        * @return MediaWikiServices
+        */
+       private function newMediaWikiServices( Config $config = null ) {
+               if ( $config === null ) {
+                       $config = $this->newTestConfig();
+               }
+
+               $instance = new MediaWikiServices( $config );
+
+               // Load the default wiring from the specified files.
+               $wiringFiles = $config->get( 'ServiceWiringFiles' );
+               $instance->loadWiringFiles( $wiringFiles );
+
+               return $instance;
+       }
+
        public function testGetInstance() {
                $services = MediaWikiServices::getInstance();
                $this->assertInstanceOf( 'MediaWiki\\MediaWikiServices', 
$services );
        }
 
+       public function testForceGlobalInstance() {
+               $newServices = $this->newMediaWikiServices();
+               $oldServices = MediaWikiServices::forceGlobalInstance( 
$newServices );
+
+               $this->assertInstanceOf( 'MediaWiki\\MediaWikiServices', 
$oldServices );
+               $this->assertNotSame( $oldServices, $newServices );
+
+               $theServices = MediaWikiServices::getInstance();
+               $this->assertSame( $theServices, $newServices );
+
+               MediaWikiServices::forceGlobalInstance( $oldServices );
+
+               $theServices = MediaWikiServices::getInstance();
+               $this->assertSame( $theServices, $oldServices );
+       }
+
+       public function testResetGlobalInstance() {
+               $newServices = $this->newMediaWikiServices();
+               $oldServices = MediaWikiServices::forceGlobalInstance( 
$newServices );
+
+               MediaWikiServices::resetGlobalInstance( $this->newTestConfig() 
);
+               $theServices = MediaWikiServices::getInstance();
+
+               $this->assertNotSame( $theServices, $newServices );
+               $this->assertNotSame( $theServices, $oldServices );
+
+               MediaWikiServices::forceGlobalInstance( $oldServices );
+       }
+
+       public function testDisableStorageBackend() {
+               $newServices = $this->newMediaWikiServices();
+               $oldServices = MediaWikiServices::forceGlobalInstance( 
$newServices );
+
+               $lbFactory = $this->getMockBuilder( 'LBFactorySimple' )
+                       ->disableOriginalConstructor()
+                       ->getMock();
+
+               $lbFactory->expects( $this->once() )
+                       ->method( 'destroy' );
+
+               $newServices->redefineService(
+                       'DBLoadBalancerFactory',
+                       function() use ( $lbFactory ) {
+                               return $lbFactory;
+                       }
+               );
+
+               // force the service to become active, so we can check that it 
does get destroyed
+               $newServices->getService( 'DBLoadBalancerFactory' );
+
+               MediaWikiServices::disableStorageBackend(); // should destroy 
DBLoadBalancerFactory
+
+               try {
+                       MediaWikiServices::getInstance()->getService( 
'DBLoadBalancerFactory' );
+                       $this->fail( 'DBLoadBalancerFactory shoudl have been 
disabled' );
+               }
+               catch ( ServiceDisabledException $ex ) {
+                       // ok, as expected
+               }
+               catch ( Throwable $ex ) {
+                       $this->fail( 'ServiceDisabledException expected, caught 
' . get_class( $ex ) );
+               }
+
+               MediaWikiServices::forceGlobalInstance( $oldServices );
+       }
+
+       public function testResetChildProcessServices() {
+               $newServices = $this->newMediaWikiServices();
+               $oldServices = MediaWikiServices::forceGlobalInstance( 
$newServices );
+
+               $lbFactory = $this->getMockBuilder( 'LBFactorySimple' )
+                       ->disableOriginalConstructor()
+                       ->getMock();
+
+               $lbFactory->expects( $this->once() )
+                       ->method( 'destroy' );
+
+               $newServices->redefineService(
+                       'DBLoadBalancerFactory',
+                       function() use ( $lbFactory ) {
+                               return $lbFactory;
+                       }
+               );
+
+               // force the service to become active, so we can check that it 
does get destroyed
+               $oldLBFactory = $newServices->getService( 
'DBLoadBalancerFactory' );
+
+               MediaWikiServices::resetChildProcessServices();
+               $finalServices = MediaWikiServices::getInstance();
+
+               $newLBFactory = $finalServices->getService( 
'DBLoadBalancerFactory' );
+
+               $this->assertNotSame( $oldLBFactory, $newLBFactory );
+
+               MediaWikiServices::forceGlobalInstance( $oldServices );
+       }
+
+       public function testResetServiceForTesting() {
+               $services = $this->newMediaWikiServices();
+               $serviceCounter = 0;
+
+               $services->defineService(
+                       'Test',
+                       function() use ( &$serviceCounter ) {
+                               $serviceCounter++;
+                               $service = $this->getMock( 
'MediaWiki\Services\DestructibleService' );
+                               $service->expects( $this->once() )->method( 
'destroy' );
+                               return $service;
+                       }
+               );
+
+               // This should do nothing. In particular, it should not create 
a service instance.
+               $services->resetServiceForTesting( 'Test' );
+               $this->assertEquals( 0, $serviceCounter, 'No service instance 
should be created yet.' );
+
+               $oldInstance = $services->getService( 'Test' );
+               $this->assertEquals( 1, $serviceCounter, 'A service instance 
should exit now.' );
+
+               // The old instance should be detached, and destroy() called.
+               $services->resetServiceForTesting( 'Test' );
+               $newInstance = $services->getService( 'Test' );
+
+               $this->assertNotSame( $oldInstance, $newInstance );
+
+               // Satisfy the expectation that destroy() is called also for 
the second service instance.
+               $newInstance->destroy();
+       }
+
+       public function testResetServiceForTesting_noDestroy() {
+               $services = $this->newMediaWikiServices();
+
+               $services->defineService(
+                       'Test',
+                       function() {
+                               $service = $this->getMock( 
'MediaWiki\Services\DestructibleService' );
+                               $service->expects( $this->never() )->method( 
'destroy' );
+                               return $service;
+                       }
+               );
+
+               $oldInstance = $services->getService( 'Test' );
+
+               // The old instance should be detached, but destroy() not 
called.
+               $services->resetServiceForTesting( 'Test', false );
+               $newInstance = $services->getService( 'Test' );
+
+               $this->assertNotSame( $oldInstance, $newInstance );
+       }
+
        public function provideGetters() {
-               // NOTE: This should list all service getters defined in 
MediaWikiServices.
-               // NOTE: For every test case defined here there should be a 
corresponding
-               // test case defined in provideGetService().
-               return [
-                       'BootstrapConfig' => [ 'getBootstrapConfig', 
Config::class ],
-                       'ConfigFactory' => [ 'getConfigFactory', 
ConfigFactory::class ],
-                       'MainConfig' => [ 'getMainConfig', Config::class ],
-                       'SiteStore' => [ 'getSiteStore', SiteStore::class ],
-                       'SiteLookup' => [ 'getSiteLookup', SiteLookup::class ],
-                       'StatsdDataFactory' => [ 'getStatsdDataFactory', 
StatsdDataFactory::class ],
-                       'EventRelayerGroup' => [ 'getEventRelayerGroup', 
EventRelayerGroup::class ],
-                       'SearchEngine' => [ 'newSearchEngine', 
SearchEngine::class ],
-                       'SearchEngineFactory' => [ 'getSearchEngineFactory', 
SearchEngineFactory::class ],
-                       'SearchEngineConfig' => [ 'getSearchEngineConfig', 
SearchEngineConfig::class ],
-                       'SkinFactory' => [ 'getSkinFactory', SkinFactory::class 
],
-               ];
+               $getServiceCases = $this->provideGetService();
+               $getterCases = [];
+
+               // All getters should be named just like the service, with 
"get" added.
+               foreach ( $getServiceCases as $name => $case ) {
+                       list( $service, $class ) = $case;
+                       $getterCases[$name] = [
+                               'get' . $service,
+                               $class,
+                       ];
+               }
+
+               return $getterCases;
        }
 
        /**
@@ -58,6 +235,8 @@
                        'SearchEngineFactory' => [ 'SearchEngineFactory', 
SearchEngineFactory::class ],
                        'SearchEngineConfig' => [ 'SearchEngineConfig', 
SearchEngineConfig::class ],
                        'SkinFactory' => [ 'SkinFactory', SkinFactory::class ],
+                       'DBLoadBalancerFactory' => [ 'DBLoadBalancerFactory', 
'LBFactory' ],
+                       'DBLoadBalancer' => [ 'DBLoadBalancer', 'LoadBalancer' 
],
                ];
        }
 
diff --git a/tests/phpunit/includes/Services/ServiceContainerTest.php 
b/tests/phpunit/includes/Services/ServiceContainerTest.php
index 942c45e..933777c 100644
--- a/tests/phpunit/includes/Services/ServiceContainerTest.php
+++ b/tests/phpunit/includes/Services/ServiceContainerTest.php
@@ -69,9 +69,51 @@
 
                $name = 'TestService92834576';
 
-               $this->setExpectedException( 'InvalidArgumentException' );
+               $this->setExpectedException( 
'MediaWiki\Services\NoSuchServiceException' );
 
                $services->getService( $name );
+       }
+
+       public function testPeekService() {
+               $services = $this->newServiceContainer();
+
+               $services->defineService(
+                       'Foo',
+                       function() {
+                               return new stdClass();
+                       }
+               );
+
+               $services->defineService(
+                       'Bar',
+                       function() {
+                               return new stdClass();
+                       }
+               );
+
+               // trigger instantiation of Foo
+               $services->getService( 'Foo' );
+
+               $this->assertInternalType(
+                       'object',
+                       $services->peekService( 'Foo' ),
+                       'Peek should return the service object if it had been 
accessed before.'
+               );
+
+               $this->assertNull(
+                       $services->peekService( 'Bar' ),
+                       'Peek should return null if the service was never 
accessed.'
+               );
+       }
+
+       public function testPeekService_fail_unknown() {
+               $services = $this->newServiceContainer();
+
+               $name = 'TestService92834576';
+
+               $this->setExpectedException( 
'MediaWiki\Services\NoSuchServiceException' );
+
+               $services->peekService( $name );
        }
 
        public function testDefineService() {
@@ -99,7 +141,7 @@
                        return $theService;
                } );
 
-               $this->setExpectedException( 'RuntimeException' );
+               $this->setExpectedException( 
'MediaWiki\Services\ServiceAlreadyDefinedException' );
 
                $services->defineService( $name, function() use ( $theService ) 
{
                        return $theService;
@@ -147,7 +189,7 @@
                ];
 
                // loading the same file twice should fail, because
-               $this->setExpectedException( 'RuntimeException' );
+               $this->setExpectedException( 
'MediaWiki\Services\ServiceAlreadyDefinedException' );
 
                $services->loadWiringFiles( $wiringFiles );
        }
@@ -184,7 +226,7 @@
                $theService = new stdClass();
                $name = 'TestService92834576';
 
-               $this->setExpectedException( 'RuntimeException' );
+               $this->setExpectedException( 
'MediaWiki\Services\NoSuchServiceException' );
 
                $services->redefineService( $name, function() use ( $theService 
) {
                        return $theService;
@@ -204,11 +246,101 @@
                // create the service, so it can no longer be redefined
                $services->getService( $name );
 
-               $this->setExpectedException( 'RuntimeException' );
+               $this->setExpectedException( 
'MediaWiki\Services\CannotReplaceActiveServiceException' );
 
                $services->redefineService( $name, function() use ( $theService 
) {
                        return $theService;
                } );
        }
 
+       public function testDisableService() {
+               $services = $this->newServiceContainer( [ 'Foo' ] );
+
+               $destructible = $this->getMock( 
'MediaWiki\Services\DestructibleService' );
+               $destructible->expects( $this->once() )
+                       ->method( 'destroy' );
+
+               $services->defineService( 'Foo', function() use ( $destructible 
) {
+                       return $destructible;
+               } );
+               $services->defineService( 'Bar', function() {
+                       return new stdClass();
+               } );
+               $services->defineService( 'Qux', function() {
+                       return new stdClass();
+               } );
+
+               // instantiate Foo and Bar services
+               $services->getService( 'Foo' );
+               $services->getService( 'Bar' );
+
+               // disable service, should call destroy() once.
+               $services->disableService( 'Foo' );
+
+               // disabled service should still be listed
+               $this->assertContains( 'Foo', $services->getServiceNames() );
+
+               // getting other services should still work
+               $services->getService( 'Bar' );
+
+               // disable non-destructible service, and not-yet-instantiated 
service
+               $services->disableService( 'Bar' );
+               $services->disableService( 'Qux' );
+
+               $this->assertNull( $services->peekService( 'Bar' ) );
+               $this->assertNull( $services->peekService( 'Qux' ) );
+
+               // disabled service should still be listed
+               $this->assertContains( 'Bar', $services->getServiceNames() );
+               $this->assertContains( 'Qux', $services->getServiceNames() );
+
+               // re-enable Bar service
+               $services->redefineService( 'Bar', function() {
+                       return new stdClass();
+               } );
+
+               $services->getService( 'Bar' );
+
+               $this->setExpectedException( 
'MediaWiki\Services\ServiceDisabledException' );
+               $services->getService( 'Qux' );
+       }
+
+       public function testDisableService_fail_undefined() {
+               $services = $this->newServiceContainer();
+
+               $theService = new stdClass();
+               $name = 'TestService92834576';
+
+               $this->setExpectedException( 
'MediaWiki\Services\NoSuchServiceException' );
+
+               $services->redefineService( $name, function() use ( $theService 
) {
+                       return $theService;
+               } );
+       }
+
+       public function testDestroy() {
+               $services = $this->newServiceContainer();
+
+               $destructible = $this->getMock( 
'MediaWiki\Services\DestructibleService' );
+               $destructible->expects( $this->once() )
+                       ->method( 'destroy' );
+
+               $services->defineService( 'Foo', function() use ( $destructible 
) {
+                       return $destructible;
+               } );
+
+               $services->defineService( 'Bar', function() {
+                       return new stdClass();
+               } );
+
+               // create the service
+               $services->getService( 'Foo' );
+
+               // destroy the container
+               $services->destroy();
+
+               $this->setExpectedException( 
'MediaWiki\Services\ContainerDisabledException' );
+               $services->getService( 'Bar' );
+       }
+
 }
diff --git a/tests/phpunit/includes/config/ConfigFactoryTest.php 
b/tests/phpunit/includes/config/ConfigFactoryTest.php
index 2288507..2c1d1e6 100644
--- a/tests/phpunit/includes/config/ConfigFactoryTest.php
+++ b/tests/phpunit/includes/config/ConfigFactoryTest.php
@@ -8,9 +8,51 @@
        public function testRegister() {
                $factory = new ConfigFactory();
                $factory->register( 'unittest', 'GlobalVarConfig::newInstance' 
);
-               $this->assertTrue( true ); // No exception thrown
+               $this->assertInstanceOf( GlobalVarConfig::class, 
$factory->makeConfig( 'unittest' ) );
+       }
+
+       /**
+        * @covers ConfigFactory::register
+        */
+       public function testRegisterInvalid() {
+               $factory = new ConfigFactory();
                $this->setExpectedException( 'InvalidArgumentException' );
                $factory->register( 'invalid', 'Invalid callback' );
+       }
+
+       /**
+        * @covers ConfigFactory::register
+        */
+       public function testRegisterInstance() {
+               $config = GlobalVarConfig::newInstance();
+               $factory = new ConfigFactory();
+               $factory->register( 'unittest', $config );
+               $this->assertSame( $config, $factory->makeConfig( 'unittest' ) 
);
+       }
+
+       /**
+        * @covers ConfigFactory::register
+        */
+       public function testRegisterAgain() {
+               $factory = new ConfigFactory();
+               $factory->register( 'unittest', 'GlobalVarConfig::newInstance' 
);
+               $config1 = $factory->makeConfig( 'unittest' );
+
+               $factory->register( 'unittest', 'GlobalVarConfig::newInstance' 
);
+               $config2 = $factory->makeConfig( 'unittest' );
+
+               $this->assertNotSame( $config1, $config2 );
+       }
+
+       /**
+        * @covers ConfigFactory::register
+        */
+       public function testGetConfigNames() {
+               $factory = new ConfigFactory();
+               $factory->register( 'foo', 'GlobalVarConfig::newInstance' );
+               $factory->register( 'bar', new HashConfig() );
+
+               $this->assertEquals( [ 'foo', 'bar' ], 
$factory->getConfigNames() );
        }
 
        /**
@@ -19,6 +61,18 @@
        public function testMakeConfig() {
                $factory = new ConfigFactory();
                $factory->register( 'unittest', 'GlobalVarConfig::newInstance' 
);
+
+               $conf = $factory->makeConfig( 'unittest' );
+               $this->assertInstanceOf( 'Config', $conf );
+               $this->assertSame( $conf, $factory->makeConfig( 'unittest' ) );
+       }
+
+       /**
+        * @covers ConfigFactory::makeConfig
+        */
+       public function testMakeConfigFallback() {
+               $factory = new ConfigFactory();
+               $factory->register( '*', 'GlobalVarConfig::newInstance' );
                $conf = $factory->makeConfig( 'unittest' );
                $this->assertInstanceOf( 'Config', $conf );
        }
@@ -48,10 +102,10 @@
         * @covers ConfigFactory::getDefaultInstance
         */
        public function testGetDefaultInstance() {
+               // NOTE: the global config factory returned here has been 
overwritten
+               // for operation in test mode. It may not reflect LocalSettings.
                $factory = ConfigFactory::getDefaultInstance();
                $this->assertInstanceOf( 'Config', $factory->makeConfig( 'main' 
) );
-
-               $this->setExpectedException( 'ConfigException' );
-               $factory->makeConfig( 'xyzzy' );
        }
+
 }
diff --git a/tests/phpunit/phpunit.php b/tests/phpunit/phpunit.php
index 77690cd..d876c45 100755
--- a/tests/phpunit/phpunit.php
+++ b/tests/phpunit/phpunit.php
@@ -139,6 +139,10 @@
                // may break testing against floating point values
                // treated with PHP's serialize()
                ini_set( 'serialize_precision', 17 );
+
+               // TODO: we should call MediaWikiTestCase::prepareServices( new 
GlobalVarConfig() ) here.
+               // But PHPUnit may not be loaded yet, so we have to wait until 
just
+               // before PHPUnit_TextUI_Command::main() is executed at the end 
of this file.
        }
 
        public function execute() {
@@ -237,4 +241,9 @@
        'Using HHVM ' . HHVM_VERSION . ' (' . PHP_VERSION . ")\n" :
        'Using PHP ' . PHP_VERSION . "\n";
 
+// Prepare global services for unit tests.
+// FIXME: this should be done in the finalSetup() method,
+// but PHPUnit may not have been loaded at that point.
+MediaWikiTestCase::prepareServices( new GlobalVarConfig() );
+
 $wgPhpUnitClass::main();

-- 
To view, visit https://gerrit.wikimedia.org/r/286314
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: merged
Gerrit-Change-Id: Ie06782ffb96e675c0aa55dc26fb8f22037e8517d
Gerrit-PatchSet: 5
Gerrit-Project: mediawiki/core
Gerrit-Branch: master
Gerrit-Owner: Daniel Kinzler <[email protected]>
Gerrit-Reviewer: Aaron Schulz <[email protected]>
Gerrit-Reviewer: Addshore <[email protected]>
Gerrit-Reviewer: Anomie <[email protected]>
Gerrit-Reviewer: Catrope <[email protected]>
Gerrit-Reviewer: Daniel Kinzler <[email protected]>
Gerrit-Reviewer: Jackmcbarn <[email protected]>
Gerrit-Reviewer: Legoktm <[email protected]>
Gerrit-Reviewer: Parent5446 <[email protected]>
Gerrit-Reviewer: Thcipriani <[email protected]>
Gerrit-Reviewer: Zppix <[email protected]>
Gerrit-Reviewer: jenkins-bot <>

_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to