MWS: Use transactions when modifying multiple resources (#7991)

* Use transactions when modifying multiple resources

This prevents partial changes from entering the database, and also
nets a nice speed-up.

* Keep track of transaction depth

…so we could someday potentially leverage SQL implementations that don't
implement nested transactions
This commit is contained in:
Rob Hoelz 2024-02-22 04:48:39 -06:00 committed by GitHub
parent 0d22bf8418
commit 790f431df0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 84 additions and 49 deletions

View File

@ -25,6 +25,7 @@ function SqlTiddlerDatabase(options) {
}
var databasePath = options.databasePath || ":memory:";
this.db = new $tw.sqlite3.Database(databasePath,{verbose: undefined && console.log});
this.transactionDepth = 0;
}
SqlTiddlerDatabase.prototype.close = function() {
@ -452,6 +453,25 @@ SqlTiddlerDatabase.prototype.getRecipeBags = function(recipename) {
return rows.map(value => value.bag_name);
};
/*
Execute the given function in a transaction, committing if successful but rolling back if an error occurs. Returns whatever the given function returns.
Calls to this function can be safely nested, but only the top-most call will actually take place in a transaction.
*/
SqlTiddlerDatabase.prototype.transaction = function(fn) {
try {
const alreadyInTransaction = this.transactionDepth > 0;
this.transactionDepth++;
if(alreadyInTransaction) {
return fn();
} else {
return this.db.transaction(fn)();
}
} finally {
this.transactionDepth--;
}
};
exports.SqlTiddlerDatabase = SqlTiddlerDatabase;
})();

View File

@ -83,25 +83,28 @@ SqlTiddlerStore.prototype.saveEntityStateTiddler = function(tiddler) {
};
SqlTiddlerStore.prototype.updateAdminWiki = function() {
// Update bags
for(const bagInfo of this.listBags()) {
this.saveEntityStateTiddler({
title: "bags/" + bagInfo.bag_name,
"bag-name": bagInfo.bag_name,
text: bagInfo.description
});
}
// Update recipes
for(const recipeInfo of this.listRecipes()) {
this.saveEntityStateTiddler({
title: "recipes/" + recipeInfo.recipe_name,
"recipe-name": recipeInfo.recipe_name,
text: recipeInfo.description,
list: $tw.utils.stringifyList(this.getRecipeBags(recipeInfo.recipe_name).map(bag_name => {
return this.entityStateTiddlerPrefix + "bags/" + bag_name;
}))
});
}
var self = this;
return this.sqlTiddlerDatabase.transaction(function() {
// Update bags
for(const bagInfo of self.listBags()) {
self.saveEntityStateTiddler({
title: "bags/" + bagInfo.bag_name,
"bag-name": bagInfo.bag_name,
text: bagInfo.description
});
}
// Update recipes
for(const recipeInfo of self.listRecipes()) {
self.saveEntityStateTiddler({
title: "recipes/" + recipeInfo.recipe_name,
"recipe-name": recipeInfo.recipe_name,
text: recipeInfo.description,
list: $tw.utils.stringifyList(self.getRecipeBags(recipeInfo.recipe_name).map(bag_name => {
return self.entityStateTiddlerPrefix + "bags/" + bag_name;
}))
});
}
});
};
/*
@ -135,17 +138,20 @@ SqlTiddlerStore.prototype.processCanonicalUriTiddler = function(tiddlerFields,ba
SqlTiddlerStore.prototype.saveTiddlersFromPath = function(tiddler_files_path,bag_name) {
// Clear out the bag
this.deleteAllTiddlersInBag(bag_name);
// Get the tiddlers
var path = require("path");
var tiddlersFromPath = $tw.loadTiddlersFromPath(path.resolve($tw.boot.corePath,$tw.config.editionsPath,tiddler_files_path));
// Save the tiddlers
for(const tiddlersFromFile of tiddlersFromPath) {
for(const tiddler of tiddlersFromFile.tiddlers) {
this.saveBagTiddler(tiddler,bag_name);
var self = this;
this.sqlTiddlerDatabase.transaction(function() {
// Clear out the bag
self.deleteAllTiddlersInBag(bag_name);
// Get the tiddlers
var path = require("path");
var tiddlersFromPath = $tw.loadTiddlersFromPath(path.resolve($tw.boot.corePath,$tw.config.editionsPath,tiddler_files_path));
// Save the tiddlers
for(const tiddlersFromFile of tiddlersFromPath) {
for(const tiddler of tiddlersFromFile.tiddlers) {
self.saveBagTiddler(tiddler,bag_name);
}
}
}
});
};
SqlTiddlerStore.prototype.logTables = function() {
@ -157,17 +163,20 @@ SqlTiddlerStore.prototype.listBags = function() {
};
SqlTiddlerStore.prototype.createBag = function(bagname,description) {
const validationBagName = this.validateItemName(bagname);
if(validationBagName) {
return {message: validationBagName};
}
this.sqlTiddlerDatabase.createBag(bagname,description);
this.saveEntityStateTiddler({
title: "bags/" + bagname,
"bag-name": bagname,
text: description
var self = this;
return this.sqlTiddlerDatabase.transaction(function() {
const validationBagName = self.validateItemName(bagname);
if(validationBagName) {
return {message: validationBagName};
}
self.sqlTiddlerDatabase.createBag(bagname,description);
self.saveEntityStateTiddler({
title: "bags/" + bagname,
"bag-name": bagname,
text: description
});
return null;
});
return null;
};
SqlTiddlerStore.prototype.listRecipes = function() {
@ -191,16 +200,19 @@ SqlTiddlerStore.prototype.createRecipe = function(recipename,bagnames,descriptio
if(bagnames.length === 0) {
return {message: "Recipes must contain at least one bag"};
}
this.sqlTiddlerDatabase.createRecipe(recipename,bagnames,description);
this.saveEntityStateTiddler({
title: "recipes/" + recipename,
"recipe-name": recipename,
text: description,
list: $tw.utils.stringifyList(bagnames.map(bag_name => {
return this.entityStateTiddlerPrefix + "bags/" + bag_name;
}))
var self = this;
return this.sqlTiddlerDatabase.transaction(function() {
self.sqlTiddlerDatabase.createRecipe(recipename,bagnames,description);
self.saveEntityStateTiddler({
title: "recipes/" + recipename,
"recipe-name": recipename,
text: description,
list: $tw.utils.stringifyList(bagnames.map(bag_name => {
return self.entityStateTiddlerPrefix + "bags/" + bag_name;
}))
});
return null;
});
return null;
};
/*
@ -270,7 +282,10 @@ SqlTiddlerStore.prototype.getRecipeTiddlers = function(recipename) {
};
SqlTiddlerStore.prototype.deleteAllTiddlersInBag = function(bagname) {
return this.sqlTiddlerDatabase.deleteAllTiddlersInBag(bagname);
var self = this;
return this.sqlTiddlerDatabase.transaction(function() {
return self.sqlTiddlerDatabase.deleteAllTiddlersInBag(bagname);
});
};
/*