Skip to content

Commit

Permalink
Better error's and warning when queries took to long
Browse files Browse the repository at this point in the history
  • Loading branch information
ThymonA committed Dec 30, 2020
1 parent 0c10b13 commit 886872c
Show file tree
Hide file tree
Showing 8 changed files with 180 additions and 111 deletions.
68 changes: 43 additions & 25 deletions MySQL.lua
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,13 @@ local rawget = rawget
local next = next
local setmetatable = setmetatable
local GetResourceState = GetResourceState
local GetCurrentResourceName = GetCurrentResourceName
local CreateThread = Citizen.CreateThread
local Wait = Citizen.Wait

local export = exports['fivem-mysql']
local mysql = setmetatable({}, {})
local mysql = setmetatable({
resource_name = 'fivem-mysql',
current_resource_name = GetCurrentResourceName()
}, {})

function mysql:typeof(input)
if (input == nil) then
Expand Down Expand Up @@ -70,45 +72,61 @@ end
function mysql:insert(query, params)
params = params or {}

assert(self:typeof(query) == 'string', 'SQL query must be a string')
assert(self:typeof(params) == 'table', 'Parameters must be a table')
local res, finished = nil, false

params = self:safeParams(params)
self:insertAsync(query, params, function(result)
res = result
finished = true
end)

repeat Citizen.Wait(0) until finished == true

return export:insert(query, params)
return res
end

function mysql:fetchAll(query, params)
params = params or {}

assert(self:typeof(query) == 'string', 'SQL query must be a string')
assert(self:typeof(params) == 'table', 'Parameters must be a table')
local res, finished = nil, false

params = self:safeParams(params)
self:fetchAllAsync(query, params, function(result)
res = result
finished = true
end)

repeat Citizen.Wait(0) until finished == true

return export:fetchAll(query, params)
return res
end

function mysql:fetchScalar(query, params)
params = params or {}

assert(self:typeof(query) == 'string', 'SQL query must be a string')
assert(self:typeof(params) == 'table', 'Parameters must be a table')
local res, finished = nil, false

params = self:safeParams(params)
self:fetchScalarAsync(query, params, function(result)
res = result
finished = true
end)

repeat Citizen.Wait(0) until finished == true

return export:fetchScalar(query, params)
return res
end

function mysql:fetchFirst(query, params)
params = params or {}

assert(self:typeof(query) == 'string', 'SQL query must be a string')
assert(self:typeof(params) == 'table', 'Parameters must be a table')
local res, finished = nil, false

params = self:safeParams(params)
self:fetchFirstAsync(query, params, function(result)
res = result
finished = true
end)

repeat Citizen.Wait(0) until finished == true

return export:fetchFirst(query, params)
return res
end

function mysql:insertAsync(query, params, callback)
Expand All @@ -120,7 +138,7 @@ function mysql:insertAsync(query, params, callback)

params = self:safeParams(params)

export:insertAsync(query, params, callback)
exports[self.resource_name]:insertAsync(query, params, callback, self.current_resource_name)
end

function mysql:fetchAllAsync(query, params, callback)
Expand All @@ -132,7 +150,7 @@ function mysql:fetchAllAsync(query, params, callback)

params = self:safeParams(params)

export:fetchAllAsync(query, params, callback)
exports[self.resource_name]:fetchAllAsync(query, params, callback, self.current_resource_name)
end

function mysql:fetchScalarAsync(query, params, callback)
Expand All @@ -144,7 +162,7 @@ function mysql:fetchScalarAsync(query, params, callback)

params = self:safeParams(params)

export:fetchScalarAsync(query, params, callback)
exports[self.resource_name]:fetchScalarAsync(query, params, callback, self.current_resource_name)
end

function mysql:fetchFirstAsync(query, params, callback)
Expand All @@ -156,7 +174,7 @@ function mysql:fetchFirstAsync(query, params, callback)

params = self:safeParams(params)

export:fetchFirstAsync(query, params, callback)
exports[self.resource_name]:fetchFirstAsync(query, params, callback, self.current_resource_name)
end

function mysql:ready(callback)
Expand All @@ -165,8 +183,8 @@ function mysql:ready(callback)

assert(self:typeof(cb) == 'function', 'Callback must be a function')

while GetResourceState('fivem-mysql') ~= 'started' do Wait(0) end
while not export:isReady() do Wait(0) end
while GetResourceState(self.resource_name) ~= 'started' do Wait(0) end
while not exports[self.resource_name]:isReady() do Wait(0) end

cb()
end)
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"pg-connection-string": "^2.4.0",
"qs": "^6.9.4",
"sqlstring": "^2.3.2",
"tracer": "^1.1.4",
"ts-node": "^9.1.1"
},
"devDependencies": {
Expand Down
2 changes: 1 addition & 1 deletion source/fivem/callback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
import { OkPacket, RowDataPacket, ResultSetHeader } from 'mysql2';

declare interface CFXCallback {
(result: RowDataPacket[][] | RowDataPacket[] | OkPacket | OkPacket[] | ResultSetHeader | boolean | number | any | any[]): void;
(result: RowDataPacket[][] | RowDataPacket[] | OkPacket | OkPacket[] | ResultSetHeader | boolean | number | any | any[], query?: string): void;
};

export {
Expand Down
14 changes: 12 additions & 2 deletions source/mysql/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
*/

import { Tracer } from 'tracer';
import { escape } from 'sqlstring';

function fixQuery(query: string) {
Expand All @@ -53,7 +54,16 @@ function fixParameters(params: { [key: string]: any }, stringifyObjects?: boolea
return result;
}

export default {
function warnIfNeeded(time: [number, number], logger: Tracer.Logger, sql: string, resource: string, interval: number) {
const queryTime = time[0] * 1e3 + time[1] * 1e-6;

if (interval <= 0 || interval > queryTime) { return; }

logger.warn(`Resource '${resource}' executed an query that took ${queryTime.toFixed()}ms to execute\n> ^4Query: ^7${sql}\n> ^4Execution time: ^7${queryTime.toFixed()}ms`);
}

export {
fixQuery,
fixParameters
fixParameters,
warnIfNeeded
}
42 changes: 23 additions & 19 deletions source/mysql/mysql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@
*/

import { CFXCallback, OkPacket, RowDataPacket, ResultSetHeader } from '../fivem/callback';
import MySQLHelper from './helpers';
import { fixParameters, fixQuery } from './helpers';
import { Tracer } from 'tracer';
import { Pool, PoolOptions, ConnectionOptions, QueryError, createPool } from 'mysql2';

declare type keyValue = { [key: string]: any };
Expand All @@ -37,8 +38,10 @@ class MySQLServer {
ready: boolean = false;
options: PoolOptions;
pool: Pool;
logger: Tracer.Logger;

constructor(connectionOptions: ConnectionOptions, readyCallback?: Function) {
constructor(connectionOptions: ConnectionOptions, logger: Tracer.Logger, readyCallback?: Function) {
this.logger = logger;
this.options = {
...connectionOptions,
...{
Expand All @@ -58,45 +61,46 @@ class MySQLServer {
}
}

beginTransaction(callback: CFXCallback) {
beginTransaction(callback: CFXCallback, resource: string) {
return this.pool.beginTransaction((err) => {
if (err) {
callback(false);
return;
}

callback([]);
err ? this.errorCallback(err, callback, resource) : callback([]);
});
}

commit(callback: CFXCallback) {
commit(callback: CFXCallback, resource: string) {
return this.pool.commit((err) => {
err ? callback(false) : callback([]);
err ? this.errorCallback(err, callback, resource) : callback([]);
});
}

rollback(callback: CFXCallback) {
return this.pool.rollback(() => callback([]));
}

end() { this.pool?.end(); }
end(resource: string) {
this.pool?.end((err) => {
if (err) {
this.logger.error(`Resource '${resource}' throw an SQL error\n> ^1Message: ^7${err.message}`);
}
});
}

execute(query: string, parameters: keyValue, callback: CFXCallback) {
execute(query: string, parameters: keyValue, callback: CFXCallback, resource: string) {
const config = this.pool?.config;

parameters = MySQLHelper.fixParameters(parameters, config?.stringifyObjects, config?.timezone);
query = MySQLHelper.fixQuery(query);
parameters = fixParameters(parameters, config?.stringifyObjects, config?.timezone);
query = fixQuery(query);

const sql = this.pool?.format(query, parameters);

return this.pool?.query(sql, parameters, (err, result) => {
err ? this.errorCallback(err, callback) : callback(result);
err ? this.errorCallback(err, callback, resource, query) : callback(result, query);
});
}

errorCallback(error: QueryError, callback: CFXCallback, rollback?: boolean) {
rollback ? this.pool.rollback(() => callback([])) : callback([]);
console.error(error.message);
errorCallback(error: QueryError, callback: CFXCallback, resource: string, query?: string) {
this.logger.error(`Resource '${resource}' throw an SQL error\n> ^1Message: ^7${error.message}`);
callback([], query);
}

isReady() { return this.ready; }
Expand Down
99 changes: 35 additions & 64 deletions source/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@
┃ along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

import { console } from 'tracer';
import { warnIfNeeded } from './mysql/helpers';
import { GetLoggerConfig, GetSlowQueryWarning } from './tracer';
import { MySQLServer, CFXCallback, OkPacket, ConnectionString, keyValue } from './mysql';

let isReady = false;
Expand All @@ -34,83 +38,50 @@ global.exports('isReady', (): boolean => { return isReady; });

const rawConnectionString = GetConvar('mysql_connection_string', 'mysql://root@localhost/fivem');
const connectionString = ConnectionString(rawConnectionString);
const server = new MySQLServer(connectionString, () => { isReady = true; });
const wait = (ms: number) => new Promise(res => setTimeout(res, ms));

global.exports('insertAsync', (query: string, parameters?: keyValue, callback?: CFXCallback): void => {
server.execute(query, parameters, (result) => {
callback((<OkPacket>result)?.insertId ?? 0);
});
});

global.exports('fetchAllAsync', (query: string, parameters?: keyValue, callback?: CFXCallback): void => {
server.execute(query, parameters, callback);
});
const slowQueryWarning = GetSlowQueryWarning();
const logger = console(GetLoggerConfig());
const server = new MySQLServer(connectionString, logger, () => { isReady = true; });

global.exports('fetchScalarAsync', (query: string, parameters?: keyValue, callback?: CFXCallback): void => {
server.execute(query, parameters, (result) => {
callback((result && result[0]) ? (Object.values(result[0])[0] ?? null) : null);
});
});
global.exports('insertAsync', (query: string, parameters?: keyValue, callback?: CFXCallback, resource?: string): void => {
const startTime = process.hrtime();

global.exports('fetchFirstAsync', (query: string, parameters?: keyValue, callback?: CFXCallback): void => {
server.execute(query, parameters, (result) => {
callback((result && result[0]) ? result[0] ?? [] : []);
});
});
resource = resource ?? GetInvokingResource();

global.exports('insert', async (query: string, parameters?: keyValue): Promise<number> => {
let res: number = null;
let done: boolean = false;

server.execute(query, parameters, (result) => {
res = <number>(<OkPacket>result?.insertId ?? 0);
done = true;
});

do { await wait(0); } while (done == false);

return res;
server.execute(query, parameters, (result, sql) => {
warnIfNeeded(process.hrtime(startTime), logger, sql, resource, slowQueryWarning);
callback((<OkPacket>result)?.insertId ?? 0);
}, resource);
});

global.exports('fetchAll', async (query: string, parameters?: keyValue): Promise<any> => {
let res: any = null;
let done: boolean = false;

server.execute(query, parameters, (result) => {
res = result;
done = true;
});
global.exports('fetchAllAsync', (query: string, parameters?: keyValue, callback?: CFXCallback, resource?: string): void => {
const startTime = process.hrtime();

do { await wait(0); } while (done == false);
resource = resource ?? GetInvokingResource();

return res;
server.execute(query, parameters, (result, sql) => {
warnIfNeeded(process.hrtime(startTime), logger, sql, resource, slowQueryWarning);
callback(result);
}, resource);
});

global.exports('fetchScalar', async (query: string, parameters?: keyValue): Promise<any> => {
let res: any = null;
let done: boolean = false;
global.exports('fetchScalarAsync', (query: string, parameters?: keyValue, callback?: CFXCallback, resource?: string): void => {
const startTime = process.hrtime();

server.execute(query, parameters, (result) => {
res = (result && result[0]) ? (Object.values(result[0])[0] ?? null) : null;
done = true;
});
resource = resource ?? GetInvokingResource();

do { await wait(0); } while (done == false);

return res;
server.execute(query, parameters, (result, sql) => {
warnIfNeeded(process.hrtime(startTime), logger, sql, resource, slowQueryWarning);
callback((result && result[0]) ? (Object.values(result[0])[0] ?? null) : null);
}, resource);
});

global.exports('fetchFirst', async (query: string, parameters?: keyValue): Promise<any> => {
let res: any = null;
let done: boolean = false;
global.exports('fetchFirstAsync', (query: string, parameters?: keyValue, callback?: CFXCallback, resource?: string): void => {
const startTime = process.hrtime();

server.execute(query, parameters, (result) => {
res = (result && result[0]) ? result[0] ?? [] : [];
done = true;
});
resource = resource ?? GetInvokingResource();

do { await wait(0); } while (done == false);

return res;
server.execute(query, parameters, (result, sql) => {
warnIfNeeded(process.hrtime(startTime), logger, sql, resource, slowQueryWarning);
callback((result && result[0]) ? result[0] ?? [] : []);
}, resource);
});
Loading

0 comments on commit 886872c

Please sign in to comment.