/* * (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 config = require('config'); const configCoAuthoring = config.get('services.CoAuthoring'); const co = require('co'); const pubsubService = require('./pubsubRabbitMQ'); const commonDefines = require('./../../Common/sources/commondefines'); const constants = require('./../../Common/sources/constants'); const utils = require('./../../Common/sources/utils'); const storage = require('./../../Common/sources/storage/storage-base'); const queueService = require('./../../Common/sources/taskqueueRabbitMQ'); const operationContext = require('./../../Common/sources/operationContext'); const sqlBase = require('./databaseConnectors/baseConnector'); const docsCoServer = require('./DocsCoServer'); const taskResult = require('./taskresult'); const cfgEditorDataStorage = config.get('services.CoAuthoring.server.editorDataStorage'); const cfgEditorStatStorage = config.get('services.CoAuthoring.server.editorStatStorage'); const editorStatStorage = require('./' + (cfgEditorStatStorage || cfgEditorDataStorage)); const cfgForgottenFiles = config.get('services.CoAuthoring.server.forgottenfiles'); const cfgTableResult = config.get('services.CoAuthoring.sql.tableResult'); const cfgRedisPrefix = configCoAuthoring.get('redis.prefix'); const redisKeyShutdown = cfgRedisPrefix + constants.REDIS_KEY_SHUTDOWN; const WAIT_TIMEOUT = 30000; const LOOP_TIMEOUT = 1000; const EXEC_TIMEOUT = WAIT_TIMEOUT + utils.getConvertionTimeout(undefined); const addSqlParam = sqlBase.addSqlParameter; function updateDoc(ctx, docId, status, callback) { return new Promise((resolve, reject) => { const values = []; const p1 = addSqlParam(status, values); const p2 = addSqlParam(callback, values); const p3 = addSqlParam(ctx.tenant, values); const p4 = addSqlParam(docId, values); const sqlCommand = `UPDATE ${cfgTableResult} SET status=${p1},callback=${p2} WHERE tenant=${p3} AND id=${p4};`; sqlBase.sqlQuery( ctx, sqlCommand, (error, result) => { if (error) { reject(error); } else { resolve(result); } }, undefined, undefined, values ); }); } function shutdown() { return co(function* () { let res = true; const ctx = new operationContext.Context(); try { const editorStat = editorStatStorage.EditorStat ? new editorStatStorage.EditorStat() : new editorStatStorage(); ctx.logger.debug('shutdown start:' + EXEC_TIMEOUT); //redisKeyShutdown is not a simple counter, so it doesn't get decremented by a build that started before Shutdown started //reset redisKeyShutdown just in case the previous run didn't finish yield editorData.cleanupShutdown(redisKeyShutdown); const queue = new queueService(); yield queue.initPromise(true, false, false, false, false, false); const pubsub = new pubsubService(); yield pubsub.initPromise(); //inner ping to update presence ctx.logger.debug('shutdown pubsub shutdown message'); yield pubsub.publish(JSON.stringify({type: commonDefines.c_oPublishType.shutdown, ctx, status: true})); ctx.logger.debug('shutdown start wait pubsub deliver'); yield utils.sleep(LOOP_TIMEOUT); const documentsWithChanges = yield sqlBase.getDocumentsWithChanges(ctx); ctx.logger.debug('shutdown docs with changes count = %s', documentsWithChanges.length); const docsWithEmptyForgotten = []; const docsWithOutOfDateForgotten = []; for (let i = 0; i < documentsWithChanges.length; ++i) { const tenant = documentsWithChanges[i].tenant; const docId = documentsWithChanges[i].id; ctx.setTenant(tenant); const forgotten = yield storage.listObjects(ctx, docId, cfgForgottenFiles); if (forgotten.length > 0) { const selectRes = yield taskResult.select(ctx, docId); if (selectRes.length > 0) { const row = selectRes[0]; if (commonDefines.FileStatus.SaveVersion !== row.status && commonDefines.FileStatus.UpdateVersion !== row.status) { docsWithOutOfDateForgotten.push([tenant, docId]); } } } else { docsWithEmptyForgotten.push([tenant, docId]); } } ctx.initDefault(); ctx.logger.debug('shutdown docs with changes and empty forgotten count = %s', docsWithEmptyForgotten.length); ctx.logger.debug('shutdown docs with changes and out of date forgotten count = %s', docsWithOutOfDateForgotten.length); const docsToConvert = docsWithEmptyForgotten.concat(docsWithOutOfDateForgotten); for (let i = 0; i < docsToConvert.length; ++i) { const tenant = docsToConvert[i][0]; const docId = docsToConvert[i][1]; //todo refactor. group tenants? ctx.setTenant(tenant); yield ctx.initTenantCache(); yield updateDoc(ctx, docId, commonDefines.FileStatus.Ok, ''); yield editorStat.addShutdown(redisKeyShutdown, docId); ctx.logger.debug('shutdown createSaveTimerPromise %s', docId); yield docsCoServer.createSaveTimer(ctx, docId, null, null, null, queue, true); } ctx.initDefault(); //sleep because of bugs in createSaveTimerPromise yield utils.sleep(LOOP_TIMEOUT); const startTime = new Date().getTime(); while (true) { const remainingFiles = yield editorStat.getShutdownCount(redisKeyShutdown); ctx.logger.debug('shutdown remaining files:%d', remainingFiles); const curTime = new Date().getTime() - startTime; if (curTime >= EXEC_TIMEOUT || remainingFiles <= 0) { if (curTime >= EXEC_TIMEOUT) { ctx.logger.debug('shutdown timeout'); } break; } yield utils.sleep(LOOP_TIMEOUT); } let countInForgotten = 0; for (let i = 0; i < docsToConvert.length; ++i) { const tenant = docsToConvert[i][0]; const docId = docsToConvert[i][1]; ctx.setTenant(tenant); const forgotten = yield storage.listObjects(ctx, docId, cfgForgottenFiles); if (forgotten.length > 0) { countInForgotten++; } else { ctx.logger.warn('shutdown missing in forgotten:%s', docId); } } ctx.initDefault(); ctx.logger.debug('shutdown docs placed in forgotten:%d', countInForgotten); ctx.logger.debug('shutdown docs with unknown status:%d', docsToConvert.length - countInForgotten); //todo needs to check queues, because there may be long conversions running before Shutdown //clean up yield editorStat.cleanupShutdown(redisKeyShutdown); yield pubsub.close(); yield queue.close(); ctx.logger.debug('shutdown end'); } catch (e) { res = false; ctx.logger.error('shutdown error:\r\n%s', e.stack); } process.exit(0); return res; }); } exports.shutdown = shutdown; shutdown();