feat: add db query timeout option

This commit is contained in:
Axel Cocat 2022-03-05 15:32:03 +01:00
parent 4b8dc1fa1c
commit ab5dcd3640
6 changed files with 105 additions and 67 deletions

View file

@ -28,5 +28,6 @@
"user": "root", "user": "root",
"password": "root", "password": "root",
"database": "acore_world" "database": "acore_world"
} },
"dbQueryTimeout": 10000
} }

View file

@ -7,7 +7,7 @@ import * as morgan from "morgan";
import { Pool, createPool } from "mysql2/promise"; import { Pool, createPool } from "mysql2/promise";
import { engine as handlebarsEngine } from "express-handlebars"; import { engine as handlebarsEngine } from "express-handlebars";
import { Config } from "./Config"; import { Config, IRealmConfig } from "./Config";
import { DbcManager } from "./data/DbcReader"; import { DbcManager } from "./data/DbcReader";
import { CharacterCustomization } from "./data/CharacterCustomization"; import { CharacterCustomization } from "./data/CharacterCustomization";
import { IndexController } from "./controllers/IndexController"; import { IndexController } from "./controllers/IndexController";
@ -148,14 +148,12 @@ export class Armory {
// Respond with html page // Respond with html page
if (req.accepts("html")) { if (req.accepts("html")) {
res.render("error.html", this.getErrorViewData(404, req)); return res.render("error.html", this.getErrorViewData(404, req));
return;
} }
// Respond with json // Respond with json
if (req.accepts("json")) { if (req.accepts("json")) {
res.json({ error: this.errorNames[404] }); return res.json({ error: this.errorNames[404] });
return;
} }
// Default to plain-text // Default to plain-text
@ -172,6 +170,10 @@ export class Armory {
return this.charsDbs[realm.toLowerCase()]; return this.charsDbs[realm.toLowerCase()];
} }
public getRealm(realm: string): IRealmConfig {
return this.config.realms.find(r => r.name.toLowerCase() === realm.toLowerCase());
}
public gc(): void { public gc(): void {
if (this.config.loadDbcs) { if (this.config.loadDbcs) {
return; return;

View file

@ -32,6 +32,7 @@ export class Config {
public hideGameMasters: boolean; public hideGameMasters: boolean;
public realms: IRealmConfig[]; public realms: IRealmConfig[];
public worldDatabase: IDatabaseConfig; public worldDatabase: IDatabaseConfig;
public dbQueryTimeout: number;
private static checkedMissingField: boolean = false; private static checkedMissingField: boolean = false;

View file

@ -1,5 +1,5 @@
import { Pool } from "mysql2/promise";
import { Query } from "express-serve-static-core"; import { Query } from "express-serve-static-core";
import { Connection, RowDataPacket } from "mysql2/promise";
export interface IResult { export interface IResult {
recordsTotal: number; recordsTotal: number;
@ -30,7 +30,7 @@ export class DataTablesSsp {
public joins: IColumnJoin[] = []; public joins: IColumnJoin[] = [];
public extraDataColumns: string[] = []; public extraDataColumns: string[] = [];
private db: Connection; private db: Pool;
private table: string; private table: string;
private primaryKey: string; private primaryKey: string;
private columnSettings: IColumnSettings[]; private columnSettings: IColumnSettings[];
@ -65,7 +65,7 @@ export class DataTablesSsp {
private orderSql: string = ""; private orderSql: string = "";
private joinSql: string = ""; private joinSql: string = "";
public constructor(query: Query, db: Connection, table: string, primaryKey: string, columnSettings: IColumnSettings[]) { public constructor(query: Query, db: Pool, table: string, primaryKey: string, columnSettings: IColumnSettings[]) {
this.start = parseInt(query.start as string, 10); this.start = parseInt(query.start as string, 10);
this.length = parseInt(query.length as string, 10); this.length = parseInt(query.length as string, 10);
this.draw = parseInt(query.draw as string, 10); this.draw = parseInt(query.draw as string, 10);
@ -198,7 +198,7 @@ export class DataTablesSsp {
`; `;
} }
public async run(): Promise<IResult> { public async run(queryTimeout: number = 10_000): Promise<IResult> {
this.limit() this.limit()
.order() .order()
.join() .join()
@ -206,16 +206,25 @@ export class DataTablesSsp {
const bindings = [...this.filterBindings, ...this.customBindings]; const bindings = [...this.filterBindings, ...this.customBindings];
let [rows, fields] = await this.db.query(this.buildTotalCountSql(), this.customBindings); let [rows, fields] = await this.db.query({
sql: this.buildTotalCountSql(),
values: this.customBindings,
timeout: queryTimeout,
});
const recordsTotal = rows[0].count; const recordsTotal = rows[0].count;
[rows, fields] = await this.db.query(this.buildFilteredCountSql(), bindings); [rows, fields] = await this.db.query({
sql: this.buildFilteredCountSql(),
values: bindings,
timeout: queryTimeout,
});
const recordsFiltered = rows[0].count; const recordsFiltered = rows[0].count;
[rows, fields] = await this.db.query({ [rows, fields] = await this.db.query({
sql: this.sql(), sql: this.sql(),
rowsAsArray: true, rowsAsArray: true,
values: bindings, values: bindings,
timeout: queryTimeout,
}); });
rows = (rows as any[][]).map(row => { rows = (rows as any[][]).map(row => {
for (let i = 0; i < this.columnSettings.length; ++i) { for (let i = 0; i < this.columnSettings.length; ++i) {

View file

@ -116,7 +116,10 @@ export class CharacterController {
} }
this.itemSocketBonuses = {}; this.itemSocketBonuses = {};
let [rows, fields] = await this.armory.worldDb.query("SELECT entry, socketBonus FROM item_template WHERE socketBonus <> 0"); let [rows, fields] = await this.armory.worldDb.query({
sql: "SELECT entry, socketBonus FROM item_template WHERE socketBonus <> 0",
timeout: this.armory.config.dbQueryTimeout,
});
for (const row of rows as RowDataPacket[]) { for (const row of rows as RowDataPacket[]) {
this.itemSocketBonuses[row.entry] = row.socketBonus; this.itemSocketBonuses[row.entry] = row.socketBonus;
} }
@ -151,7 +154,7 @@ export class CharacterController {
const realmName = req.params.realm; const realmName = req.params.realm;
const charName = req.params.name; const charName = req.params.name;
const realm = this.getRealm(realmName); const realm = this.armory.getRealm(realmName);
if (realm === undefined) { if (realm === undefined) {
// Could not find realm // Could not find realm
return next(404); return next(404);
@ -195,7 +198,7 @@ export class CharacterController {
const realmName = req.params.realm; const realmName = req.params.realm;
const charName = req.params.name; const charName = req.params.name;
const realm = this.getRealm(realmName); const realm = this.armory.getRealm(realmName);
if (realm === undefined) { if (realm === undefined) {
// Could not find realm // Could not find realm
return next(404); return next(404);
@ -222,7 +225,7 @@ export class CharacterController {
const realmName = req.params.realm; const realmName = req.params.realm;
const charName = req.params.name; const charName = req.params.name;
const realm = this.getRealm(realmName); const realm = this.armory.getRealm(realmName);
if (realm === undefined) { if (realm === undefined) {
// Could not find realm // Could not find realm
return next(404); return next(404);
@ -244,7 +247,7 @@ export class CharacterController {
const realmName = req.params.realm; const realmName = req.params.realm;
const character = parseInt(req.params.character) || -1; const character = parseInt(req.params.character) || -1;
const realm = this.getRealm(realmName); const realm = this.armory.getRealm(realmName);
if (realm === undefined) { if (realm === undefined) {
// Could not find realm // Could not find realm
return next(404); return next(404);
@ -275,13 +278,10 @@ export class CharacterController {
}; };
} }
private getRealm(realm: string): IRealmConfig {
return this.armory.config.realms.find(r => r.name.toLowerCase() === realm.toLowerCase());
}
private async getCharacterData(realm: IRealmConfig, character: string | number): Promise<ICharacterData> { private async getCharacterData(realm: IRealmConfig, character: string | number): Promise<ICharacterData> {
const where = typeof character === "string" ? "LOWER(`characters`.`name`) = LOWER(?)" : "`characters`.`guid` = ?"; const where = typeof character === "string" ? "LOWER(`characters`.`name`) = LOWER(?)" : "`characters`.`guid` = ?";
const [rows, fields] = await this.armory.getCharactersDb(realm.name).query(` const [rows, fields] = await this.armory.getCharactersDb(realm.name).query({
sql: `
SELECT \`characters\`.\`guid\`, \`characters\`.\`name\`, \`race\`, \`class\`, \`gender\`, \`level\`, \`skin\`, \`face\`, \`hairStyle\`, \`hairColor\`, \`facialStyle\`, \`playerFlags\`, \`online\`, \`guild\`.\`name\` AS \`guild\` SELECT \`characters\`.\`guid\`, \`characters\`.\`name\`, \`race\`, \`class\`, \`gender\`, \`level\`, \`skin\`, \`face\`, \`hairStyle\`, \`hairColor\`, \`facialStyle\`, \`playerFlags\`, \`online\`, \`guild\`.\`name\` AS \`guild\`
FROM \`characters\` FROM \`characters\`
LEFT JOIN \`guild_member\` ON \`guild_member\`.\`guid\` = \`characters\`.\`guid\` LEFT JOIN \`guild_member\` ON \`guild_member\`.\`guid\` = \`characters\`.\`guid\`
@ -290,7 +290,10 @@ export class CharacterController {
WHERE WHERE
${where} ${where}
AND (\`account_access\`.\`id\` IS NULL OR \`account_access\`.\`RealmID\` NOT IN (-1, ${realm.realmId}) OR \`account_access\`.\`gmlevel\` = 0 OR ? = 0) AND (\`account_access\`.\`id\` IS NULL OR \`account_access\`.\`RealmID\` NOT IN (-1, ${realm.realmId}) OR \`account_access\`.\`gmlevel\` = 0 OR ? = 0)
`, [character, this.armory.config.hideGameMasters ? 1 : 0]); `,
values: [character, this.armory.config.hideGameMasters ? 1 : 0],
timeout: this.armory.config.dbQueryTimeout,
});
if ((rows as RowDataPacket[]).length === 0) { if ((rows as RowDataPacket[]).length === 0) {
return null; return null;
@ -299,21 +302,29 @@ export class CharacterController {
} }
private async getEquipmentData(realm: string, charGuid: number): Promise<IEquipmentData[]> { private async getEquipmentData(realm: string, charGuid: number): Promise<IEquipmentData[]> {
const [rows, fields] = await this.armory.getCharactersDb(realm).query(` const [rows, fields] = await this.armory.getCharactersDb(realm).query({
sql: `
SELECT character_inventory.slot, item_instance.itemEntry, item_instance.flags, item_instance.enchantments, item_instance.randomPropertyId SELECT character_inventory.slot, item_instance.itemEntry, item_instance.flags, item_instance.enchantments, item_instance.randomPropertyId
FROM character_inventory FROM character_inventory
JOIN item_instance ON item_instance.guid = character_inventory.item JOIN item_instance ON item_instance.guid = character_inventory.item
WHERE character_inventory.guid = ? AND character_inventory.bag = 0 AND character_inventory.slot IN (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18) WHERE character_inventory.guid = ? AND character_inventory.bag = 0 AND character_inventory.slot IN (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18)
`, [charGuid]); `,
values: [charGuid],
timeout: this.armory.config.dbQueryTimeout,
});
return rows as RowDataPacket[] as IEquipmentData[]; return rows as RowDataPacket[] as IEquipmentData[];
} }
private async getMounts(realm: string, charGuid: number): Promise<IMount[]> { private async getMounts(realm: string, charGuid: number): Promise<IMount[]> {
const [rows, fields] = await this.armory.getCharactersDb(realm).query(` const [rows, fields] = await this.armory.getCharactersDb(realm).query({
sql: `
SELECT spell SELECT spell
FROM character_spell FROM character_spell
WHERE guid = ? AND spell IN (?) WHERE guid = ? AND spell IN (?)
`, [charGuid, this.mountSpells]); `,
values:[charGuid, this.mountSpells],
timeout: this.armory.config.dbQueryTimeout,
});
return (rows as RowDataPacket[]) return (rows as RowDataPacket[])
.map(row => this.mountBySpellId[row.spell]) .map(row => this.mountBySpellId[row.spell])
@ -640,11 +651,15 @@ export class CharacterController {
} }
private async getTalents(realm: string, character: number): Promise<number[][]> { private async getTalents(realm: string, character: number): Promise<number[][]> {
const [rows, fields] = await this.armory.getCharactersDb(realm).query(` const [rows, fields] = await this.armory.getCharactersDb(realm).query({
sql: `
SELECT spell, specMask SELECT spell, specMask
FROM character_talent FROM character_talent
WHERE guid = ? WHERE guid = ?
`, [character]); `,
values: [character],
timeout: this.armory.config.dbQueryTimeout,
});
const talents: number[][] = [[], []]; const talents: number[][] = [[], []];
for (const row of rows as RowDataPacket[]) { for (const row of rows as RowDataPacket[]) {
@ -691,11 +706,15 @@ export class CharacterController {
} }
private async getGlyphs(realm: string, character: number): Promise<any[][]> { private async getGlyphs(realm: string, character: number): Promise<any[][]> {
const [rows, fields] = await this.armory.getCharactersDb(realm).query(` const [rows, fields] = await this.armory.getCharactersDb(realm).query({
sql: `
SELECT guid, talentGroup, glyph1, glyph2, glyph3, glyph4, glyph5, glyph6 SELECT guid, talentGroup, glyph1, glyph2, glyph3, glyph4, glyph5, glyph6
FROM character_glyphs FROM character_glyphs
WHERE guid = ? WHERE guid = ?
`, [character]); `,
values: [character],
timeout: this.armory.config.dbQueryTimeout,
});
const glyphs = [[], []]; const glyphs = [[], []];
for (const row of rows as RowDataPacket[]) { for (const row of rows as RowDataPacket[]) {
@ -731,11 +750,15 @@ export class CharacterController {
.toArray(); .toArray();
const achievements = await Promise.all(promises); const achievements = await Promise.all(promises);
const [rows, fields] = await this.armory.getCharactersDb(realm).query(` const [rows, fields] = await this.armory.getCharactersDb(realm).query({
sql: `
SELECT achievement, date SELECT achievement, date
FROM character_achievement FROM character_achievement
WHERE guid = ? WHERE guid = ?
`, [charData.guid]); `,
values: [charData.guid],
timeout: this.armory.config.dbQueryTimeout,
});
const earned = {}; const earned = {};
for (const row of rows as RowDataPacket[]) { for (const row of rows as RowDataPacket[]) {
earned[row.achievement] = { earned[row.achievement] = {

View file

@ -54,21 +54,23 @@ export class IndexController {
} }
const db = this.armory.getCharactersDb(realm.name); const db = this.armory.getCharactersDb(realm.name);
const conn = await db.getConnection();
if (!(realm.name in this.charsetCache)) { if (!(realm.name in this.charsetCache)) {
let [rows, fields] = await db.query(` let [rows, fields] = await db.query({
sql: `
SELECT CCSA.character_set_name FROM information_schema.\`TABLES\` T, SELECT CCSA.character_set_name FROM information_schema.\`TABLES\` T,
information_schema.\`COLLATION_CHARACTER_SET_APPLICABILITY\` CCSA information_schema.\`COLLATION_CHARACTER_SET_APPLICABILITY\` CCSA
WHERE CCSA.collation_name = T.table_collation WHERE CCSA.collation_name = T.table_collation
AND T.table_schema = "${conn.config.database}" AND T.table_schema = "${(await db.getConnection()).config.database}"
AND T.table_name = "characters" AND T.table_name = "characters"
`); `,
timeout: this.armory.config.dbQueryTimeout,
});
this.charsetCache[realm.name] = rows[0].character_set_name; this.charsetCache[realm.name] = rows[0].character_set_name;
} }
const charSet = this.charsetCache[realm.name]; const charSet = this.charsetCache[realm.name];
let ssp = new DataTablesSsp(req.query, conn, "characters", "guid", [ let ssp = new DataTablesSsp(req.query, db, "characters", "guid", [
{ name: "name", collation: `${charSet}_general_ci` }, { name: "name", collation: `${charSet}_general_ci` },
{ table: "guild", name: "name" }, { table: "guild", name: "name" },
{ name: "level" }, { name: "level" },
@ -89,7 +91,7 @@ export class IndexController {
const result = await ssp const result = await ssp
.where("`deleteInfos_Account` IS NULL") .where("`deleteInfos_Account` IS NULL")
.run(); .run(this.armory.config.dbQueryTimeout);
(result as any).realm = realm.name; (result as any).realm = realm.name;
res.json(result); res.json(result);