Problem
- Drupal 8 cannot be re-installed if there is a settings.php that contains (working) $databases info already.
Cause
install_begin_request()
only uses a minimal/mock DI container until (1) configuration directories (2) database connection (3) settings.php have been set up.- If 1), 2), and 3) exist and are functional already, then the installer immediately switches to the regular DrupalKernel.
- That works in the regular installation procedure, but when aforementioned preconditions are met, then it does not, because the base system database tables have not been installed yet.
Steps to reproduce
- Have an existing D8 installation.
- Delete PHP storage files, configuration files, and drop all database tables.
- Visit install.php
Actual result
Additional uncaught exception thrown while handling exception.
OriginalDrupal\Core\Database\DatabaseExceptionWrapper: SQLSTATE[42S02]: Base table or view not found: 1146 Table 'test_drupal8.semaphore' doesn't exist: INSERT INTO {semaphore} (name, value, expire) VALUES (:db_insert_placeholder_0, :db_insert_placeholder_1, :db_insert_placeholder_2); Array ( [:db_insert_placeholder_0] => theme_registry:runtime:seven:Drupal\Core\Utility\ThemeRegistry [:db_insert_placeholder_1] => 204232833852aa2d6d389376.96185241 [:db_insert_placeholder_2] => 1386884491.2037 ) in Drupal\Core\Database\Connection->query() (line 568 of \core\lib\Drupal\Core\Database\Connection.php).
AdditionalDrupal\Core\Database\DatabaseExceptionWrapper: SQLSTATE[42S02]: Base table or view not found: 1146 Table 'test_drupal8.semaphore' doesn't exist: INSERT INTO {semaphore} (name, value, expire) VALUES (:db_insert_placeholder_0, :db_insert_placeholder_1, :db_insert_placeholder_2); Array ( [:db_insert_placeholder_0] => theme_registry:runtime:seven:Drupal\Core\Utility\ThemeRegistry [:db_insert_placeholder_1] => 204232833852aa2d6d389376.96185241 [:db_insert_placeholder_2] => 1386884491.2507 ) in Drupal\Core\Database\Connection->query() (line 568 of \core\lib\Drupal\Core\Database\Connection.php).
Fatal error: Call to a member function id() on a non-object in H:\unleashedmind\test\drupal8\core\includes\session.inc on line 179
Expected result
Regular installer, sans database setup step.
Comment | File | Size | Author |
---|---|---|---|
#31 | interdiff.txt | 1.1 KB | sun |
#31 | drupal8.reinstall.31.patch | 10.69 KB | sun |
#26 | drupal8.reinstall.26.patch | 10.35 KB | sun |
Comments
Comment #2
sunHm. Digging further into this ultimately brings me back to my original patch in #1798732-137: Convert install_task, install_time and install_current_batch to use the state system
Installation fails, because the 'keyvalue.memory' service is unknown.
If you refresh the error page (new HTTP request), then you get the error message "Table 'variable' already exists."
The DI container (or several services on it) has to be swapped out mid-request, so as to make services us the freshly created database tables - or alternatively, the installer has to redirect after installing base system database tables, so that the new HTTP page request boots the regular runtime service container (using the database).
Comment #3
larowlanComment #4
sunThe previous suspicion was only partially correct.
drupal_install_system()
swaps out the minimal service container with a regular DrupalKernel. Albeit not as cleanly separated as in my original patch, that existing code appears to work as intended.The problem is caused much earlier in the request:
install_begin_request()
, one of the very first functions being invoked to set up the base environment before the actual main installer logic is executed, checks whether settings.php exists, config directories exist, and a database connection can be established. If all conditions are met, then the regular DrupalKernel is initialized.However, those three conditions are not sufficient; the services in the regular container additionally need a persistent storage; i.e., the database tables of the base system schema. Due to that, the installer breaks with "table not found" errors.
The proposed fix is to add a new verification "base_system_verified" to the early environment setup, which essentially tests whether the last defined table in the System module schema exists in the database. Only if that is the case, a regular DrupalKernel is used.
The patch also replaces a Settings overloading/reversion hack for the minimal service container, which exists to make the installer use memory implementations for all key/value stores (instead of database implementations). It is sufficient to register the key/value memory factory services in the minimal container. The only reason for why that was not possible before is that various other services are type-hinting the KeyValueFactory class as opposed to a KeyValueFactoryInterface.
Lastly, this patch also fixes the error message "table 'variable' already exists", which occasionally appeared under certain edge-case conditions (e.g., using the Back button of your browser, or by reloading an early installer page), and which was caused by the same condition that also caused the regular DrupalKernel to be used; i.e., the installer does not know whether and when the base system has been installed.
Comment #7
sunSorry, the KeyValueFactory changes turned out to be not really relevant here, so I've split that out into #2156265: KeyValueFactory is swappable, add an interface and fix type-hints
Comment #9
sunAll of the test failures appear to be DUTB tests. The DUTB base class implements a similar key/value memory storage hack like the installer (but differently).
By splitting that fix into #2156265: KeyValueFactory is swappable, add an interface and fix type-hints, it appears we introduced a hard dependency on that interface to get committed, in order to move forward here... :-/
Comment #10
sunI was confused in #9 — the failing tests are web tests, not DUTB tests.
Meanwhile I was able to confirm that merging the KeyValueFactoryInterface patch back into this patch here fixes the fatal errors in web tests.
Hence, waiting for that patch to get committed.
Until then, reviews and feedback is welcome :)
Comment #11
jibranWell the intall.inc file scares me so I can't tell that changes are legit or not but I was able to find an issue. Sorry for not being much helpful here. :)
80 char limit.
Comment #12
larowlanThats a lot of documentation we're losing. Any reason why?
Comment #13
pwolanin CreditAttribution: pwolanin commentedI thought I filed a duplicate issue, but having trouble finding it. I might consider this critical.
Comment #16
sunIt's green :)
I'm not sure whether any further explanations for the proposed changes are missing (besides those I gave in comments above already). Please let me know if anything is unclear.
re: @larowlan: Blatant removal of lengthy comments
I added those lines myself when the configuration system and the Config\InstallStorage was introduced originally.
Core has changed and advanced a lot since then (primarily: Settings), and so the details covered in that comment are mostly obsolete now.
The main point the comment tries to explain is that the installer essentially boils down to two discrete phases:
The very first installer screens are operating in (1), which means that they cannot use a regular DrupalKernel, because plenty of services would need to be overridden in order to work in an "empty" environment (no settings, no config, no database, no storage). → Every HTTP page request rebuilds all necessary bootstrap information from scratch.
But as soon as (2) is reached and the base system database tables exist (persistent storage for base system services à la cache, k/v, lock, config, etc), we are, in fact, operating in a regular Drupal environment.
As a testament to that, some time ago, I was able to install Drupal 8 with System module only. Obviously resulted in pretty much blank pages only, but that's to be expected, isn't it? :)
However, if you complete the train of thought, the much bigger effect of (2) is that the entire remaining operations of the installer are happening in a regular Drupal environment. As a consequence, you can simply install/enable modules as in a regular environment, because you're operating in a normal Drupal system → no more special-casing for the installer.
We could even turn the remainder of the installer into a module of its own (which could then be swapped out by distros/install-profiles), but that's a debate of its own and not to be discussed here.
That said, the majority of the above explanation is irrelevant for this issue and patch here. The only parts that matter are:
$install_state
flag to signify that the base system is operable.Doing so fixes the reported bug:
Even if
settings.php
,$databases
, and$config_directories
exist, that does not mean that the base system is operable (and persistent storage is available).install_begin_request()
is enhanced with a new environment condition that checks whether the base system tables have been installed already.Since this check is required upfront to make the decision of whether to boot a regular DrupalKernel or build a minimal installer service container, it has to happen very early in the request. For that reason, we try to query the database, and we simply wrap that check into a try/catch block.
Comment #17
sunI noticed that we have a
$install_state['database_tables_exist']
key already that had similar in purpose, but is unused.As a final clean-up step, attached patch removes the unused and obsolete 'database_tables_exist' flag, because the unique condition we're looking for really is that the base system is operable. We do not want to know whether individual pieces like database tables and whatnot have been set up, but instead, we need to know whether the base system is fully operational or not.
Comment #20
sunAny further issues with this patch? Or can we move forward here? :)
Comment #21
andypostOverall the aproach is awesome!
Why not check all tables?
Comment #25
sunThe existing patch is back to green - testbot fluke.
Checking all tables would needlessly slow down the installer — if the last table defined in system_schema() has been created, then we can be reasonably sure that all other (previous) tables have been installed as well.
That is, because hook_schema() definitions are processed in the order in which they are defined. In other words, if any of the tables could not be created, then the last table was not created either.
This argumentation is not meant to say that the overall
base_system_verified
check could not be improved and made more solid in the future — I could actually see plenty of room for doing so, but for now, I'd like to focus on (1) fixing this concrete bug and (2) introducing/hardening the underlying concept of two discrete phases in the installer: pre-base-system and post-base-system.Any other questions? :)
Comment #26
sunRe-rolled against latest HEAD, and:
Removed this change to drupal_install_system(), as I'm no longer sure whether it still applies (the base_system_verified condition should kick in already), and also, because it's technically outside of the scope of this issue here.
Comment #28
sun26: drupal8.reinstall.26.patch queued for re-testing.
Comment #29
andypostGreat clean-up, I found no nitpicks
Comment #30
webchickAwesome, happy to see this fixed, since this has bitten me a number of times.
A couple of small things.
I think I'd prefer a specific check for a given table than just arbitrarily taking the last one from the list, because that one could change from time to time, and might conceivably conflict with an existing table name if Drupal's being installed to another database w/ prefixes or whatever. (I'm worried specifically about seemingly random testbot fails when testbot1 is testing against one version of a schema and testbot2 is testing against another changed by the patch it's working on, because they're testing two different table names for this check.)
Just catch? No exception raised? Could we at least have a comment that explains what just happened in this event?
Comment #31
sunIt took me a few minutes to digest and understand this comment ;) I hope I understood it, and I don't think any of the concerns apply, and here's why:
Tests are operating in cleanly separated environments.
They are executed in parallel on the same machine/testbot (the example of testbot1 vs. testbot2 does not apply), but our existing database table prefixing is robust enough to not result in table name clashes. If that would be an issue, then we would see random test failures today already (which we don't).
To my knowledge, we do not support changes to
settings.php
after starting the installer.That would be the only way to achieve the conflict in expectations being made; i.e., changing the
'prefix'
insettings.php
after hitting second or third installer page, aftersettings.php
has been (re)written.By now, I have a relatively excellent understanding of Drupal's installer, and I'm fairly confident that none of the other code in the installer accounts for that case either.
I would not recommend to support that edge-case in the first place.
Just as of last week or so, someone filed a bug report against Drupal core, complaining that the installer blew up, after manually changing essential values (IIRC, $databases), and hitting the reload/back button in a browser was initiated a day before. Do we want to support that?
I think the clear answer is No. → settings.php declares how Drupal will be installed. It either exists already or will be created automatically. But changing the declared settings in between installer steps is not something we can or want to support at any point in time. The entire installer would have to be rewritten to account for that, because e.g. the database (or config directories) could be manipulated (literally) at any point in time.
Based on that reasoning (which hopefully makes sense), checking the last defined table in
system_schema()
is absolutely sufficient.We're already accepting the extra LoC of checking the last defined table instead of the first, so as to ensure that all defined base system tables have been created (as they're created sequentially in the order they're defined). Sans the unsupported edge-cases outlined above, that is a simple, but yet, 100% complete "test" coverage that is possible within the early installer environment.
Checking a specific table of the defined ones would have no benefit to the taken approach. If at all, it would only introduce bugs later on, in case there is a new table definition appended to
system_schema()
and if we'd forget update the installer accordingly. :)Added docs to the case of catching the exception.
Comment #32
andypostComment is fine. I can't imagine the case when latest table in the schema list could collide - this means something goes totally wrong.
Comment #33
Dries CreditAttribution: Dries commentedCommitted to 8.x. Thanks sun.