Skip to content
Advertisement

Sequelize: Avoid Race Condition When Updating Field with Unique Value?

I have a field in my postgres db called authorizedUserNumber. This field is set by default to 0, and does not auto-increment, because it is only asigned when a user has been fully onboarded.

Okay, so let’s say a new user has been fully onboarded and I want to assign a unique number to the field authorizedUserNumber. In the event I have multiple servers running, I want to detect collisions of unique numbers in this field, so as to protect against race conditions.

I thought of defining authorizedUserNumber as a Sequelize unique field, and trying something like this:

// get current max authorizedUserNumber
let userWithMaxOnboardedauthorizedUserNumber = await connectors.usersClinical.findAll({
    attributes: [
        sequelize.fn('MAX', sequelize.col('authorizedUserNumber'))
    ],
});
let newLatestauthorizedUserNumber = userWithMaxOnboardedauthorizedUserNumber[0].authorizedUserNumber;
newLatestauthorizedUserNumber += 1;

let numAttempts = 0
let done = false;
while ((!done) && (numAttempts <= 50)){
    try{
        user = await updateUser(user.userId, {authorizedUserNumber: newLatestauthorizedUserNumber});
        done = true;
    }catch(e){
        // a unique field will throw an error if you try to store a duplicate value to it
        console.log(`Collision in assigning unique authorizedUserNumber. UserId: ${user.userId}`);
        newLatestauthorizedUserNumber += 1;
        numAttempts+= 1;
    }
}

if (!done){
    console.error(`Could not assign unique authorizedUserNumber. UserId: ${user.userId}`);
}

The problem with this code, is that if the field authorizedUserNumber is defined as unique, then I can’t put a default value in it. So there’s no way to have it be empty prior to having the correct value placed in it.

What’s the best practice for dealing with this sort of situation?

UPDATE:

Thanks to @Belayer for providing the solution.

Here are some notes on how I implemented it in Sequelize/Postgres.

Sequelize, AFAICT, does not yet support sequences. So I used a raw query in Sequelize to create the sequence:

let sql = `
CREATE SEQUENCE authorizedUserNumber_seq
START WITH 1
INCREMENT BY 1;`

let result;
try{
    result = await db.query(sql);
    console.log(`sql code to create authorizedUserNumber_seq has been run successfully.`)
}
catch(e){
    result = null;
    console.error(`Error in creating authorizedUserNumber_seq.`)
}

Then when it’s time to authorize the new user and assign a unique user number, I again use a raw query, with the following sql:

let sql = `UPDATE usersClinical
           SET "authorizedUserNumber" =  nextval('authorizedUserNumber_seq')
           WHERE "userId" = '${user.userId}';`

Advertisement

Answer

Rather than defaulting to 0 just let the column be null when not set. Since the default is null there can be any number of them without violating a unique constraint. Then create a sequence for that column (do not set as column default). There is no requirement for a sequence to auto-increment, the nextval can be assigned when needed. Make the assignment from the sequence when the new user becomes fully on-boarded.

create table users ( id          integer generated always as identity
                   , name        text
                   , assignedid  integer
                   , constraint  assigned_uk unique (assignedid) 
                   ) ;                  

create sequence user_assigned_seq;    

You can even make the assignment when user is created if desired. (see demo )

User contributions licensed under: CC BY-SA
6 People found this is helpful
Advertisement