The default sequence API is broken but tests pass because MySQL overrides the implementation. The issue as it lies is that we shouldn't write UPDATE queries to the sequence column because it is a serial column. However, occasions where the next requested sequence value is a lot larger than the current value could result in a lot of INSERT queries which will slow performance rapidly. Such an example would be when importing masses of users.

I currently don't see a solution that will satisfy at the moment. The best I can come up with is:

public function nextId($existing_id = 0) {
    $transaction = $this->startTransaction();
    do { 
      $id = $this->query("INSERT INTO {sequences} (value) VALUES (default)");
    }
    while ($existing_id > $id);
    return $id;
}

This implementation will be lengthy during things like user imports, however - user imports could make updating the sequence API a task in a bulk import.

Thoughts??

Comments

david strauss’s picture

We could store a base and use a sequence beginning with one added to it. There's really no need to make the original sequence value big just to start the sequence big.

david strauss’s picture

Specifically, we could drop the last ID argument and simply add variable_get('sequence_minimum', 0) to the sequence value before we return. That would allow users to set a minimum value in settings.php.

Most importantly, this solution would be fast and work on any supported database.

josh waihi’s picture

I like David's suggestion here. So we could write something like:

public function nextId() {
    return variable_get('sequence_minimum', 0) + $this->query("INSERT INTO {sequences} (value) VALUES (default)");
}

Which would even suit PostgreSQL :)

chx’s picture

Do not kill automatism. I already said so and will continue: it's not the task of the user to keep anything up to date. That's a poor implementation.

While the variable is a good idea, we need to keep it auto updated , race condition free...

josh waihi’s picture

Well, we could automate it ourselves:

public function nextId($existing_id = 0) {
    $transaction = $this->startTransaction();
    $id = $this->query("INSERT INTO {sequences} (value) VALUES (default)");
    if ((variable_get('sequence_minimum', 0) + $id) < $existing_id)) {
        $sequence_minimum = $existing_id - $id - 1;
        variable_set('sequence_minimum', $sequence_minimum);
    }
    return variable_get('sequence_minimum', 0) + $id;
}
david strauss’s picture

@Josh #5 is not race condition-free. Note that the analysis below is transaction isolation level-dependent.

* Thread 1: Variables are loaded.
* Thread 2: Variables are loaded.
* Thread 1: Calls nextId(100)
* Thread 2: Calls nextId(10)
* Thread 1: Gets $id = 1
* Thread 2: Gets $id = 2
* Thread 1: Notices $id < sequence_minimum (read as 0), sets sequence_minimum to 98 (= 100 - 1 - 1).
* Thread 2: Notices $id < sequence_minimum (read as 0), sets sequence_minimum to 7 (= 10 - 2 - 1).
* Thread 1: Returns 99 (= 98 + 1).
* Thread 2: Returns 9 (= 7 + 2).
* Result: sequence_minimum is set to 7, despite a value of 99 being issued in a different thread. At a later point, 99 could be even re-issued!

$existing_id is also misused here. We don't want to increase the sequence_minimum every time we request an ID. Setting variables like that would be a performance nightmare.

Crell’s picture

Wouldn't that introduce unwanted drift? Over time, the sequence Id in the DB would get more and more out of sync with reality based on the variable's "fudge factor". That makes me uncomfortable.

david strauss’s picture

@Crell No, the "fudge factor" should only update if there's a radical change in IDs. During normal site operation, it shouldn't change at all; it should be a constant offset. (Also, calling variable_set() for all new keys would be a huge problem.)

josh waihi’s picture

Don't we have a lock API now too? we can make this atomic with that no? If need be we could set the offset as a property in the class definition and update it by calling variable_set on __destruct

josh waihi’s picture

actually that whole destruct idea wouldn't work. It would virtually turn Drupal into a single thread system

chx’s picture

What should happen here is that you try to get an id first. If it's bigger than existing_id plus offset which is the common case, return it. If it's lower then try to acquire a lock. When succeeded, read the offset and if it's still too small, update it and return. If it became large enough, good for you.

josh waihi’s picture

We can't actually use variable_get/set because they cache and break atomicity so how about this instead: We add another column to the sequences table that stores the offset and defaults to 0:


public function nextId($existing_id = 0) {
    $transaction = $this->startTransaction();
    // Returning NULL prevents PostgreSQL from doing an additional query.
    $this->query("INSERT INTO {sequences} (value, seq_min) VALUES (default, (SELECT MAX(seq_min) FROM {sequences}))", array(), array('return' => Database::RETURN_NULL));
    $id = $this->query("SELECT (MAX(seq_min) + MAX(value) FROM {sequences}")->fetchField();
    if ($id <= $existing) {
        $this->query("UPDATE {sequences} SET seq_min = (:existing - MAX(value)) WHERE value = MAX(value)", array(':existing' => $existing_id));
        $id = $this->query("SELECT (MAX(seq_min) + MAX(value)) FROM {sequences}")->fetchField();
    }
    return $id;
}

The transaction should mean we don't need to take out a lock. This uses a maximum of 4 queries and a minimum of 2.

Crell’s picture

Doesn't postgres HAVE a sequences system of its own that could be used?

chx’s picture

No. Read the original issue, #35 I still remember that. You can not make pgsql sequences to jump beyond a certain value in a race condition free way that's why we are here. You can INSERT into the same table you SELECT from? Wow.

josh waihi’s picture

You can INSERT into the same table you SELECT from? Wow

What is that suppose to mean? PostgreSQL does a SELECT to get the last insert id (thats inside the PDO driver).

So any objections with the implementation? If not I'll write a patch. Will also truncate the table as all we need is sequence and offset.

david strauss’s picture

Title: Make sequnece API work on non-MySQL databases » Make sequence API work on non-MySQL databases

Fixing spelling.

andypost’s picture

Now we're going to invent a pair of crutches due to the fact that forgot about #350407: Anonymous should not appear in the users table at all and commit #356074: Provide a sequences API

chx’s picture

note that sequences API is more than users.

chx’s picture

Yesterday we had a discussion with Damien.

  1. There will not be a default, nextid becomes abstract. This is not ANSI SQL trying to find a default is pointless.
  2. When PostgreSQL gets a value too small then it will lock the table, retry the INSERT and if it's still too small then alter the sequence. There is no lock-free to do this. This is a rare enough operation not to worry on locking As you can't lock a sequence, we need the table.
  3. SQLite can write into a serial so it can simply INSERT -- too small -- write a larger one - INSERT again -- return the results.
Crell’s picture

This makes sense to me. Although by "too small", do you mean "query fails"? Remember that any query failure is going to break any open transaction. So make sure that running nextId() inside a transaction won't break the transaction even if it has to loop up to the next available ID.

josh waihi’s picture

Status: Active » Needs review
StatusFileSize
new3.97 KB

here is the PostgreSQL implementation as suggested by chx and DamZ. Notice I'm not using the Drupal transaction API because postgreSQL needs to commit the transaction to release the table lock (PostgreSQL allows you to commit a transaction within a transaction :))

I haven't tested this but I will try get some time to do so

Status: Needs review » Needs work

The last submitted patch failed testing.

josh waihi’s picture

Status: Needs work » Needs review
StatusFileSize
new6.89 KB

whoops, lets try that again.

Status: Needs review » Needs work

The last submitted patch failed testing.

chx’s picture

The patch is really messed up, it shows the powers of git nicely. Also, use $existing + 1 not ++$existing since you do not use $existing any more.

josh waihi’s picture

Status: Needs work » Needs review
StatusFileSize
new2.91 KB

I'm using this to create my diff files:

git diff --no-prefix origin/master

has worked in the passed

Status: Needs review » Needs work

The last submitted patch failed testing.

josh waihi’s picture

Status: Needs work » Needs review
StatusFileSize
new3.61 KB

attached is patch that works for PostgreSQL, need sqlite implementation added to it though. Chx, can you do that part?

josh waihi’s picture

StatusFileSize
new4.16 KB

I actually found some time to test my patch - I had a few syntax issues with DBTNG - but I got the sequences API working on PostgreSQL. Still need someone to make it work on SQLite.

chx’s picture

Status: Needs review » Reviewed & tested by the community

There is always the next commit if this works on pgsql, get it in and then set it to CNW and I will get to it next week.

josh waihi’s picture

StatusFileSize
new4.79 KB

cool, attached are some better comments.

webchick’s picture

Hm. It's not clear to me if Crell's points in #20 have been addressed? I would expect to see some expanded test coverage with this patch.

Status: Reviewed & tested by the community » Needs work

The last submitted patch, , failed testing.

dave reid’s picture

Status: Needs work » Needs review

My bad.

Crell’s picture

I don't know enough about Postgres' handling of transactions to say if this will choke or not. I've never had to work with savepoints. That may well avoid the "roll back all of it" problem that MySQL has and that our base system inherits.

I defer to Josh on that question, although I agree with webchick that I'd like to see a unit test to confirm that we handle "nextID inside a transaction" gracefully.

Re-test of 633678-better-comments.patch from comment #31 was requested by Josh Waihi.

josh waihi’s picture

According to my research (http://www.postgresql.org/docs/8.1/static/sql-savepoint.html) and talking in #postgres, SAVEPOINT nextid allows PostgreSQL to rollback to that point without failing the transaction using ROLLBACK TO SAVEPOINT nextid. Because the savepoint is labeled the same, a new savepoint (calling nextid again, and the situation occuring again) will overwrite the old savepoint. Savepoints are released on COMMIT

How can this be tested? Does simpletest support race conditions? I've tested PostgreSQL on the sequences API tests, and my patch passes it. If you wanted to test the sequences API within a transaction, we could do this:

  /**
   * Test that the Sequences API works within a transaction.
   */
  function testDbNextIdTransaction() {
    $first = db_next_id();
    
    $transaction = db_transaction();

    $second = db_next_id($first);
    $this->assertEqual($first + 1, $second, t('Sequence provides a larger number than the existing ID within a transaction.'));
    
    // When the transaction fails, the sequence will be reset the first id.
    $transaction->rollback();
    unset($transaction);
    
    $third = db_next_id();
    $this->assertEqual($second, $third, t('Sequence is rolled back to a value before the transaction.'));
  }

But would it pass on MySQL. Crell, webchick - back to you.

david strauss’s picture

@Crell You seem to be implying that MySQL doesn't support SAVEPOINT, which it does: http://dev.mysql.com/doc/refman/5.0/en/savepoint.html We could make use of savepoints to simulate true nested transactions, at least on MySQL, PostgreSQL, and SQLite. We wouldn't even have to make real changes to the transaction API. We could name the snapshots based on nesting depth and release them as nested "commit"s happen.

david strauss’s picture

Another issue may ease the implementation of this one: http://drupal.org/node/669794

josh waihi’s picture

StatusFileSize
new4.75 KB

I've re-written the patch for a few reasons:

  1. PostgreSQL needed to clean up the sequences table similar to what MySQL does. Then I realised PostgreSQL doesn't actually need to insert into the table at all but merely use it as a lock when altering the sequence. So now, my patch retrieves the next value in the sequences directly from the sequence within PostgreSQL. This also prevents having to register a shutdown function.
  2. PostgreSQL sequences live outside the scope of transactions. If a sequence is altered within a transaction and the transaction is rolled back, the sequence remains altered. So long our logic raised the sequence value rather than lower it. I don't see this as being an issue. However, this did raise the question if a SAVEPOINT was needed. They are however, in case a LOCK on the table cannot be acquired.

I've tested this against simpletests sequence API tests and it works fine. I don't believe more tests are needed to prove functionality.
Lets get this in ASAP so I can move on with fix the other areas of Drupal that are broken in PostgreSQL.

josh waihi’s picture

StatusFileSize
new4.4 KB

After talking with DamZ in IRC, we implemented a better locking system with PostgreSQL Advisory Locks which are designed to be used by applications rather than designed to lock tables (though they can). Advisory locks require a numeric id to hold as the lock identifier I've created the POSTGRESQL_NEXTID_LOCK constant to hold this value.

shunting’s picture

633678-better-locking.patch works for me on PostGres 8.3 for OS X.

Thanks so much, this was driving me nuts.

Crell’s picture

#42 looks a lot cleaner to my eyes, although I'm still no postgres expert. I'd say this is done when Josh and DamZ say it is. :-) I like where it's going, though.

josh waihi’s picture

Status: Needs review » Reviewed & tested by the community

I've tested and confirmed that #42 is a good solution. DamZ agreed with me. Lets get this in and move on to Sqlite implementation.

webchick’s picture

Status: Reviewed & tested by the community » Needs work

For some reason the database.inc hunk is failing for me. :( I can haz re-roll?

josh waihi’s picture

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

done. Also pushed the implemented default nextID() to the sqlite driver since it didn't have an implementation. This is a TODO for chx or DamZ

Status: Needs review » Needs work
Issue tags: -PostgreSQL, -Database API, -Sequence API

The last submitted patch, 633678-webchick-reroll.patch, failed testing.

Status: Needs work » Needs review
Issue tags: +PostgreSQL, +Database API, +Sequence API

Re-test of 633678-webchick-reroll.patch from comment #47 was requested by Josh Waihi.

josh waihi’s picture

Status: Needs review » Reviewed & tested by the community

cool, test bot is happy.

webchick’s picture

Status: Reviewed & tested by the community » Fixed

Ok, great. Committed to HEAD. Thanks! :)

Status: Fixed » Closed (fixed)

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

ivansb@drupal.org’s picture

I don't like the whole Idea of locking and overriding how nextval works to push the sequence further neither I like the use of max() in save_user (when you've concurrency max is not going to work!) but what about this technique for postgresql?

select setval('pizza', greatest(nextval('pizza'), 32));