mirror of
https://github.com/ONLYOFFICE/server.git
synced 2026-04-07 14:04:35 +08:00
[perf] Speed up doc_changes inserts: bindDefs, MAX_EXECUTE_MANY_ROWS, IGNORE_ROW_ON_DUPKEY_INDEX, cap 2-way concurrency; Fix bug 76277
This commit is contained in:
2
.github/workflows/oracleDatabaseTests.yml
vendored
2
.github/workflows/oracleDatabaseTests.yml
vendored
@ -45,7 +45,7 @@ jobs:
|
|||||||
- name: Creating schema
|
- name: Creating schema
|
||||||
run: |
|
run: |
|
||||||
docker cp ./schema/oracle/createdb.sql oracle:/
|
docker cp ./schema/oracle/createdb.sql oracle:/
|
||||||
docker exec oracle sqlplus -s onlyoffice/onlyoffice@//localhost/onlyoffice @/createdb.sql
|
docker exec oracle sqlplus -s onlyoffice/onlyoffice@//localhost/onlyoffice "@/createdb.sql"
|
||||||
|
|
||||||
- name: Run Jest
|
- name: Run Jest
|
||||||
run: npm run "integration database tests"
|
run: npm run "integration database tests"
|
||||||
|
|||||||
@ -48,7 +48,7 @@
|
|||||||
- multi-integer-range 5.2.0 ([MIT](https://raw.githubusercontent.com/smikitky/node-multi-integer-range/master/LICENSE))
|
- multi-integer-range 5.2.0 ([MIT](https://raw.githubusercontent.com/smikitky/node-multi-integer-range/master/LICENSE))
|
||||||
- multiparty 4.2.3 ([MIT](https://raw.githubusercontent.com/pillarjs/multiparty/master/LICENSE))
|
- multiparty 4.2.3 ([MIT](https://raw.githubusercontent.com/pillarjs/multiparty/master/LICENSE))
|
||||||
- mysql2 3.13.0 ([MIT](https://raw.githubusercontent.com/sidorares/node-mysql2/master/License))
|
- mysql2 3.13.0 ([MIT](https://raw.githubusercontent.com/sidorares/node-mysql2/master/License))
|
||||||
- oracledb 6.8.0 ([(Apache-2.0 OR UPL-1.0)](https://raw.githubusercontent.com/oracle/node-oracledb/main/LICENSE.txt))
|
- oracledb 6.9.0 ([(Apache-2.0 OR UPL-1.0)](https://raw.githubusercontent.com/oracle/node-oracledb/main/LICENSE.txt))
|
||||||
- pg 8.14.0 ([MIT](https://raw.githubusercontent.com/brianc/node-postgres/master/LICENSE))
|
- pg 8.14.0 ([MIT](https://raw.githubusercontent.com/brianc/node-postgres/master/LICENSE))
|
||||||
- redis 4.7.0 ([MIT](https://raw.githubusercontent.com/redis/node-redis/master/LICENSE))
|
- redis 4.7.0 ([MIT](https://raw.githubusercontent.com/redis/node-redis/master/LICENSE))
|
||||||
- retry 0.13.1 ([MIT](https://raw.githubusercontent.com/tim-kos/node-retry/master/License))
|
- retry 0.13.1 ([MIT](https://raw.githubusercontent.com/tim-kos/node-retry/master/License))
|
||||||
|
|||||||
16
DocService/npm-shrinkwrap.json
generated
16
DocService/npm-shrinkwrap.json
generated
@ -1651,6 +1651,11 @@
|
|||||||
"esprima": "^4.0.0"
|
"esprima": "^4.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"json-schema-traverse": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="
|
||||||
|
},
|
||||||
"json5": {
|
"json5": {
|
||||||
"version": "2.2.3",
|
"version": "2.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
|
||||||
@ -2058,9 +2063,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"oracledb": {
|
"oracledb": {
|
||||||
"version": "6.8.0",
|
"version": "6.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/oracledb/-/oracledb-6.8.0.tgz",
|
"resolved": "https://registry.npmjs.org/oracledb/-/oracledb-6.9.0.tgz",
|
||||||
"integrity": "sha512-A4ds4n4xtjPTzk1gwrHWuMeWsEcrScF0GFgVebGrhNpHSAzn6eDwMKcSbakZODKfFcI099iqhmqWsgko8D+7Ww=="
|
"integrity": "sha512-NwPbIGPv6m0GTFSbyy4/5WEjsKMiiJRxztLmYUcfD3oyh/uXdmVmKOwEWr84wFwWJ/0wQrYQh4PjnzvShibRaA=="
|
||||||
},
|
},
|
||||||
"pako": {
|
"pako": {
|
||||||
"version": "1.0.11",
|
"version": "1.0.11",
|
||||||
@ -2336,11 +2341,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
|
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
|
||||||
"integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg=="
|
"integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg=="
|
||||||
},
|
},
|
||||||
"json-schema-traverse": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
|
||||||
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="
|
|
||||||
},
|
|
||||||
"require-from-string": {
|
"require-from-string": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
|
||||||
|
|||||||
@ -32,7 +32,7 @@
|
|||||||
"multi-integer-range": "5.2.0",
|
"multi-integer-range": "5.2.0",
|
||||||
"multiparty": "4.2.3",
|
"multiparty": "4.2.3",
|
||||||
"mysql2": "3.13.0",
|
"mysql2": "3.13.0",
|
||||||
"oracledb": "6.8.0",
|
"oracledb": "6.9.0",
|
||||||
"pg": "8.14.0",
|
"pg": "8.14.0",
|
||||||
"redis": "4.7.0",
|
"redis": "4.7.0",
|
||||||
"retry": "0.13.1",
|
"retry": "0.13.1",
|
||||||
|
|||||||
@ -42,6 +42,11 @@ const cfgTableResult = configSql.get('tableResult');
|
|||||||
const cfgTableChanges = configSql.get('tableChanges');
|
const cfgTableChanges = configSql.get('tableChanges');
|
||||||
const cfgMaxPacketSize = configSql.get('max_allowed_packet');
|
const cfgMaxPacketSize = configSql.get('max_allowed_packet');
|
||||||
|
|
||||||
|
// Limit rows per executeMany call to avoid large internal batches causing server instability
|
||||||
|
// Especially important for 21c 21.3 with NCLOB columns and batched inserts
|
||||||
|
// Higher to reduce round-trips while still bounded by cfgMaxPacketSize
|
||||||
|
const MAX_EXECUTE_MANY_ROWS = 2000;
|
||||||
|
|
||||||
const connectionConfiguration = {
|
const connectionConfiguration = {
|
||||||
user: configSql.get('dbUser'),
|
user: configSql.get('dbUser'),
|
||||||
password: configSql.get('dbPass'),
|
password: configSql.get('dbPass'),
|
||||||
@ -130,7 +135,20 @@ async function executeQuery(ctx, sqlCommand, values = [], noModifyRes = false, n
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function executeBunch(ctx, sqlCommand, values = [], noLog = false) {
|
/**
|
||||||
|
* Execute a batched DML statement using executeMany with optional bind options.
|
||||||
|
* Notes:
|
||||||
|
* - Accepts array-of-arrays for positional binds (recommended for :0..:N placeholders)
|
||||||
|
* - Options can include bindDefs, batchErrors, autoCommit, etc.
|
||||||
|
* - Logs batchErrors summary when present to aid debugging while keeping normal return shape
|
||||||
|
* @param {object} ctx - request context with logger
|
||||||
|
* @param {string} sqlCommand - SQL text with positional bind placeholders
|
||||||
|
* @param {Array<Array<any>>|Array<object>} values - rows to bind
|
||||||
|
* @param {object} [options] - executeMany options (e.g., { bindDefs: [...], batchErrors: true })
|
||||||
|
* @param {boolean} [noLog=false] - disable error logging
|
||||||
|
* @returns {{affectedRows:number}} affected rows count aggregate
|
||||||
|
*/
|
||||||
|
async function executeBunch(ctx, sqlCommand, values = [], options, noLog = false) {
|
||||||
let connection = null;
|
let connection = null;
|
||||||
try {
|
try {
|
||||||
if (!pool) {
|
if (!pool) {
|
||||||
@ -139,17 +157,36 @@ async function executeBunch(ctx, sqlCommand, values = [], noLog = false) {
|
|||||||
|
|
||||||
connection = await pool.getConnection();
|
connection = await pool.getConnection();
|
||||||
|
|
||||||
const result = await connection.executeMany(sqlCommand, values);
|
const result = await connection.executeMany(sqlCommand, values, options);
|
||||||
|
|
||||||
|
// Log batch errors if requested, without changing the public return shape
|
||||||
|
if (options?.batchErrors && Array.isArray(result?.batchErrors) && result.batchErrors.length && !noLog) {
|
||||||
|
const allDup = result.batchErrors.every(e => e?.errorNum === 1); // ORA-00001
|
||||||
|
const logMessage = `executeMany() batchErrors for: ${sqlCommand} -> count=${result.batchErrors.length}${allDup ? ' (duplicates)' : ''}`;
|
||||||
|
if (allDup) {
|
||||||
|
ctx.logger.debug(logMessage);
|
||||||
|
} else {
|
||||||
|
ctx.logger.error(logMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return { affectedRows: result?.rowsAffected ?? 0 };
|
return { affectedRows: result?.rowsAffected ?? 0 };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (!noLog) {
|
if (!noLog) {
|
||||||
ctx.logger.error(`sqlQuery() error while executing query: ${sqlCommand}\n${error.stack}`);
|
ctx.logger.error(`executeBunch() error while executing query: ${sqlCommand}\n${error.stack}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
connection?.close();
|
if (connection) {
|
||||||
|
try {
|
||||||
|
await connection.close();
|
||||||
|
} catch (error) {
|
||||||
|
if (!noLog) {
|
||||||
|
ctx.logger.error(`connection.close() error while executing batched query: ${sqlCommand}\n${error.stack}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -287,7 +324,20 @@ function insertChanges(ctx, tableChanges, startIndex, objChanges, docId, index,
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function insertChangesAsync(ctx, tableChanges, startIndex, objChanges, docId, index, user) {
|
/**
|
||||||
|
* Insert a sequence of change records into the doc_changes table using executeMany.
|
||||||
|
* Removes APPEND_VALUES hint, adds explicit bindDefs, and chunks batches to reduce risk of ORA-03106.
|
||||||
|
* @param {object} ctx - request context
|
||||||
|
* @param {string} tableChanges - table name
|
||||||
|
* @param {number} startIndex - start offset in objChanges
|
||||||
|
* @param {Array<{change:string,time:Date|number|string}>} objChanges - changes payload
|
||||||
|
* @param {string} docId - document id
|
||||||
|
* @param {number} index - starting change_id value
|
||||||
|
* @param {{id:string,idOriginal:string,username:string}} user - user info
|
||||||
|
* @param {boolean} [allowParallel=true] - allow one-level parallel execution for next chunk
|
||||||
|
* @returns {Promise<{affectedRows:number}>}
|
||||||
|
*/
|
||||||
|
async function insertChangesAsync(ctx, tableChanges, startIndex, objChanges, docId, index, user, allowParallel = true) {
|
||||||
if (startIndex === objChanges.length) {
|
if (startIndex === objChanges.length) {
|
||||||
return { affectedRows: 0 };
|
return { affectedRows: 0 };
|
||||||
}
|
}
|
||||||
@ -295,7 +345,7 @@ async function insertChangesAsync(ctx, tableChanges, startIndex, objChanges, doc
|
|||||||
const parametersCount = 8;
|
const parametersCount = 8;
|
||||||
const maxPlaceholderLength = ':99'.length;
|
const maxPlaceholderLength = ':99'.length;
|
||||||
// (parametersCount - 1) - separator symbols length.
|
// (parametersCount - 1) - separator symbols length.
|
||||||
const maxInsertStatementLength = `INSERT /*+ APPEND_VALUES*/INTO ${tableChanges} VALUES()`.length + maxPlaceholderLength * parametersCount + (parametersCount - 1);
|
const maxInsertStatementLength = `INSERT INTO ${tableChanges} VALUES()`.length + maxPlaceholderLength * parametersCount + (parametersCount - 1);
|
||||||
let packetCapacityReached = false;
|
let packetCapacityReached = false;
|
||||||
|
|
||||||
const values = [];
|
const values = [];
|
||||||
@ -308,11 +358,16 @@ async function insertChangesAsync(ctx, tableChanges, startIndex, objChanges, doc
|
|||||||
const lengthUtf8Row = maxInsertStatementLength + indexBytes + timeBytes
|
const lengthUtf8Row = maxInsertStatementLength + indexBytes + timeBytes
|
||||||
+ 4 * (ctx.tenant.length + docId.length + user.id.length + user.idOriginal.length + user.username.length + objChanges[currentIndex].change.length);
|
+ 4 * (ctx.tenant.length + docId.length + user.id.length + user.idOriginal.length + user.username.length + objChanges[currentIndex].change.length);
|
||||||
|
|
||||||
if (lengthUtf8Row + lengthUtf8Current >= cfgMaxPacketSize && currentIndex > startIndex) {
|
// Chunk by packet size and by max rows per batch
|
||||||
|
if ((lengthUtf8Row + lengthUtf8Current >= cfgMaxPacketSize || (values.length >= MAX_EXECUTE_MANY_ROWS)) && currentIndex > startIndex) {
|
||||||
packetCapacityReached = true;
|
packetCapacityReached = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ensure TIMESTAMP bind is a valid JS Date
|
||||||
|
const _t = objChanges[currentIndex].time;
|
||||||
|
const changeTime = _t instanceof Date ? _t : new Date(_t);
|
||||||
|
|
||||||
const parameters = [
|
const parameters = [
|
||||||
ctx.tenant,
|
ctx.tenant,
|
||||||
docId,
|
docId,
|
||||||
@ -321,28 +376,56 @@ async function insertChangesAsync(ctx, tableChanges, startIndex, objChanges, doc
|
|||||||
user.idOriginal,
|
user.idOriginal,
|
||||||
user.username,
|
user.username,
|
||||||
objChanges[currentIndex].change,
|
objChanges[currentIndex].change,
|
||||||
objChanges[currentIndex].time
|
changeTime
|
||||||
];
|
];
|
||||||
|
|
||||||
const rowValues = { ...parameters };
|
// Use positional binding (array-of-arrays) for :0..:7 placeholders
|
||||||
|
values.push(parameters);
|
||||||
values.push(rowValues);
|
|
||||||
lengthUtf8Current += lengthUtf8Row;
|
lengthUtf8Current += lengthUtf8Row;
|
||||||
}
|
}
|
||||||
|
|
||||||
const placeholder = [];
|
const placeholder = [];
|
||||||
for (let i = 0; i < parametersCount; i++) {
|
for (let i = 1; i <= parametersCount; i++) {
|
||||||
placeholder.push(`:${i}`);
|
placeholder.push(`:${i}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const sqlInsert = `INSERT /*+ APPEND_VALUES*/INTO ${tableChanges} VALUES(${placeholder.join(',')})`;
|
// Use IGNORE_ROW_ON_DUPKEY_INDEX to avoid duplicate-key errors on retries and speed up inserts
|
||||||
const result = await executeBunch(ctx, sqlInsert, values);
|
const sqlInsert = `INSERT /*+ IGNORE_ROW_ON_DUPKEY_INDEX(${tableChanges} DOC_CHANGES_UNIQUE) */ INTO ${tableChanges} VALUES(${placeholder.join(',')})`;
|
||||||
|
|
||||||
|
// Explicit bind definitions to avoid thin-driver type inference pitfalls on NVARCHAR2/NCLOB/TIMESTAMP
|
||||||
|
const bindDefs = [
|
||||||
|
{ type: oracledb.DB_TYPE_NVARCHAR, maxSize: 255 }, // tenant NVARCHAR2(255)
|
||||||
|
{ type: oracledb.DB_TYPE_NVARCHAR, maxSize: 255 }, // id NVARCHAR2(255)
|
||||||
|
{ type: oracledb.DB_TYPE_NUMBER }, // change_id NUMBER
|
||||||
|
{ type: oracledb.DB_TYPE_NVARCHAR, maxSize: 255 }, // user_id NVARCHAR2(255)
|
||||||
|
{ type: oracledb.DB_TYPE_NVARCHAR, maxSize: 255 }, // user_id_original NVARCHAR2(255)
|
||||||
|
{ type: oracledb.DB_TYPE_NVARCHAR, maxSize: 255 }, // user_name NVARCHAR2(255)
|
||||||
|
{ type: oracledb.DB_TYPE_NCLOB }, // change_data NCLOB
|
||||||
|
{ type: oracledb.DB_TYPE_TIMESTAMP } // change_date TIMESTAMP
|
||||||
|
];
|
||||||
|
|
||||||
|
// With IGNORE_ROW_ON_DUPKEY_INDEX, duplicates are skipped server-side; disable batchErrors to reduce overhead
|
||||||
|
const executeOptions = { bindDefs, batchErrors: false, autoCommit: true };
|
||||||
|
|
||||||
|
// Execute current batch and optionally process next chunk concurrently if allowed
|
||||||
|
const p1 = executeBunch(ctx, sqlInsert, values, executeOptions);
|
||||||
|
|
||||||
if (packetCapacityReached) {
|
if (packetCapacityReached) {
|
||||||
const recursiveValue = await insertChangesAsync(ctx, tableChanges, currentIndex, objChanges, docId, index, user);
|
if (allowParallel) {
|
||||||
result.affectedRows += recursiveValue.affectedRows;
|
// Start processing the remaining chunks concurrently (single-level parallelism)
|
||||||
|
const p2 = insertChangesAsync(ctx, tableChanges, currentIndex, objChanges, docId, index, user, false);
|
||||||
|
const [r1, r2] = await Promise.all([p1, p2]);
|
||||||
|
r1.affectedRows += r2.affectedRows;
|
||||||
|
return r1;
|
||||||
|
}
|
||||||
|
// Parallelism not allowed: finish this batch, then continue sequentially
|
||||||
|
const r1 = await p1;
|
||||||
|
const r2 = await insertChangesAsync(ctx, tableChanges, currentIndex, objChanges, docId, index, user, false);
|
||||||
|
r1.affectedRows += r2.affectedRows;
|
||||||
|
return r1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const result = await p1;
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user