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??
| Comment | File | Size | Author |
|---|---|---|---|
| #47 | 633678-webchick-reroll.patch | 5.23 KB | josh waihi |
| #42 | 633678-better-locking.patch | 4.4 KB | josh waihi |
| #41 | 633678-use-sequences-instead.patch | 4.75 KB | josh waihi |
| #31 | 633678-better-comments.patch | 4.79 KB | josh waihi |
| #29 | 633678-hey-my-patch-works.patch | 4.16 KB | josh waihi |
Comments
Comment #1
david straussWe 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.
Comment #2
david straussSpecifically, 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 insettings.php.Most importantly, this solution would be fast and work on any supported database.
Comment #3
josh waihi commentedI like David's suggestion here. So we could write something like:
Which would even suit PostgreSQL :)
Comment #4
chx commentedDo 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...
Comment #5
josh waihi commentedWell, we could automate it ourselves:
Comment #6
david strauss@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), setssequence_minimumto 98 (= 100 - 1 - 1).* Thread 2: Notices $id <
sequence_minimum(read as 0), setssequence_minimumto 7 (= 10 - 2 - 1).* Thread 1: Returns 99 (= 98 + 1).
* Thread 2: Returns 9 (= 7 + 2).
* Result:
sequence_minimumis 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_idis also misused here. We don't want to increase thesequence_minimumevery time we request an ID. Setting variables like that would be a performance nightmare.Comment #7
Crell commentedWouldn'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.
Comment #8
david strauss@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.)
Comment #9
josh waihi commentedDon'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
Comment #10
josh waihi commentedactually that whole destruct idea wouldn't work. It would virtually turn Drupal into a single thread system
Comment #11
chx commentedWhat 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.
Comment #12
josh waihi commentedWe 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:
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.
Comment #13
Crell commentedDoesn't postgres HAVE a sequences system of its own that could be used?
Comment #14
chx commentedNo. 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.
Comment #15
josh waihi commentedWhat 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.
Comment #16
david straussFixing spelling.
Comment #17
andypostNow 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
Comment #18
chx commentednote that sequences API is more than users.
Comment #19
chx commentedYesterday we had a discussion with Damien.
Comment #20
Crell commentedThis 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.
Comment #21
josh waihi commentedhere 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
Comment #23
josh waihi commentedwhoops, lets try that again.
Comment #25
chx commentedThe 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.
Comment #26
josh waihi commentedI'm using this to create my diff files:
has worked in the passed
Comment #28
josh waihi commentedattached is patch that works for PostgreSQL, need sqlite implementation added to it though. Chx, can you do that part?
Comment #29
josh waihi commentedI 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.
Comment #30
chx commentedThere 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.
Comment #31
josh waihi commentedcool, attached are some better comments.
Comment #32
webchickHm. 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.
Comment #34
dave reidMy bad.
Comment #35
Crell commentedI 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.
Comment #38
josh waihi commentedAccording to my research (http://www.postgresql.org/docs/8.1/static/sql-savepoint.html) and talking in #postgres,
SAVEPOINT nextidallows PostgreSQL to rollback to that point without failing the transaction usingROLLBACK 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 onCOMMITHow 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:
But would it pass on MySQL. Crell, webchick - back to you.
Comment #39
david strauss@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.Comment #40
david straussAnother issue may ease the implementation of this one: http://drupal.org/node/669794
Comment #41
josh waihi commentedI've re-written the patch for a few reasons:
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.
Comment #42
josh waihi commentedAfter 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.
Comment #43
shunting commented633678-better-locking.patch works for me on PostGres 8.3 for OS X.
Thanks so much, this was driving me nuts.
Comment #44
Crell commented#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.
Comment #45
josh waihi commentedI've tested and confirmed that #42 is a good solution. DamZ agreed with me. Lets get this in and move on to Sqlite implementation.
Comment #46
webchickFor some reason the database.inc hunk is failing for me. :( I can haz re-roll?
Comment #47
josh waihi commenteddone. 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
Comment #50
josh waihi commentedcool, test bot is happy.
Comment #51
webchickOk, great. Committed to HEAD. Thanks! :)
Comment #53
ivansb@drupal.orgI 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));