diff --git a/Common/sources/operationContext.js b/Common/sources/operationContext.js index 75d276e7..a87d56ad 100644 --- a/Common/sources/operationContext.js +++ b/Common/sources/operationContext.js @@ -53,6 +53,8 @@ Context.prototype.init = function(tenant, docId, userId, opt_shardKey, opt_WopiS this.config = null; this.secret = null; this.license = null; + //cache + this.taskResultCache = null; }; Context.prototype.initDefault = function() { this.init(tenantManager.getDefautTenant(), constants.DEFAULT_DOC_ID, constants.DEFAULT_USER_ID, undefined); diff --git a/DocService/sources/DocsCoServer.js b/DocService/sources/DocsCoServer.js index 081b42fc..0987dc20 100644 --- a/DocService/sources/DocsCoServer.js +++ b/DocService/sources/DocsCoServer.js @@ -962,9 +962,21 @@ async function applyForceSaveCache(ctx, docId, forceSave, type, opt_userConnecti res.notModified = true; } } else if (!forceSave.started) { - res.startedForceSave = await editorData.checkAndStartForceSave(ctx, docId); - res.ok = !!res.startedForceSave; - return res; + const isTypeToSendFile = commonDefines.c_oAscForceSaveTypes.Command === type || + commonDefines.c_oAscForceSaveTypes.Button === type || + commonDefines.c_oAscForceSaveTypes.Timeout === type || + commonDefines.c_oAscForceSaveTypes.Form === type; + if (isTypeToSendFile) { + const selectRes = await taskResult.selectWithCache(ctx, docId); + if (selectRes.length > 0 && !selectRes[0].callback) { + ctx.logger.debug('applyForceSaveCache empty callback: %s', docId); + res.notModified = true; + return res; + } + } + res.startedForceSave = await editorData.checkAndStartForceSave(ctx, docId); + res.ok = !!res.startedForceSave; + return res; } else if (commonDefines.c_oAscForceSaveTypes.Form === type || commonDefines.c_oAscForceSaveTypes.Internal === type) { res.ok = true; res.inProgress = true; @@ -1407,13 +1419,13 @@ function* cleanDocumentOnExit(ctx, docId, deleteChanges, opt_userIndex) { } yield unlockWopiDoc(ctx, docId, opt_userIndex); } -function* cleanDocumentOnExitNoChanges(ctx, docId, opt_userId, opt_userIndex, opt_forceClose) { +function* cleanDocumentOnExitNoChanges(ctx, docId, opt_userId, opt_userIndex, opt_forceClose, opt_deleteChanges) { var userAction = opt_userId ? new commonDefines.OutputAction(commonDefines.c_oAscUserAction.Out, opt_userId) : null; // We send that everyone is gone and there are no changes (to set the status on the server about the end of editing) yield sendStatusDocument(ctx, docId, c_oAscChangeBase.No, userAction, opt_userIndex, undefined, undefined, undefined, opt_forceClose); //if the user entered the document, the connection was broken, all information was deleted on the server, //when the connection is restored, the userIndex will be saved and it will match the userIndex of the next user - yield* cleanDocumentOnExit(ctx, docId, false, opt_userIndex); + yield* cleanDocumentOnExit(ctx, docId, opt_deleteChanges || false, opt_userIndex); } function createSaveTimer(ctx, docId, opt_userId, opt_userIndex, opt_userLcid, opt_queue, opt_noDelay, opt_initShardKey) { diff --git a/DocService/sources/ai/aiProxyHandler.js b/DocService/sources/ai/aiProxyHandler.js index 001ae83e..c7ba16b3 100644 --- a/DocService/sources/ai/aiProxyHandler.js +++ b/DocService/sources/ai/aiProxyHandler.js @@ -39,6 +39,7 @@ const utils = require('./../../../Common/sources/utils'); const operationContext = require('./../../../Common/sources/operationContext'); const commonDefines = require('./../../../Common/sources/commondefines'); const docsCoServer = require('./../DocsCoServer'); +const statsDClient = require('./../../../Common/sources/statsdclient'); // Import the new aiEngine module const aiEngine = require('./aiEngineWrapper'); @@ -49,6 +50,7 @@ const cfgTokenEnableBrowser = config.get('services.CoAuthoring.token.enable.brow const cfgAiSettings = config.get('aiSettings'); const AI = aiEngine.AI; +const clientStatsD = statsDClient.getClient(); /** * Helper function to set CORS headers if the request origin is allowed * @@ -149,6 +151,8 @@ async function proxyRequest(req, res) { // Create operation context for logging const ctx = new operationContext.Context(); ctx.initFromRequest(req); + const startDate = new Date(); + let success = false; try { ctx.logger.info('Start proxyRequest'); @@ -260,6 +264,7 @@ async function proxyRequest(req, res) { // Use pipeline to pipe the response data to the client await pipeline(result.stream, res); + success = true; } catch (error) { ctx.logger.error(`proxyRequest: AI API request error: %s`, error); @@ -278,6 +283,10 @@ async function proxyRequest(req, res) { }); } } finally { + // Record the time taken for the proxyRequest in StatsD (skip cors requests and errors) + if (clientStatsD && success) { + clientStatsD.timing('coauth.aiProxy', new Date() - startDate); + } ctx.logger.info('End proxyRequest'); } } diff --git a/DocService/sources/canvasservice.js b/DocService/sources/canvasservice.js index 1926a576..a807e819 100644 --- a/DocService/sources/canvasservice.js +++ b/DocService/sources/canvasservice.js @@ -651,7 +651,7 @@ function* commandSendMailMerge(ctx, cmd, outputData) { } } let commandSfctByCmd = co.wrap(function*(ctx, cmd, opt_priority, opt_expiration, opt_queue, opt_initShardKey) { - var selectRes = yield taskResult.select(ctx, cmd.getDocId()); + var selectRes = yield taskResult.selectWithCache(ctx, cmd.getDocId()); var row = selectRes.length > 0 ? selectRes[0] : null; if (!row) { return false; @@ -1807,7 +1807,7 @@ exports.saveFromChanges = function(ctx, docId, statusInfo, optFormat, opt_userId //we do a select, because during the timeout the information could change var selectRes = yield taskResult.select(ctx, docId); var row = selectRes.length > 0 ? selectRes[0] : null; - if (row && row.status == commonDefines.FileStatus.SaveVersion && row.status_info == statusInfo) { + if (row && row.status == commonDefines.FileStatus.SaveVersion && row.status_info == statusInfo && row.callback) { if (null == optFormat) { optFormat = changeFormatByOrigin(ctx, row, constants.AVS_OFFICESTUDIO_FILE_OTHER_OOXML); } @@ -1837,6 +1837,10 @@ exports.saveFromChanges = function(ctx, docId, statusInfo, optFormat, opt_userId yield docsCoServer.editorStat.addShutdown(redisKeyShutdown, docId); } ctx.logger.debug('AddTask saveFromChanges'); + } else if(row && !row.callback) { + ctx.logger.debug('saveFromChanges empty callback: %s', docId); + yield docsCoServer.cleanDocumentOnExitNoChangesPromise(ctx, docId, opt_userId, opt_userIndex, false, true); + //todo restore status } else { if (row) { ctx.logger.debug('saveFromChanges status mismatch: row: %d; %d; expected: %d', row.status, row.status_info, statusInfo); diff --git a/DocService/sources/taskresult.js b/DocService/sources/taskresult.js index 4a5fd6be..96d634be 100644 --- a/DocService/sources/taskresult.js +++ b/DocService/sources/taskresult.js @@ -98,7 +98,20 @@ TaskResultData.prototype.completeDefaults = function() { function upsert(ctx, task) { return sqlBase.upsert(ctx, task); } - +/** + * Return TaskResult rows for docId, caching the last query result on ctx.taskResultCache or fetching from the database. + * @param {object} ctx + * @param {string} docId + * @returns {Promise>} + */ +async function selectWithCache(ctx, docId) { + //todo merge with select and remove on update + if (ctx.taskResultCache && ctx.taskResultCache[0].id === docId) { + return ctx.taskResultCache; + } + ctx.taskResultCache = await select(ctx, docId); + return ctx.taskResultCache; +} function select(ctx, docId) { return new Promise(function(resolve, reject) { let values = []; @@ -316,6 +329,7 @@ function removeIf(ctx, mask) { exports.TaskResultData = TaskResultData; exports.upsert = upsert; exports.select = select; +exports.selectWithCache = selectWithCache; exports.update = update; exports.updateIf = updateIf; exports.restoreInitialPassword = restoreInitialPassword;