Merge pull request 'feature/runtime-config-3' (#46) from feature/runtime-config-3 into develop

Reviewed-on: https://git.onlyoffice.com/ONLYOFFICE/server/pulls/46
This commit is contained in:
Sergey Konovalov
2025-07-17 09:21:10 +00:00
4 changed files with 164 additions and 20 deletions

View File

@ -0,0 +1,98 @@
/*
* (c) Copyright Ascensio System SIA 2010-2024
*
* This program is a free software product. You can redistribute it and/or
* modify it under the terms of the GNU Affero General Public License (AGPL)
* version 3 as published by the Free Software Foundation. In accordance with
* Section 7(a) of the GNU AGPL its Section 15 shall be amended to the effect
* that Ascensio System SIA expressly excludes the warranty of non-infringement
* of any third-party rights.
*
* This program is distributed WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. For
* details, see the GNU AGPL at: http://www.gnu.org/licenses/agpl-3.0.html
*
* You can contact Ascensio System SIA at 20A-6 Ernesta Birznieka-Upish
* street, Riga, Latvia, EU, LV-1050.
*
* The interactive user interfaces in modified source and object code versions
* of the Program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU AGPL version 3.
*
* Pursuant to Section 7(b) of the License you must retain the original Product
* logo when distributing the program. Pursuant to Section 7(e) we decline to
* grant you any rights under trademark law for use of our trademarks.
*
* All the Product's GUI elements, including illustrations and icon sets, as
* well as technical writing content are licensed under the terms of the
* Creative Commons Attribution-ShareAlike 4.0 International. See the License
* terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
*
*/
'use strict';
const fs = require('fs');
/**
* Reloads an NPM module by clearing it from require.cache and re-requiring it
* @param {string} moduleName - Name of the module to reload
* @returns {Object} The freshly loaded module
*/
function reloadNpmModule(moduleName) {
try {
const moduleId = require.resolve(moduleName);
delete require.cache[moduleId];
return require(moduleName);
} catch (error) {
console.error(`Failed to reload module ${moduleName}:`, error.message);
throw error;
}
}
/**
* Requires config module with runtime configuration support
* @param {Object} opt_additionalConfig - Additional configuration to merge
* @returns {Object} config module
*/
function requireConfigWithRuntime(opt_additionalConfig) {
let config = require('config');
try {
const configFilePath = config.get('runtimeConfig.filePath');
if (configFilePath) {
// Update NODE_CONFIG with runtime configuration
if (configFilePath) {
const configData = fs.readFileSync(configFilePath, 'utf8');
let curNodeConfig;
if (process.env['NODE_CONFIG']) {
curNodeConfig = JSON.parse(process.env['NODE_CONFIG']);
} else {
curNodeConfig = {};
}
const fileConfig = JSON.parse(configData);
// Merge configurations: NODE_CONFIG -> runtime -> additional
curNodeConfig = config.util.extendDeep(curNodeConfig, fileConfig);
if (opt_additionalConfig) {
curNodeConfig = config.util.extendDeep(curNodeConfig, opt_additionalConfig);
}
process.env['NODE_CONFIG'] = JSON.stringify(curNodeConfig);
}
config = reloadNpmModule('config');
}
} catch (err) {
if (err.code !== 'ENOENT') {
console.error('Failed to load runtime config: %s', err.stack);
}
}
return config;
}
module.exports = {
reloadNpmModule,
requireConfigWithRuntime
};

View File

@ -39,10 +39,6 @@ var ms = require('ms');
var taskResult = require('./taskresult');
var docsCoServer = require('./DocsCoServer');
var canvasService = require('./canvasservice');
var storage = require('./../../Common/sources/storage/storage-base');
var utils = require('./../../Common/sources/utils');
var logger = require('./../../Common/sources/logger');
var constants = require('./../../Common/sources/constants');
var commondefines = require('./../../Common/sources/commondefines');
var queueService = require('./../../Common/sources/taskqueueRabbitMQ');
var operationContext = require('./../../Common/sources/operationContext');
@ -53,7 +49,7 @@ var cfgExpFilesCron = config.get('services.CoAuthoring.expire.filesCron');
var cfgExpDocumentsCron = config.get('services.CoAuthoring.expire.documentsCron');
var cfgExpFiles = config.get('services.CoAuthoring.expire.files');
var cfgExpFilesRemovedAtOnce = config.get('services.CoAuthoring.expire.filesremovedatonce');
var cfgForceSaveStep = ms(config.get('services.CoAuthoring.autoAssembly.step'));
var cfgForceSaveStep = config.get('services.CoAuthoring.autoAssembly.step');
function getCronStep(cronTime){
let cronJob = new cron.CronJob(cronTime, function(){});
@ -66,21 +62,41 @@ let expDocumentsStep = getCronStep(cfgExpDocumentsCron);
var checkFileExpire = function(expireSeconds) {
return co(function* () {
let ctx = new operationContext.Context();
let currentExpFilesStep = expFilesStep;
try {
ctx.logger.info('checkFileExpire start');
yield ctx.initTenantCache();
const expFiles = ctx.getCfg('services.CoAuthoring.expire.files', cfgExpFiles);
const expFilesRemovedAtOnce = ctx.getCfg('services.CoAuthoring.expire.filesremovedatonce', cfgExpFilesRemovedAtOnce);
const currentFilesCron = ctx.getCfg('services.CoAuthoring.expire.filesCron', cfgExpFilesCron);
currentExpFilesStep = getCronStep(currentFilesCron);
let removedCount = 0;
var expired;
var currentRemovedCount;
do {
currentRemovedCount = 0;
expired = yield taskResult.getExpired(ctx, cfgExpFilesRemovedAtOnce, expireSeconds ?? cfgExpFiles);
expired = yield taskResult.getExpired(ctx, expFilesRemovedAtOnce, expireSeconds ?? expFiles);
expired.sort((a, b) => a.tenant.localeCompare(b.tenant));
let currentTenant = null;
for (var i = 0; i < expired.length; ++i) {
let tenant = expired[i].tenant;
let docId = expired[i].id;
let shardKey = sqlBase.DocumentAdditional.prototype.getShardKey(expired[i].additional);
let wopiSrc = sqlBase.DocumentAdditional.prototype.getWopiSrc(expired[i].additional);
ctx.init(tenant, docId, ctx.userId, shardKey, wopiSrc);
yield ctx.initTenantCache();
if (currentTenant !== tenant) {
ctx.init(tenant, docId, ctx.userId, shardKey, wopiSrc);
yield ctx.initTenantCache();
currentTenant = tenant;
} else {
ctx.setDocId(docId);
ctx.setShardKey(shardKey);
ctx.setWopiSrc(wopiSrc);
}
//todo tenant
//check that no one is in the document
let editorsCount = yield docsCoServer.getEditorsCountPromise(ctx, docId);
@ -99,7 +115,7 @@ var checkFileExpire = function(expireSeconds) {
} catch (e) {
ctx.logger.error('checkFileExpire error: %s', e.stack);
} finally {
setTimeout(checkFileExpire, expFilesStep);
setTimeout(checkFileExpire, currentExpFilesStep);
}
});
};
@ -108,21 +124,34 @@ var checkDocumentExpire = function() {
var queue = null;
var removedCount = 0;
var startSaveCount = 0;
let currentExpDocumentsStep = expDocumentsStep;
let ctx = new operationContext.Context();
try {
ctx.logger.info('checkDocumentExpire start');
yield ctx.initTenantCache();
const currentDocumentsCron = ctx.getCfg('services.CoAuthoring.expire.documentsCron', cfgExpDocumentsCron);
currentExpDocumentsStep = getCronStep(currentDocumentsCron);
var now = (new Date()).getTime();
let expiredKeys = yield docsCoServer.editorData.getDocumentPresenceExpired(now);
if (expiredKeys.length > 0) {
queue = new queueService();
yield queue.initPromise(true, false, false, false, false, false);
expiredKeys.sort((a, b) => a[0].localeCompare(b[0]));
let currentTenant = null;
for (var i = 0; i < expiredKeys.length; ++i) {
let tenant = expiredKeys[i][0];
let docId = expiredKeys[i][1];
if (docId) {
ctx.init(tenant, docId, ctx.userId);
yield ctx.initTenantCache();
if (currentTenant !== tenant) {
ctx.init(tenant, docId, ctx.userId);
yield ctx.initTenantCache();
currentTenant = tenant;
} else {
ctx.setDocId(docId);
}
var hasChanges = yield docsCoServer.hasChanges(ctx, docId);
if (hasChanges) {
//todo opt_initShardKey from getDocumentPresenceExpired data or from db
@ -147,7 +176,8 @@ var checkDocumentExpire = function() {
} catch (e) {
ctx.logger.error('checkDocumentExpire error: %s', e.stack);
}
setTimeout(checkDocumentExpire, expDocumentsStep);
setTimeout(checkDocumentExpire, currentExpDocumentsStep);
}
});
};
@ -155,9 +185,12 @@ let forceSaveTimeout = function() {
return co(function* () {
let queue = null;
let pubsub = null;
let currentForceSaveStep = cfgForceSaveStep;
let ctx = new operationContext.Context();
try {
ctx.logger.info('forceSaveTimeout start');
yield ctx.initTenantCache();
currentForceSaveStep = ctx.getCfg('services.CoAuthoring.autoAssembly.step', cfgForceSaveStep);
let now = (new Date()).getTime();
let expiredKeys = yield docsCoServer.editorData.getForceSaveTimer(now);
if (expiredKeys.length > 0) {
@ -167,14 +200,24 @@ let forceSaveTimeout = function() {
pubsub = new pubsubService();
yield pubsub.initPromise();
expiredKeys.sort((a, b) => a[0].localeCompare(b[0]));
let actions = [];
let currentTenant = null;
for (let i = 0; i < expiredKeys.length; ++i) {
let tenant = expiredKeys[i][0];
let docId = expiredKeys[i][1];
if (docId) {
ctx.init(tenant, docId, ctx.userId);
yield ctx.initTenantCache();
//todo opt_initShardKey from ForceSave data or from db
if (currentTenant !== tenant) {
ctx.init(tenant, docId, ctx.userId);
yield ctx.initTenantCache();
//todo opt_initShardKey from ForceSave data or from db
currentTenant = tenant;
} else {
ctx.setDocId(docId);
}
actions.push(docsCoServer.startForceSave(ctx, docId, commondefines.c_oAscForceSaveTypes.Timeout,
undefined, undefined, undefined, undefined,
undefined, undefined, undefined, undefined, queue, pubsub, undefined, true));
@ -196,17 +239,18 @@ let forceSaveTimeout = function() {
yield pubsub.close();
}
} catch (e) {
ctx.logger.error('checkDocumentExpire error: %s', e.stack);
ctx.logger.error('forceSaveTimeout cleanup error: %s', e.stack);
}
setTimeout(forceSaveTimeout, cfgForceSaveStep);
setTimeout(forceSaveTimeout, ms(currentForceSaveStep));
}
});
};
exports.startGC = function() {
//runtime config is read on start
setTimeout(checkDocumentExpire, expDocumentsStep);
setTimeout(checkFileExpire, expFilesStep);
setTimeout(forceSaveTimeout, cfgForceSaveStep);
setTimeout(forceSaveTimeout, ms(cfgForceSaveStep));
};
exports.getCronStep = getCronStep;
exports.checkFileExpire = checkFileExpire;

View File

@ -32,7 +32,8 @@
'use strict';
const config = require('config');
const moduleReloader = require('./../../Common/sources/moduleReloader');
const config = moduleReloader.requireConfigWithRuntime();
//process.env.NODE_ENV = config.get('services.CoAuthoring.server.mode');
const logger = require('./../../Common/sources/logger');
const co = require('co');

View File

@ -35,12 +35,13 @@
const cluster = require('cluster');
const logger = require('./../../Common/sources/logger');
const operationContext = require('./../../Common/sources/operationContext');
const moduleReloader = require('./../../Common/sources/moduleReloader');
const config = moduleReloader.requireConfigWithRuntime();
if (cluster.isMaster) {
const fs = require('fs');
const co = require('co');
const os = require('os');
const config = require('config');
const license = require('./../../Common/sources/license');
const cfgLicenseFile = config.get('license.license_file');