* Fix major issue with SQLite transactions + aync code causing collisions

This commit is contained in:
Bryan Ashby 2017-09-16 17:13:11 -06:00
parent 68247d87e8
commit 1e250f06d9
8 changed files with 116 additions and 122 deletions

View File

@ -6,6 +6,7 @@ const conf = require('./config.js');
// deps // deps
const sqlite3 = require('sqlite3'); const sqlite3 = require('sqlite3');
const sqlite3Trans = require('sqlite3-transactions');
const paths = require('path'); const paths = require('path');
const async = require('async'); const async = require('async');
const _ = require('lodash'); const _ = require('lodash');
@ -13,14 +14,19 @@ const assert = require('assert');
const moment = require('moment'); const moment = require('moment');
// database handles // database handles
let dbs = {}; const dbs = {};
exports.getTransactionDatabase = getTransactionDatabase;
exports.getModDatabasePath = getModDatabasePath; exports.getModDatabasePath = getModDatabasePath;
exports.getISOTimestampString = getISOTimestampString; exports.getISOTimestampString = getISOTimestampString;
exports.initializeDatabases = initializeDatabases; exports.initializeDatabases = initializeDatabases;
exports.dbs = dbs; exports.dbs = dbs;
function getTransactionDatabase(db) {
return new sqlite3Trans.TransactionDatabase(db);
}
function getDatabasePath(name) { function getDatabasePath(name) {
return paths.join(conf.config.paths.db, `${name}.sqlite3`); return paths.join(conf.config.paths.db, `${name}.sqlite3`);
} }
@ -55,7 +61,7 @@ function getISOTimestampString(ts) {
function initializeDatabases(cb) { function initializeDatabases(cb) {
async.eachSeries( [ 'system', 'user', 'message', 'file' ], (dbName, next) => { async.eachSeries( [ 'system', 'user', 'message', 'file' ], (dbName, next) => {
dbs[dbName] = new sqlite3.Database(getDatabasePath(dbName), err => { dbs[dbName] = new sqlite3Trans.TransactionDatabase(new sqlite3.Database(getDatabasePath(dbName), err => {
if(err) { if(err) {
return cb(err); return cb(err);
} }
@ -65,7 +71,7 @@ function initializeDatabases(cb) {
return next(null); return next(null);
}); });
}); });
}); }));
}, err => { }, err => {
return cb(err); return cb(err);
}); });

View File

@ -110,9 +110,8 @@ module.exports = class FileEntry {
} }
const self = this; const self = this;
let inTransaction = false;
async.series( async.waterfall(
[ [
function check(callback) { function check(callback) {
if(isUpdate && !self.fileId) { if(isUpdate && !self.fileId) {
@ -121,22 +120,20 @@ module.exports = class FileEntry {
return callback(null); return callback(null);
}, },
function startTrans(callback) { function startTrans(callback) {
return fileDb.run('BEGIN;', callback); return fileDb.beginTransaction(callback);
}, },
function storeEntry(callback) { function storeEntry(trans, callback) {
inTransaction = true;
if(isUpdate) { if(isUpdate) {
fileDb.run( trans.run(
`REPLACE INTO file (file_id, area_tag, file_sha256, file_name, storage_tag, desc, desc_long, upload_timestamp) `REPLACE INTO file (file_id, area_tag, file_sha256, file_name, storage_tag, desc, desc_long, upload_timestamp)
VALUES(?, ?, ?, ?, ?, ?, ?, ?);`, VALUES(?, ?, ?, ?, ?, ?, ?, ?);`,
[ self.fileId, self.areaTag, self.fileSha256, self.fileName, self.storageTag, self.desc, self.descLong, getISOTimestampString() ], [ self.fileId, self.areaTag, self.fileSha256, self.fileName, self.storageTag, self.desc, self.descLong, getISOTimestampString() ],
err => { err => {
return callback(err); return callback(err, trans);
} }
); );
} else { } else {
fileDb.run( trans.run(
`REPLACE INTO file (area_tag, file_sha256, file_name, storage_tag, desc, desc_long, upload_timestamp) `REPLACE INTO file (area_tag, file_sha256, file_name, storage_tag, desc, desc_long, upload_timestamp)
VALUES(?, ?, ?, ?, ?, ?, ?);`, VALUES(?, ?, ?, ?, ?, ?, ?);`,
[ self.areaTag, self.fileSha256, self.fileName, self.storageTag, self.desc, self.descLong, getISOTimestampString() ], [ self.areaTag, self.fileSha256, self.fileName, self.storageTag, self.desc, self.descLong, getISOTimestampString() ],
@ -144,34 +141,34 @@ module.exports = class FileEntry {
if(!err) { if(!err) {
self.fileId = this.lastID; self.fileId = this.lastID;
} }
return callback(err); return callback(err, trans);
} }
); );
} }
}, },
function storeMeta(callback) { function storeMeta(trans, callback) {
async.each(Object.keys(self.meta), (n, next) => { async.each(Object.keys(self.meta), (n, next) => {
const v = self.meta[n]; const v = self.meta[n];
return FileEntry.persistMetaValue(self.fileId, n, v, next); return FileEntry.persistMetaValue(self.fileId, n, v, trans, next);
}, },
err => { err => {
return callback(err); return callback(err, trans);
}); });
}, },
function storeHashTags(callback) { function storeHashTags(trans, callback) {
const hashTagsArray = Array.from(self.hashTags); const hashTagsArray = Array.from(self.hashTags);
async.each(hashTagsArray, (hashTag, next) => { async.each(hashTagsArray, (hashTag, next) => {
return FileEntry.persistHashTag(self.fileId, hashTag, next); return FileEntry.persistHashTag(self.fileId, hashTag, trans, next);
}, },
err => { err => {
return callback(err); return callback(err, trans);
}); });
} }
], ],
err => { (err, trans) => {
// :TODO: Log orig err // :TODO: Log orig err
if(inTransaction) { if(trans) {
fileDb.run(err ? 'ROLLBACK;' : 'COMMIT;', err => { trans[err ? 'rollback' : 'commit'](err => {
return cb(err); return cb(err);
}); });
} else { } else {
@ -207,8 +204,13 @@ module.exports = class FileEntry {
); );
} }
static persistMetaValue(fileId, name, value, cb) { static persistMetaValue(fileId, name, value, transOrDb, cb) {
return fileDb.run( if(!_.isFunction(cb) && _.isFunction(transOrDb)) {
cb = transOrDb;
transOrDb = fileDb;
}
return transOrDb.run(
`REPLACE INTO file_meta (file_id, meta_name, meta_value) `REPLACE INTO file_meta (file_id, meta_name, meta_value)
VALUES (?, ?, ?);`, VALUES (?, ?, ?);`,
[ fileId, name, value ], [ fileId, name, value ],
@ -249,15 +251,20 @@ module.exports = class FileEntry {
); );
} }
static persistHashTag(fileId, hashTag, cb) { static persistHashTag(fileId, hashTag, transOrDb, cb) {
fileDb.serialize( () => { if(!_.isFunction(cb) && _.isFunction(transOrDb)) {
cb = transOrDb;
transOrDb = fileDb;
}
transOrDb.serialize( () => {
fileDb.run( fileDb.run(
`INSERT OR IGNORE INTO hash_tag (hash_tag) `INSERT OR IGNORE INTO hash_tag (hash_tag)
VALUES (?);`, VALUES (?);`,
[ hashTag ] [ hashTag ]
); );
fileDb.run( transOrDb.run(
`REPLACE INTO file_hash_tag (hash_tag_id, file_id) `REPLACE INTO file_hash_tag (hash_tag_id, file_id)
VALUES ( VALUES (
(SELECT hash_tag_id (SELECT hash_tag_id

View File

@ -321,8 +321,13 @@ Message.prototype.load = function(options, cb) {
); );
}; };
Message.prototype.persistMetaValue = function(category, name, value, cb) { Message.prototype.persistMetaValue = function(category, name, value, transOrDb, cb) {
const metaStmt = msgDb.prepare( if(!_.isFunction(cb) && _.isFunction(transOrDb)) {
cb = transOrDb;
transOrDb = msgDb;
}
const metaStmt = transOrDb.prepare(
`INSERT INTO message_meta (message_id, meta_category, meta_name, meta_value) `INSERT INTO message_meta (message_id, meta_category, meta_name, meta_value)
VALUES (?, ?, ?, ?);`); VALUES (?, ?, ?, ?);`);
@ -341,18 +346,6 @@ Message.prototype.persistMetaValue = function(category, name, value, cb) {
}); });
}; };
Message.startTransaction = function(cb) {
msgDb.run('BEGIN;', err => {
cb(err);
});
};
Message.endTransaction = function(hadError, cb) {
msgDb.run(hadError ? 'ROLLBACK;' : 'COMMIT;', err => {
cb(err);
});
};
Message.prototype.persist = function(cb) { Message.prototype.persist = function(cb) {
if(!this.isValid()) { if(!this.isValid()) {
@ -361,14 +354,12 @@ Message.prototype.persist = function(cb) {
const self = this; const self = this;
async.series( async.waterfall(
[ [
function beginTransaction(callback) { function beginTransaction(callback) {
Message.startTransaction(err => { return msgDb.beginTransaction(callback);
return callback(err);
});
}, },
function storeMessage(callback) { function storeMessage(trans, callback) {
// generate a UUID for this message if required (general case) // generate a UUID for this message if required (general case)
const msgTimestamp = moment(); const msgTimestamp = moment();
if(!self.uuid) { if(!self.uuid) {
@ -379,7 +370,7 @@ Message.prototype.persist = function(cb) {
self.message); self.message);
} }
msgDb.run( trans.run(
`INSERT INTO message (area_tag, message_uuid, reply_to_message_id, to_user_name, from_user_name, subject, message, modified_timestamp) `INSERT INTO message (area_tag, message_uuid, reply_to_message_id, to_user_name, from_user_name, subject, message, modified_timestamp)
VALUES (?, ?, ?, ?, ?, ?, ?, ?);`, VALUES (?, ?, ?, ?, ?, ?, ?, ?);`,
[ self.areaTag, self.uuid, self.replyToMsgId, self.toUserName, self.fromUserName, self.subject, self.message, getISOTimestampString(msgTimestamp) ], [ self.areaTag, self.uuid, self.replyToMsgId, self.toUserName, self.fromUserName, self.subject, self.message, getISOTimestampString(msgTimestamp) ],
@ -388,13 +379,13 @@ Message.prototype.persist = function(cb) {
self.messageId = this.lastID; self.messageId = this.lastID;
} }
return callback(err); return callback(err, trans);
} }
); );
}, },
function storeMeta(callback) { function storeMeta(trans, callback) {
if(!self.meta) { if(!self.meta) {
return callback(null); return callback(null, trans);
} }
/* /*
Example of self.meta: Example of self.meta:
@ -410,7 +401,7 @@ Message.prototype.persist = function(cb) {
*/ */
async.each(Object.keys(self.meta), (category, nextCat) => { async.each(Object.keys(self.meta), (category, nextCat) => {
async.each(Object.keys(self.meta[category]), (name, nextName) => { async.each(Object.keys(self.meta[category]), (name, nextName) => {
self.persistMetaValue(category, name, self.meta[category][name], err => { self.persistMetaValue(category, name, self.meta[category][name], trans, err => {
nextName(err); nextName(err);
}); });
}, err => { }, err => {
@ -418,18 +409,22 @@ Message.prototype.persist = function(cb) {
}); });
}, err => { }, err => {
callback(err); callback(err, trans);
}); });
}, },
function storeHashTags(callback) { function storeHashTags(trans, callback) {
// :TODO: hash tag support // :TODO: hash tag support
return callback(null); return callback(null, trans);
} }
], ],
err => { (err, trans) => {
Message.endTransaction(err, transErr => { if(trans) {
return cb(err ? err : transErr, self.messageId); trans[err ? 'rollback' : 'commit'](transErr => {
}); return cb(err ? err : transErr, self.messageId);
});
} else {
return cb(err);
}
} }
); );
}; };

View File

@ -189,15 +189,13 @@ module.exports = class User {
// :TODO: set various defaults, e.g. default activation status, etc. // :TODO: set various defaults, e.g. default activation status, etc.
self.properties.account_status = Config.users.requireActivation ? User.AccountStatus.inactive : User.AccountStatus.active; self.properties.account_status = Config.users.requireActivation ? User.AccountStatus.inactive : User.AccountStatus.active;
async.series( async.waterfall(
[ [
function beginTransaction(callback) { function beginTransaction(callback) {
userDb.run('BEGIN;', err => { return userDb.beginTransaction(callback);
return callback(err);
});
}, },
function createUserRec(callback) { function createUserRec(trans, callback) {
userDb.run( trans.run(
`INSERT INTO user (user_name) `INSERT INTO user (user_name)
VALUES (?);`, VALUES (?);`,
[ self.username ], [ self.username ],
@ -213,11 +211,11 @@ module.exports = class User {
self.properties.account_status = User.AccountStatus.active; self.properties.account_status = User.AccountStatus.active;
} }
return callback(null); return callback(null, trans);
} }
); );
}, },
function genAuthCredentials(callback) { function genAuthCredentials(trans, callback) {
User.generatePasswordDerivedKeyAndSalt(password, (err, info) => { User.generatePasswordDerivedKeyAndSalt(password, (err, info) => {
if(err) { if(err) {
return callback(err); return callback(err);
@ -225,85 +223,56 @@ module.exports = class User {
self.properties.pw_pbkdf2_salt = info.salt; self.properties.pw_pbkdf2_salt = info.salt;
self.properties.pw_pbkdf2_dk = info.dk; self.properties.pw_pbkdf2_dk = info.dk;
return callback(null); return callback(null, trans);
}); });
}, },
function setInitialGroupMembership(callback) { function setInitialGroupMembership(trans, callback) {
self.groups = Config.users.defaultGroups; self.groups = Config.users.defaultGroups;
if(User.RootUserID === self.userId) { // root/SysOp? if(User.RootUserID === self.userId) { // root/SysOp?
self.groups.push('sysops'); self.groups.push('sysops');
} }
return callback(null); return callback(null, trans);
}, },
function saveAll(callback) { function saveAll(trans, callback) {
self.persist(false, err => { self.persistWithTransaction(trans, err => {
return callback(err); return callback(err, trans);
}); });
} }
], ],
err => { (err, trans) => {
if(err) { if(trans) {
const originalError = err; trans[err ? 'rollback' : 'commit'](transErr => {
userDb.run('ROLLBACK;', err => { return cb(err ? err : transErr);
assert(!err);
return cb(originalError);
}); });
} else { } else {
userDb.run('COMMIT;', err => { return cb(err);
return cb(err);
});
} }
} }
); );
} }
persist(useTransaction, cb) { persistWithTransaction(trans, cb) {
assert(this.userId > 0); assert(this.userId > 0);
const self = this; const self = this;
async.series( async.series(
[ [
function beginTransaction(callback) {
if(useTransaction) {
userDb.run('BEGIN;', err => {
return callback(err);
});
} else {
return callback(null);
}
},
function saveProps(callback) { function saveProps(callback) {
self.persistProperties(self.properties, err => { self.persistProperties(self.properties, trans, err => {
return callback(err); return callback(err);
}); });
}, },
function saveGroups(callback) { function saveGroups(callback) {
userGroup.addUserToGroups(self.userId, self.groups, err => { userGroup.addUserToGroups(self.userId, self.groups, trans, err => {
return callback(err); return callback(err);
}); });
} }
], ],
err => { err => {
if(err) { return cb(err);
if(useTransaction) {
userDb.run('ROLLBACK;', err => {
return cb(err);
});
} else {
return cb(err);
}
} else {
if(useTransaction) {
userDb.run('COMMIT;', err => {
return cb(err);
});
} else {
return cb(null);
}
}
} }
); );
} }
@ -340,13 +309,18 @@ module.exports = class User {
); );
} }
persistProperties(properties, cb) { persistProperties(properties, transOrDb, cb) {
if(!_.isFunction(cb) && _.isFunction(transOrDb)) {
cb = transOrDb;
transOrDb = userDb;
}
const self = this; const self = this;
// update live props // update live props
_.merge(this.properties, properties); _.merge(this.properties, properties);
const stmt = userDb.prepare( const stmt = transOrDb.prepare(
`REPLACE INTO user_property (user_id, prop_name, prop_value) `REPLACE INTO user_property (user_id, prop_name, prop_value)
VALUES (?, ?, ?);` VALUES (?, ?, ?);`
); );

View File

@ -33,8 +33,13 @@ function getGroupsForUser(userId, cb) {
}); });
} }
function addUserToGroup(userId, groupName, cb) { function addUserToGroup(userId, groupName, transOrDb, cb) {
userDb.run( if(!_.isFunction(cb) && _.isFunction(transOrDb)) {
cb = transOrDb;
transOrDb = userDb;
}
transOrDb.run(
'REPLACE INTO user_group_member (group_name, user_id) ' + 'REPLACE INTO user_group_member (group_name, user_id) ' +
'VALUES(?, ?);', 'VALUES(?, ?);',
[ groupName, userId ], [ groupName, userId ],
@ -44,10 +49,10 @@ function addUserToGroup(userId, groupName, cb) {
); );
} }
function addUserToGroups(userId, groups, cb) { function addUserToGroups(userId, groups, transOrDb, cb) {
async.each(groups, function item(groupName, next) { async.each(groups, function item(groupName, next) {
addUserToGroup(userId, groupName, next); addUserToGroup(userId, groupName, transOrDb, next);
}, function complete(err) { }, function complete(err) {
cb(err); cb(err);
}); });

View File

@ -3,7 +3,10 @@
// ENiGMA½ // ENiGMA½
const MenuModule = require('../core/menu_module.js').MenuModule; const MenuModule = require('../core/menu_module.js').MenuModule;
const getModDatabasePath = require('../core/database.js').getModDatabasePath; const {
getModDatabasePath,
getTransactionDatabase
} = require('../core/database.js').getModDatabasePath;
const ViewController = require('../core/view_controller.js').ViewController; const ViewController = require('../core/view_controller.js').ViewController;
const ansi = require('../core/ansi_term.js'); const ansi = require('../core/ansi_term.js');
const theme = require('../core/theme.js'); const theme = require('../core/theme.js');
@ -392,10 +395,10 @@ exports.getModule = class BBSListModule extends MenuModule {
async.series( async.series(
[ [
function openDatabase(callback) { function openDatabase(callback) {
self.database = new sqlite3.Database( self.database = getTransactionDatabase(new sqlite3.Database(
getModDatabasePath(moduleInfo), getModDatabasePath(moduleInfo),
callback callback
); ));
}, },
function createTables(callback) { function createTables(callback) {
self.database.serialize( () => { self.database.serialize( () => {

View File

@ -3,7 +3,10 @@
// ENiGMA½ // ENiGMA½
const MenuModule = require('../core/menu_module.js').MenuModule; const MenuModule = require('../core/menu_module.js').MenuModule;
const getModDatabasePath = require('../core/database.js').getModDatabasePath; const {
getModDatabasePath,
getTransactionDatabase
} = require('../core/database.js').getModDatabasePath;
const ViewController = require('../core/view_controller.js').ViewController; const ViewController = require('../core/view_controller.js').ViewController;
const theme = require('../core/theme.js'); const theme = require('../core/theme.js');
const ansi = require('../core/ansi_term.js'); const ansi = require('../core/ansi_term.js');
@ -263,12 +266,12 @@ exports.getModule = class OnelinerzModule extends MenuModule {
async.series( async.series(
[ [
function openDatabase(callback) { function openDatabase(callback) {
self.db = new sqlite3.Database( self.db = getTransactionDatabase(new sqlite3.Database(
getModDatabasePath(exports.moduleInfo), getModDatabasePath(exports.moduleInfo),
err => { err => {
return callback(err); return callback(err);
} }
); ));
}, },
function createTables(callback) { function createTables(callback) {
self.db.run( self.db.run(

View File

@ -42,6 +42,7 @@
"ptyw.js": "NuSkooler/ptyw.js", "ptyw.js": "NuSkooler/ptyw.js",
"sanitize-filename": "^1.6.1", "sanitize-filename": "^1.6.1",
"sqlite3": "^3.1.9", "sqlite3": "^3.1.9",
"sqlite3-transactions": "^0.0.5",
"ssh2": "^0.5.5", "ssh2": "^0.5.5",
"temptmp": "^1.0.0", "temptmp": "^1.0.0",
"uuid": "^3.1.0", "uuid": "^3.1.0",