Putting this in as a bug report because the ultimate effect is a corrupted site, although in some ways it's also a feature request (scalability).

If you enable UUID module on a site which already has a large number of entities on it, the module install can fail part way through due to timeouts or OOM, leaving you with a partially updated site that is difficult to recover. I'll try and provide some more concrete data in due course, but for now let's say we're talking a site of moderate size: 150,000 nodes and 150,000 users or so.

The problem ultimately is that on enable, this module attempts to:

  1. Add the DB column and index for every entity it can find and ...
  2. Grab all the rows from that DB table (technically, all the rows without a UUID, but practically speaking that means all of them) and ...
  3. Update all of them adding a UUID

This is all within the same page request that enabled the module. This really is never going to scale well.

I'd like to consider batching or otherwise queueing this initial setup step. The obvious problem with this approach being that if any part of the system depends on UUIDs being set on all entities, the site is not really "ready" until all entities have finished being updated, so there needs to be some flag to say "initial population of UUIDs completed", such that any module wanting to use UUIDs can remain inactive until the site is ready.

Comments

alan evans’s picture

So, a few numbers ... here's a "smallish" test site with 10000 nodes and 10000 users and <= 5 comments per node (devel_generate content and users). Note that nothing else is using the DB and it's installed on SSD - the DB is quite fast.

I've installed using drush this time because I wanted to make sure I didn't hit any sort of page timeout. The events should be somewhat self-explanatory if read alongside the code. Times in MS, memory in bytes.

0.01 INSTALL BEGIN. MEM:81308616
25.59 GOT SCHEMA DEF. MEM:  81310192
36.45 BEGIN ENTITY user. MEM:  81318712
1028.07 ALTERED BASE TABLE FOR user. MEM:  81319144
2485.81 INDEXED BASE TABLE user. MEM:  81319136
2485.95 BEGIN ENTITY node. MEM:  81319136
4736.26 ALTERED BASE TABLE FOR node. MEM:  81319128
7133.79 INDEXED BASE TABLE node. MEM:  81319120
7958.6 ALTERED REVISION TABLE FOR node. MEM:  81319128
8924.46 INDEXED REVISION TABLE node. MEM:  81319128
8924.61 BEGIN ENTITY comment. MEM:  81319096
10663.54 ALTERED BASE TABLE FOR comment. MEM:  81319104
12680.66 INDEXED BASE TABLE comment. MEM:  81319104
12680.78 BEGIN ENTITY file. MEM:  81319096
14022.39 ALTERED BASE TABLE FOR file. MEM:  81319104
15472.45 INDEXED BASE TABLE file. MEM:  81319104
15472.6 BEGIN ENTITY taxonomy_term. MEM:  81319112
15498.88 ALTERED BASE TABLE FOR taxonomy_term. MEM:  81319112
15572.38 INDEXED BASE TABLE taxonomy_term. MEM:  81319112
15572.5 BEGIN SYNC. MEM:  81312056
15594.8 BEGIN SYNC comment. MEM:  81631648
26434.39 SYNCED BASE TABLE comment. MEM:  81632184
26434.53 BEGIN SYNC node. MEM:  81632176
34979.19 SYNCED BASE TABLE node. MEM:  81632176
41969.77 SYNCED REVISION TABLE node. MEM:  81632184
41969.88 BEGIN SYNC redirect. MEM:  81632184
41969.94 BEGIN SYNC file. MEM:  81632176
49096.32 SYNCED BASE TABLE file. MEM:  81632176
49096.45 BEGIN SYNC taxonomy_term. MEM:  81632192
49097.67 SYNCED BASE TABLE taxonomy_term. MEM:  81632104
49097.75 BEGIN SYNC taxonomy_vocabulary. MEM:  81632104
49097.8 BEGIN SYNC user. MEM:  81632072
56208.58 SYNCED BASE TABLE user. MEM:  81632176
56208.7 BEGIN SYNC wysiwyg_profile. MEM:  81632192
56208.75 BEGIN SYNC menu_link. MEM:  81632184
56208.81 DONE. MEM:  81628528

When enabling from the admin/modules page, on many typical configurations, the module installation dies, leaving about half the tables done. You can get around this of course by upping various timeout thresholds but you wouldn't typically want to do that just for the sake of installing modules - there are good reasons for keeping timeouts limited.

What's also somewhat interesting is that memory here is hardly a problem at all - it grows by about 0.5MB during install.

skwashd’s picture

Priority: Normal » Major

Thanks for the bug report. Upping to major as it makes a mess of a site.

I will spend some time thinking about how best we can address this problem.

aaronbauman’s picture

Maybe you can provide a standalone php script to initialize the new UUID fields?
Or don't try to do so much during install, but provide an "initialize" interface that will finalize the install using batch API?

I'm getting a hard fail and totally borked database on UUID install as well

joseph.olstad’s picture

StatusFileSize
new648 bytes

I've come up with a simple script for use with drush that uses the batch processing to get around timeout limits that are observed when installing this module on extremely large websites.

This example could lead to an improved uuid.install for future versions. However for now, this script is to be used as follows:

drush --root=/path/to/drupal/site php-script uuid_install_script_for_extremely_large_sites-1728310-4.php

see code:


if (!db_field_exists('users', 'uuid')) {
$operations_install = array(
    array('module_enable', array(array('uuid'), TRUE)),
   );

  $t_args = array('%variable' => 'module_enable(\'uuid\',TRUE)');
  $title_install = t('Creating uuid schema to be able to process for extremely large sites by calling %variable in batch', $t_args);

  $batch = array(
    'title' => $title_install,
    'operations' => $operations_install,
    'file' => drupal_get_path('inc', 'module'),
    /* 'finished' => 'module_exists(\'uuid\')',*/
  );
  batch_set($batch);
  $batch =& batch_get();
  $batch['progressive'] = FALSE;
  drush_backend_batch_process();
  drush_print(print_r($batch,1));
  watchdog('batch', '$batch=' . print_r($batch,1) );
  watchdog('batch','batch finished');
} else {
  drush_print("uuid already installed");
}

the reasoning is, uuid_sync_all is the function that times out on extremely large sites, but simply calling module_enable in a batch operation should (according to my theory) do all the work necessary in a batch function so it (hopefully) won't time out. So, this operation if on extremely large sites might take several hours or even days depending on the server performance and site configuration and size of site , the operation should in theory process the whole operation. I haven't actually tested this with extremely large sites but such a test could be done using the devel content generator to prepare a site.

I developed the above script on drupal core 7.34 and tested it against only 4 nodes so it does need to be tested with extremely large sites.

Any feedback is welcome. Perhaps this isn't the solution however I have used the batch processing in the past but not exactly in this way although similarly.

perhaps drush enable module already does this sort of batch wrapper it'self, I'd be interested to hear feedback on this as it's probably not a unique situation, there could be other situations where extremely large schemas need some sort of batch processing (maybe not in this manner but in a similar manner).

Thanks.

joseph.olstad’s picture

Status: Active » Needs review
joseph.olstad’s picture

Status: Needs review » Needs work

the script from comment #4 probably won't resolve memory limit issues the enable module is clumped one batch, what probably should happen is to break down the uuid_sync_all function (called during uuid_install() ) into several batch operations so that memory limits aren't exceeded when processing insanely huge amounts of entities. what could be done is to batch queue up a thousand entities at a time or so .

What the script from comment#4 does do is prevent timeout errors, generally batch jobs won't time out, of course they face the same memory limits that any php operation does but have the ability to break down operations into smaller chunks.

with that said, feel free to test the script with insane datasets and find out what the limits might be with a given configuration and total number of entities.

mustanggb’s picture

Just updated menu_token which now has a dependency on uuid, so had to retroactively install this module, hitting 3 million individual node_revision entries isn't fun.

Perhaps this could be optimised to a single query per database table?

joseph.olstad’s picture

Hi @MustangGB, so you used the script from comment #4? As for optimisation, try xhprof, it will give you a graphical representation of your bottlenecks with latency between calls and the bottleneck in red. xhprof was designed by facebook to optimise their php code, we have used it in Drupal to optimise core and several contrib modules. I've not done this yet for uuid_install() but if you are interested you could run xhprof and it will point out a lot of problem areas. As for 3 million node_revisions, good thing the install only needs to happen once!

antongp’s picture

I'd remove trying to generate missing uuids from installation tasks (hook_install(), hook_modules_installed() implementations) and maybe show some warning/error message about needs to create missing uuids, something like Node module does when content access permissions need to be rebuilt.

Creation of missing uuids may be performed via administration UI then (UUID module allows to do this via admin/config/system/uuid), but it should be done via batch. So there is problem with the _uuid_sync_table() function which tries to fetch all records which do not have assigned uuid and then loop through them and update each record - it should process records in batches. This function isn't called directly, it's called in the hook_uuid_sync() implementation (which is invoked by by the uuid_sync_all()) - so there is problem with hook_uuid_sync(), it should allow batching somehow...

The only implementation of the hook_uuid_sync() I saw is uuid_uuid_sync() (the UUID module own implementation), I searched through number of contrib modules which have integration with the UUID but haven't found any other implementations of this hook, so not sure what exactly may be performed here (it would be nice to see such examples)...

megachriz’s picture

Status: Needs work » Needs review
StatusFileSize
new5.67 KB

This patch adds a queue called 'uuid_sync' for generating uuids in tables. Generating uuids for a table is moved from _uuid_sync_table() to UuidSyncQueue::process() and a limit of processing 50 items per cycle is added. This means that when an item from the queue 'uuid_sync' is processed, uuids will be generated for a maximum of 50 records. If indeed 50 records were processed, there may be more records to generate uuids for, so the item will be put back in the queue in that case. During a single cron run, multiple items may be processed from the queue. During my testing, for about 13.500 records an uuid was generated on a single cron run.

Upon install, the module will still add uuid columns to tables (which can also take a while), but the task of generating uuids will only happen on cron runs.

This solution isn't perfect as you'll end up with a situation where some entities will have uuids and others won't yet (until enough cron runs have completed). I don't know the drawbacks of that situation. But this is at least a start.

mrded’s picture

Why don't you on install just initialise necessary tables, and when an entity is requested, only then generate UUID?
It will work basically the same as cache. First request will check UUID existence, if it doesn't exist - create it, otherwise just return.

mstrelan’s picture

#10 works well for me as a workaround. I think if it were to be committed it should have a hook_requirements() warning and maybe a UI to batch process the queue. I'm currently running drush queue-run uuid_sync on ~150 000 nodes.

quiquee’s picture

Anyone could get this done for me ?

I ended up with a corrupted db because lack of warning. I had to recover it manually but I have probably now a little mess to sort out. I am having 0.25million nodes and close to 2million comments and 0,4million private messages.

Any professional help appreciated
Thank you