The way MySQL manages replication is by running the same query on a slave as it did on the primary master.

When the module assigns a new UUID on the master SQL server, the query dutifully replicates to any slave servers and is executed there as well.

At this point the data on the master and slave is no longer identical, as the call to UUID() on the slave is guaranteed to not produce the same identifier on the slave as it did on the master.

This is likely to cause problems for anyone using MySQL replication, even if replication is only used so that a dump slave is available for backups.

The only way to fix this is by using an actual UUID string in the INSERT query, not a call to a non-deterministic function. This means the initial bulk assignment of UUID for users and nodes may take far longer to run, but it will then no longer break replicating setups.

Comments

cafuego’s picture

Oh, I did a bit of research and it turns out that with MySQL 5.1 and later (I run 5.0) I can work around this by setting

binlog_format=mixed

in my.cnf.

Still, it would be nice to not have to upgrade all my production servers.

cafuego’s picture

Status: Active » Needs review
StatusFileSize
new4.58 KB

Patch attached. This patch defines a function uuid_uuid() which generates a new UUID without needing a database connection. The function is from http://php.net/manual/en/function.uniqid.php#65879

I'm also told that binlog_format=mixed is somewhat broken in MySQL 5.1, so not having to rely on that would be good.

Also, I suppose this would make the module work with PgSQL :-)

recidive’s picture

Hi, patch looks interesting.

Dumb question: is that version 4 random UUID collision proof?

cafuego’s picture

StatusFileSize
new5.25 KB

This new patch makes use of the PECL extension to generate UUIDs if the extension is available. If not, it uses the function off php.net.

anarchivist’s picture

cafuego's patch fails with the PECL extension installed as there's already a uuid_is_valid() function provided by the extension. I'll reroll a patch with a modified version of the function.

anarchivist’s picture

StatusFileSize
new7.19 KB

Rerolled patch containing cafuego's code and my modifications to the validation function.

recidive’s picture

Status: Needs review » Postponed (maintainer needs more info)

Hi, you didn't reply to my dumb question: is that version 4 random UUID collision proof?

anarchivist’s picture

@recidive - sorry, meant to reply to your comment before, but didn't. I'm actually inclined to believe that this UUIDv4 generation function is not collision proof. If I understand the specification properly, Version 4 UUIDs require that the digits be created by a cryptographically secure random number generator. cafuego's patch uses the UUID extension if available; if not, it generates UUIDv4s using code in comment #65879 on uniqid. In the second case, the code uses mt_rand(), an implementation of the Mersenne Twister, which is not cryptographically secure.

I think part of the problem is making a cross-platform UUID generator implementation that is collision-proof. If you look at some of the other comments on uniqid, other people have implemented UUIDv4 generators that read directly from /dev/urandom, which isn't available on Windows.

I guess there are two ways to approach this - put the burden on the web server, or put the burden on the database server. The former is seeming to be more complicated. In the case of the latter, both MySQL and Postgres have UUID generation functions, but they're different. We could create a wrapper function that calls the proper function depending on the database platform being used. Part of the problem with statement-based replication in the case of this module is that functions like uuid_user() and uuid_nodeapi() call MySQL's UUID() function within an INSERT query. If we had another function that ran "SELECT UUID();" on MySQL, and the return value was passed to the INSERT query, I believe we would not have the same problems with statement-based replication.

b-prod’s picture

Status: Postponed (maintainer needs more info) » Needs review
StatusFileSize
new7.56 KB

Here is a patch that take care about UUID() compatibility with PgSQL (need tests from users of PgSQL).

The uuid_uuid() function returns a valid UUID, created with MySQL or PgSQL if version > 8.4 ; or created with the function suggested by anarchivist (http://drupal.org/node/502622#comment-1973150) - I changed spaces into dash to have the same result as with MySQL UUID().

So the replication problem no more exists by the way. The main disadvantage of this uuid_uuid() function is it makes an additional query in database, but it allow support of PgSQL.

anarchivist’s picture

@B-Prod, as far as I can tell this patch does necessarily work for Postgres; in my understanding, PgSQL doesn't come with a native UUID generation function which therefore requires an extension of some sort. The uuid-ossp extension is available (as well as a few modules). This seems to be the option suggested by the Postgres community.

In addition, this whole thing is potentially made more complicated by the availability of a UUID datatype in PgSQL 8.3 and higher. We can get around this by just not worrying about how the UUID is actually stored in the DB (i.e. by casting it as a formatted string).

killes@www.drop.org’s picture

Couldn't we get this applied and then worry about postgres when there are some people who want to use it with postgres ...?

b-prod’s picture

StatusFileSize
new6.81 KB

Actually, the uuid-ossp extension is available in PgSQL 8.4 but not enabled by default. So I created a new patch that use the anarchivist function if database type is PgSQL.

anarchivist’s picture

@B-Prod, yes, that's true, but uuid-ossp is also available for PgSQL 8.3. However, uuid-ossp does not provide a UUID() function; the comparable query to "SELECT UUID();" on MySQL would be "SELECT UUID_GENERATE_V4();" on Postgres.

Crell’s picture

Any progress here? I find myself in need of a good non-SQL-derived UUID operation myself and the patch in #12 looks like just what I want. I'm probably going to just steal the code but I hope it gets into a stable release of this module soon so I can just make a dependency. :-)

recidive’s picture

@Crell, does that means you tested and confirm the patch in #12 works and it's RTBC?

I'm probably taking a shot at cleaning up some things and tagging a stable release soon. Please help whenever you can.

cafuego’s picture

StatusFileSize
new7.07 KB

I reckon that if the PECL extension is available, the function should default to using that instead of the database call.

I've done a wee bit of benchmarking on creating a million UUIDs and even with the function_exists() call added, the pecl function call is in about 8 times faster than getting a UUID from the database. Not surprising, considering the database adds round trip delays etc.

  // Retrieve a million uuids via the database.
  $start_1 = microtime(TRUE);
  for($i = 0; $i < 1000000; $i++) {
    $uuid = db_result(db_query("SELECT UUID()"));
  }
  $end_1 = microtime(TRUE);

  // Retrieve a million uuids via the pecl extension.
  $start_2 = microtime(TRUE);
  for($i = 0; $i < 1000000; $i++) {
    if(function_exists("uuid_create")) {
      $uuid = uuid_create(UUID_TYPE_DEFAULT);
    }
  }
  $end_2 = microtime(TRUE);

  printf("Database: %f\n", $end_1 - $start_1);
  printf("Function: %f\n", $end_2 - $start_2);

Database: 68.645064
Function: 7.997510

This ratio remains pretty stable even if you generate only 100 UUIDs.

I've attached an updated patch.

cafuego’s picture

Actually, with a package update from php5-uuid 1.0.2 to php5-uuid 1.6.2 and now using libossp-uuid, the function call is actually 5-6 times slower than the database version. That is one gigantic performance drop.

Maybe back to the original order then and default to database if present.

recidive’s picture

StatusFileSize
new8.44 KB

Ok, I tested the patch and found a problem with it in uuid_sync() function. Since it's using INSERT INTO ... SELECT ... syntax, we can't pass a single UUID to it as it'll set the same UUID for all items it's generating.

We need another approach to this. We can SELECT, loop and INSERT, but this won't scale well for tons of records. Maybe we need batch API implemented for this?

This may be too much for this patch. So maybe we can implement this with simple loop and file a separate issue for batch API implementation.

What do you think?

I want this patch commited ASAP so I can tag a beta release.

I have been using this code in production for over a year now, and it's quite stable.

I'm attaching patch against DRUPAL-6--1 branch. HEAD is not in sync, so patches should be taken against DRUPAL-6--1. I also changed the order to mysql function first and added the taxonomy part.

recidive’s picture

Status: Needs review » Needs work
Crell’s picture

I didn't test the patch directly, but I copied the uuid_uuid() function to my own code and it generated something that looks unique. :-) I have not verified it mathematically or anything like that.

Is performance really a big concern? We're generating UUIDs. That's not a critical path operation 99% of the time, so a few microseconds difference doesn't matter. There's plenty of other places to optimize.

Also note that the copyright portion of the docblock needs to be removed. Hell, I'd almost like to see that function in core, but I suppose that's probably asking too much. :-)

recidive’s picture

Ok, at this point what we need to decide is how we do to sync up UUIDs. The way it's coded in the patch don't work, and generate duplicates. Should we simple do SELECT -> loop -> INSERT?

Crell’s picture

What do you mean sync up? If the UUID is not generated by the DB as part of a larger insert/update statement then it is only run once, on the master. Problem solved. IMO we should bypass the DB anyway and just use the PECL module / user-space clone of it and be done with it so that we're using the same algorithm.

recidive’s picture

Check uuid_sync() it is called in hook_enable() for generating UUID for content you already have.

It currently calls queries like that:

db_query("INSERT INTO {uuid_node} SELECT n.nid, UUID() FROM {node} AS n WHERE n.type = '%s' AND NOT EXISTS (SELECT nid FROM {uuid_node} WHERE nid = n.nid)", $type);

the proposed change for change this for using uuid_uuid() instead of UUID() is:

db_query("INSERT INTO {uuid_node} SELECT n.nid, '%s' FROM {node} AS n WHERE n.type = '%s' AND NOT EXISTS (SELECT nid FROM {uuid_node} WHERE nid = n.nid)", uuid_uuid(), $type);

This will generate a lot of duplicates, since the uuid_uuid() function will be called only once, thus will generate only one UUID that will be used for inserting all missing rows.

That's the part that needs fixing in the patch, we should just change that to a loop, or implement batch API.

cafuego’s picture

My original patch did indeed run a nice loop in uuid_sync().

cafuego’s picture

Status: Needs work » Needs review
StatusFileSize
new9.56 KB

Hrm. My original patch did indeed run a nice loop in uuid_sync() and fix the problem there.

I've re-applied those changes in the attached patch, which should apply to DRUPAL-6--1. This one also touches the uuid.install file, it removes some whitespace there ;-)

Speaking of the install file - I notice the uuid columns in the schema are indexed, but are in fact not unique keys. Since the whole point of a uUid is to be unique, would it be worth dropping the current index on each table and replacing it with a unique key?

That way if the system manages to generate a non-unique UuID and tries to insert it, the user will know about it. If we have a way of determining the error value thrown by the query, we can in fact quietly re-run such an insert with a new UUID and make the problem go away.

cafuego’s picture

StatusFileSize
new11.17 KB

Ugh, I forgot to redo the vocabulary and term_data queries in uuid.admin.inc.

The attached r5 patch includes all fixes from r4; and

  • fixes the uuid bulk generate queries for vocabularies and term_data
  • drops the indexes on all tables and adds unique keys instead
  • fixes a small typo (it's => its) on the admin page
  • removes the copyright part of the comment block in the uuid_uuid() function

Seems to work happily on my test box. I have over 100,000 test users on that thing and bulk generating UUIDs for them was fine (took 20-odd seconds).

recidive’s picture

Priority: Normal » Critical
StatusFileSize
new11.31 KB

Thanks cafuego for moving this along.

I took another crack at this and have replaced your approach for avoiding custom queries when syncing nodes with this (untested):


  $types = variable_get('uuid_automatic_for_nodes', array());

  // Remove disabled node types.
  $types = array_filter($types);
  if (!empty($types)) {
    $placeholders = db_placeholders($types, 'varchar');

    // Nodes.
    $result = db_query("SELECT nid FROM {node} WHERE type IN (" . $placeholders . ") AND nid NOT IN (SELECT nid FROM {uuid_node})", $types);
    while ($item = db_fetch_object($result)) {
      db_query("INSERT INTO {uuid_node} (nid, uuid) VALUES(%d, '%s')", $item->nid, uuid_uuid());
    }

    // Revisions.
    $result = db_query("SELECT nr.vid AS vid FROM {node_revisions} AS nr INNER JOIN {node} n ON nr.nid = n.nid WHERE n.type IN (" . $placeholders . ") AND nr.vid NOT IN (SELECT vid FROM {uuid_node_revisions})", $types);
    while ($item = db_fetch_object($result)) {
      db_query("INSERT INTO {uuid_node_revisions} (vid, uuid) VALUES(%d, '%s')", $item->vid, uuid_uuid());
    }
  }

Do you think this will work? Can you test syncing UUIDs with this new patch, so we can commit it and move on?

Are you sure $types variable has more values than the types which have UUID generation enabled? Can you paste a dump of $types here for us to explain?

Also, have you tested the indexes -> unique keys upgrade path?

Thanks again.

cafuego’s picture

Status: Fixed » Needs review
StatusFileSize
new11.43 KB

Are you sure $types variable has more values than the types which have UUID generation enabled? Can you paste a dump of $types here for us to explain?

Yup. The admin form always returns an array that is keyed with all node types. The value for the selected ones is the same as the key; the value for non-selected one is 0. Here are mine:

Before array_filter()

Array (
    [page] => page
    [profile] => 0
    [story] => 0
)

After array_filter()

Array (
    [page] => page
)

So without the array_filter, you end up running the {node} and {uuid_node} queries with type = '0' for each of the node types you didn't want a uuid for. They'll fail and not break anything, but I think it's kind of ugly :-) I've added it in for vids as well in the patch attached below, as that suffers from the same issue. With my single vocabulary I got (without array_filter):

query: SELECT nr.vid AS vid FROM node_revisions AS nr INNER JOIN node n ON nr.nid = n.nid WHERE n.type IN ('profile') AND nr.vid NOT IN (SELECT vid FROM uuid_node_revisions)

query: SELECT v.vid FROM vocabulary AS v WHERE v.vid IN (0) AND NOT EXISTS (SELECT vid FROM uuid_vocabulary WHERE vid = v.vid)

... and I'm sure we're better off avoiding that.

Do you think this will work? Can you test syncing UUIDs with this new patch, so we can commit it and move on?

It will work, as it now won't replicate a statement with a UUID() function but an actual string, so the same string will end up on all MySQL servers. Problem gone :-) I've been running with a version of this patch since June and I've had no replication related issues since then. If I load a node by uuid on the slave, I get the same result as on the master :-)

Also, have you tested the indexes -> unique keys upgrade path?

The following queries were executed
uuid module
Update #6002

    * ALTER TABLE {uuid_node} DROP INDEX uuid_node_uuid_idx
    * ALTER TABLE {uuid_node} ADD UNIQUE KEY uuid_node_uuid_key (uuid)
    * ALTER TABLE {uuid_node_revisions} DROP INDEX uuid_node_revisions_uuid_idx
    * ALTER TABLE {uuid_node_revisions} ADD UNIQUE KEY uuid_node_revisions_uuid_key (uuid)
    * ALTER TABLE {uuid_users} DROP INDEX uuid_users_uuid_idx
    * ALTER TABLE {uuid_users} ADD UNIQUE KEY uuid_users_uuid_key (uuid)
    * ALTER TABLE {uuid_vocabulary} DROP INDEX uuid_vocabulary_uuid_idx
    * ALTER TABLE {uuid_vocabulary} ADD UNIQUE KEY uuid_vocabulary_uuid_key (uuid)
    * ALTER TABLE {uuid_term_data} DROP INDEX uuid_term_data_uuid_idx
    * ALTER TABLE {uuid_term_data} ADD UNIQUE KEY uuid_term_data_uuid_key (uuid)

So that seems to work OK. Note that *if* a user's site has non-unique UUID data in any of the uuid tables already, this will fail for that table.

Actually, it's likely an upgrade from 6000 to 6002 will throw some errors, as the 6001 update at that stage will already add the unique keys on the uuid_vocabulary and uuid_term_data tables as they're created. That will make 6002 display an error for those tables. I can't find any api docs on functions that test if a given index exists on a given table and I am hesitant to stick in queries that will potentially not run on Postgres, seeing as this patch would make UUID work on Postgres.

It may be better to check the uuid module version before the upgrade and display a drupal_set_message() that it's OK to ignore errors for the uuid_vocabulary and uuid_term_data tables if the version the user started from was 6000.

A cosmetic fix for a next version would be to disable the 'Create missing UUIDs' button if the checkboxes or radio buttons have unsaved changes. I kept finding myself clicking it after checking the node type boxes and then losing my settings :-)

recidive’s picture

Status: Needs review » Fixed

Commited to DRUPAL-6--1 branch.

I've also tagged a beta1 release that should be packed soon.

Thanks!

cafuego’s picture

Status: Needs review » Fixed

Yay :-)

Status: Fixed » Closed (fixed)

Automatically closed -- issue fixed for 2 weeks with no activity.