I'm trying to implement a feature that detects whether or not an account has already signed up. Now when doing parallel requests I'm getting a deadlock. I think I understand why it is happening but I'm unsure on how to solve it.
Here's a simplified version of what I'm doing;
START TRANSACTION;
-- check if user has already signed up (returned rows > 0, throw error if so)
SELECT * FROM users WHERE email = '[email protected]' FOR UPDATE;
-- user has not signed up yet.. create the account.
INSERT INTO users SET ...;
COMMIT;
Now this in itself works fine. However when two parallel request happen, a deadlock is made because the transaction will both create a FOR UPDATE
lock, which is allowed because initially when there is no account signed up yet there are no rows to lock. Atleast, that's what I think is happening.. correct me if I'm wrong.
I'm curious on how I were to fix this, I still want to check whether not an account has registered already so I can show the user a message. Of course the email has a unique constraint
but I do not want to rely on that because the auto increment
index will increment, even when it violates the constraint.
Also I'm using typeorm, a sample of my code;
public async registerUser(email: string, password: string, displayName?: string) {
const connection = await getConnection();
connection.transaction(async (manager) => {
// First we need to make sure that this email isn't already registered. If
// it has been registered we can throw a simple UserError which will be
// caught by our error handler.
const hasAlreadyRegistered = await this.findUser(email, manager);
if (hasAlreadyRegistered) throw new UserError('Email has already been registered.');
// At last we can create the user, linking him to the previously created
// authentication strategy.
const user = new User();
user.email = email;
user.displayName = displayName || randomBytes(8).toString('hex');
user.strategies = [authentication];
await manager.save(user);
logger.silly('> Created user row.');
return user;
});
}
I have solved this by just checking for the constraint error in the end (per suggestion of @Shadow). It saves me a lot of hassle.
Code
try {
await manager.save(user);
} catch (err: any) {
// Check whether or not this entry violates an unique constraint.
if (err.code === 'ER_DUP_ENTRY') {
throw new UserError('Email has already been registered.');
} else throw err;
}