[bug] Add internal/cluster/pre-stop handler to graceful shutdown in k8s cluster

This commit is contained in:
Sergey Konovalov
2025-11-12 20:08:39 +03:00
parent bde82221dd
commit 8fb8d22ffb
3 changed files with 42 additions and 4 deletions

View File

@ -175,6 +175,7 @@ const lockDocumentsTimerId = {}; //to drop connection that can't unlockDocument
let pubsub;
let queue;
let shutdownFlag = false;
let preStopFlag = false;
const expDocumentsStep = gc.getCronStep(cfgExpDocumentsCron);
const MIN_SAVE_EXPIRATION = 60000;
@ -191,6 +192,10 @@ function getIsShutdown() {
return shutdownFlag;
}
function getIsPreStop() {
return preStopFlag;
}
function getEditorConfig(ctx) {
let tenEditor = ctx.getCfg('services.CoAuthoring.editor', cfgEditor);
tenEditor = JSON.parse(JSON.stringify(tenEditor));
@ -1534,7 +1539,7 @@ function* cleanDocumentOnExit(ctx, docId, deleteChanges, opt_userIndex) {
//clean redis (redisKeyPresenceSet and redisKeyPresenceHash removed with last element)
yield editorData.cleanDocumentOnExit(ctx, docId);
if (editorStatProxy?.deleteKey) {
if (preStopFlag && editorStatProxy?.deleteKey) {
yield editorStatProxy.deleteKey(docId);
}
//remove changes
@ -1772,6 +1777,7 @@ exports.hasEditors = hasEditors;
exports.getEditorsCountPromise = co.wrap(getEditorsCount);
exports.getCallback = getCallback;
exports.getIsShutdown = getIsShutdown;
exports.getIsPreStop = getIsPreStop;
exports.hasChanges = hasChanges;
exports.cleanDocumentOnExitPromise = co.wrap(cleanDocumentOnExit);
exports.cleanDocumentOnExitNoChangesPromise = co.wrap(cleanDocumentOnExitNoChanges);
@ -2178,7 +2184,7 @@ exports.install = function (server, app, callbackFunction) {
);
}
} else {
if (hvals?.length <= 0 && editorStatProxy?.deleteKey) {
if (preStopFlag && hvals?.length <= 0 && editorStatProxy?.deleteKey) {
yield editorStatProxy.deleteKey(docId);
}
}
@ -4214,9 +4220,12 @@ exports.install = function (server, app, callbackFunction) {
ctx.initFromConnection(conn);
//todo group by tenant
yield ctx.initTenantCache();
const tenExpSessionIdle = ms(ctx.getCfg('services.CoAuthoring.expire.sessionidle', cfgExpSessionIdle));
let tenExpSessionIdle = ms(ctx.getCfg('services.CoAuthoring.expire.sessionidle', cfgExpSessionIdle)) || 0;
const tenExpSessionAbsolute = ms(ctx.getCfg('services.CoAuthoring.expire.sessionabsolute', cfgExpSessionAbsolute));
const tenExpSessionCloseCommand = ms(ctx.getCfg('services.CoAuthoring.expire.sessionclosecommand', cfgExpSessionCloseCommand));
if (preStopFlag && (tenExpSessionIdle > 5 * 60 * 1000 || tenExpSessionIdle <= 0)) {
tenExpSessionIdle = 5 * 60 * 1000; //5 minutes
}
const maxMs = nowMs + Math.max(tenExpSessionCloseCommand, expDocumentsStep);
let tenant = tenants[ctx.tenant];
@ -4732,6 +4741,26 @@ exports.shutdown = function (req, res) {
}
});
};
exports.preStop = async function (req, res) {
let output = false;
const ctx = new operationContext.Context();
try {
ctx.initFromRequest(req);
await ctx.initTenantCache();
preStopFlag = req.method === 'PUT';
ctx.logger.info('preStop set flag', preStopFlag);
if (preStopFlag) {
await gc.checkFileExpire(0);
}
output = true;
} catch (err) {
ctx.logger.error('preStop error %s', err.stack);
} finally {
res.setHeader('Content-Type', 'text/plain');
res.send(output.toString());
ctx.logger.info('preStop end');
}
};
/**
* Get active connections array
* @returns {Array} Active connections

View File

@ -467,6 +467,9 @@ const cleanupCache = co.wrap(function* (ctx, docId) {
const removeRes = yield taskResult.remove(ctx, docId);
if (removeRes.affectedRows > 0) {
yield storage.deletePath(ctx, docId);
if (docsCoServer?.editorStatProxy?.deleteKey) {
yield docsCoServer.editorStatProxy.deleteKey(docId);
}
res = true;
}
ctx.logger.debug('cleanupCache docId=%s db.affectedRows=%d', docId, removeRes.affectedRows);
@ -479,6 +482,9 @@ const cleanupCacheIf = co.wrap(function* (ctx, mask) {
if (removeRes.affectedRows > 0) {
sqlBase.deleteChanges(ctx, mask.key, null);
yield storage.deletePath(ctx, mask.key);
if (docsCoServer?.editorStatProxy?.deleteKey) {
yield docsCoServer.editorStatProxy.deleteKey(mask.key);
}
res = true;
}
ctx.logger.debug('cleanupCacheIf db.affectedRows=%d', removeRes.affectedRows);
@ -1300,7 +1306,7 @@ const commandSfcCallback = co.wrap(function* (ctx, cmd, isSfcm, isEncrypted) {
//todo simultaneous opening
//clean redis (redisKeyPresenceSet and redisKeyPresenceHash removed with last element)
yield docsCoServer.editorData.cleanDocumentOnExit(ctx, docId);
if (docsCoServer?.editorStatProxy?.deleteKey) {
if (docsCoServer.getIsPreStop() && docsCoServer?.editorStatProxy?.deleteKey) {
yield docsCoServer.editorStatProxy.deleteKey(docId);
}
//to unlock wopi file

View File

@ -274,6 +274,9 @@ docsCoServer.install(server, app, () => {
app.use('/info', infoRouter(docsCoServer.getConnections));
app.put('/internal/cluster/inactive', utils.checkClientIp, docsCoServer.shutdown);
app.delete('/internal/cluster/inactive', utils.checkClientIp, docsCoServer.shutdown);
app.put('/internal/cluster/pre-stop', utils.checkClientIp, docsCoServer.preStop);
app.delete('/internal/cluster/pre-stop', utils.checkClientIp, docsCoServer.preStop);
app.get('/internal/connections/edit', docsCoServer.getEditorConnectionsCount);
function checkWopiEnable(req, res, next) {