Add locking to save_account

This commit is contained in:
KernelDeimos 2024-05-15 18:22:54 -04:00
parent b72e5b7e02
commit 691c8f1436
4 changed files with 226 additions and 108 deletions

View File

@ -210,6 +210,9 @@ const install = async ({ services, app }) => {
const { AntiCSRFService } = require('./services/auth/AntiCSRFService'); const { AntiCSRFService } = require('./services/auth/AntiCSRFService');
services.registerService('anti-csrf', AntiCSRFService); services.registerService('anti-csrf', AntiCSRFService);
const { LockService } = require('./services/LockService');
services.registerService('lock', LockService);
} }
const install_legacy = async ({ services }) => { const install_legacy = async ({ services }) => {

View File

@ -75,6 +75,14 @@ router.post('/save_account', auth, express.json(), async (req, res, next)=>{
return res.status(429).send('Too many requests.'); return res.status(429).send('Too many requests.');
} }
const svc_lock = req.services.get('lock');
return svc_lock.lock([
`save-account:username:${req.body.username}`,
`save-account:email:${req.body.email}`
], async () => {
await new Promise((rslv) => {
setTimeout(rslv, 5000);
});
// duplicate username check, do this only if user has supplied a new username // duplicate username check, do this only if user has supplied a new username
if(req.body.username !== req.user.username && await username_exists(req.body.username)) if(req.body.username !== req.user.username && await username_exists(req.body.username))
return res.status(400).send('This username already exists in our database. Please use another one.'); return res.status(400).send('This username already exists in our database. Please use another one.');
@ -192,6 +200,7 @@ router.post('/save_account', auth, express.json(), async (req, res, next)=>{
referral_code: user.referral_code, referral_code: user.referral_code,
} }
}) })
});
}) })
module.exports = router module.exports = router

View File

@ -0,0 +1,105 @@
const { RWLock } = require("../util/lockutil");
const BaseService = require("./BaseService");
/**
* LockService implements robust critical sections when the behavior
* might return early or throw an error.
*
* This serivces uses RWLock but always locks in write mode.
*/
class LockService extends BaseService {
async _construct () {
this.locks = {};
}
async _init () {
const svc_commands = this.services.get('commands');
svc_commands.registerCommands('lock', [
{
id: 'locks',
description: 'lists locks',
handler: async (args, log) => {
for ( const name in this.locks ) {
let line = name + ': ';
if ( this.locks[name].effective_mode === RWLock.TYPE_READ ) {
line += `READING (${this.locks[name].readers_})`;
log.log(line);
}
else
if ( this.locks[name].effective_mode === RWLock.TYPE_WRITE ) {
line += 'WRITING';
log.log(line);
}
else {
line += 'UNKNOWN';
log.log(line);
// log the lock's internal state
const lines = JSON.stringify(
this.locks[name],
null, 2
).split('\n');
for ( const line of lines ) {
log.log(' -> ' + line);
}
}
}
}
}
]);
}
async lock (name, opt_options, callback) {
if ( typeof opt_options === 'function' ) {
callback = opt_options;
opt_options = {};
}
// If name is an array, lock all of them
if ( Array.isArray(name) ) {
const names = name;
// TODO: verbose log option by service
// console.log('LOCKING NAMES', names)
const section = names.reduce((current_callback, name) => {
return async () => {
return await this.lock(name, opt_options, current_callback);
};
}, callback);
return await section();
}
if ( ! this.locks[name] ) {
const rwlock = new RWLock();
this.locks[name] = rwlock;
}
const handle = await this.locks[name].wlock();
// TODO: verbose log option by service
// console.log(`\x1B[36;1mLOCK (${name})\x1B[0m`);
let timeout, timed_out;
if ( opt_options.timeout ) {
timeout = setTimeout(() => {
handle.unlock();
// TODO: verbose log option by service
// throw new Error(`lock ${name} timed out`);
}, opt_options.timeout);
}
try {
return await callback();
} finally {
if ( timeout ) {
clearTimeout(timeout);
}
if ( ! timed_out ) {
// TODO: verbose log option by service
// console.log(`\x1B[36;1mUNLOCK (${name})\x1B[0m`);
handle.unlock();
}
}
}
}
module.exports = { LockService };

View File

@ -23,6 +23,7 @@ const BaseService = require("../BaseService");
const MODE_READ = Symbol('read'); const MODE_READ = Symbol('read');
const MODE_WRITE = Symbol('write'); const MODE_WRITE = Symbol('write');
// TODO: DRY: could use LockService now
class FSLockService extends BaseService { class FSLockService extends BaseService {
async _construct () { async _construct () {
this.locks = {}; this.locks = {};