Use taskId instead of states to check bad state

a) this is because, we have install state and run state.
b) we have to put taskId as part of the transaction to prevent race
This commit is contained in:
Girish Ramakrishnan
2019-08-29 12:14:42 -07:00
parent ed57260fcf
commit 9cf833dab2
2 changed files with 64 additions and 106 deletions

View File

@@ -322,17 +322,17 @@ function validateDataDir(dataDir) {
return null;
}
function getDuplicateErrorDetails(error, location, domainObject, portBindings, alternateDomains) {
assert.strictEqual(error.reason, DatabaseError.ALREADY_EXISTS);
function getDuplicateErrorDetails(errorMessage, location, domainObject, portBindings, alternateDomains) {
assert.strictEqual(typeof errorMessage, 'string');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof portBindings, 'object');
assert(Array.isArray(alternateDomains));
var match = error.message.match(/ER_DUP_ENTRY: Duplicate entry '(.*)' for key '(.*)'/);
var match = errorMessage.match(/ER_DUP_ENTRY: Duplicate entry '(.*)' for key '(.*)'/);
if (!match) {
debug('Unexpected SQL error message.', error);
return new AppsError(AppsError.INTERNAL_ERROR, error);
debug('Unexpected SQL error message.', errorMessage);
return new AppsError(AppsError.INTERNAL_ERROR, new Error(errorMessage));
}
// check if the location or alternateDomains conflicts
@@ -576,15 +576,23 @@ function mailboxNameForLocation(location, manifest) {
return (location ? location : manifest.title.toLowerCase().replace(/[^a-zA-Z0-9]/g, '')) + '.app';
}
function scheduleTask(appId, args, callback) {
function scheduleTask(appId, args, values, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof args, 'object');
assert.strictEqual(typeof values, 'object');
assert.strictEqual(typeof callback, 'function');
assert(values.installationState || values.runState);
tasks.add(tasks.TASK_APP, [ appId, args ], function (error, taskId) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
appdb.update(appId, { taskId: taskId }, function (error) {
values.errorMessage = '';
values.taskId = taskId;
appdb.setTask(appId, values, function (error) {
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(new AppsError(AppsError.ALREADY_EXISTS, error.message));
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.BAD_STATE));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
appTaskManager.scheduleTask(appId, taskId, function (error) {
@@ -713,7 +721,7 @@ function install(data, user, auditSource, callback) {
};
appdb.add(appId, appStoreId, manifest, location, domain, translatePortBindings(portBindings, manifest), data, function (error) {
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails(error, location, domainObject, portBindings, data.alternateDomains));
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails(error.message, location, domainObject, portBindings, data.alternateDomains));
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, error.message));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
@@ -728,7 +736,7 @@ function install(data, user, auditSource, callback) {
const restoreConfig = backupId ? { backupId: backupId, backupFormat: backupFormat } : null;
scheduleTask(appId, { restoreConfig }, function (error, result) {
scheduleTask(appId, { restoreConfig }, { installationState: appdb.ISTATE_PENDING_INSTALL }, function (error, result) {
if (error) return callback(error);
eventlog.add(eventlog.ACTION_APP_INSTALL, auditSource, { appId: appId, app: result });
@@ -750,9 +758,10 @@ function configure(appId, data, user, auditSource, callback) {
get(appId, function (error, app) {
if (error) return callback(error);
if (app.installationState !== appdb.ISTATE_INSTALLED && app.installationState !== appdb.ISTATE_ERROR) return callback(new AppsError(AppsError.BAD_STATE, `Not allowed in this app state : ${app.installationState}`));
if (app.taskId) return callback(new AppsError(AppsError.BAD_STATE, `Not allowed in this app state : ${app.installationState} / ${app.runState}`));
let domain, location, portBindings, values = { installationState: appdb.ISTATE_PENDING_CONFIGURE };
let domain, location, portBindings, values = { };
if ('location' in data && 'domain' in data) {
location = values.location = data.location.toLowerCase();
domain = values.domain = data.domain.toLowerCase();
@@ -870,18 +879,13 @@ function configure(appId, data, user, auditSource, callback) {
debug(`configure: id:${appId}`);
appdb.setInstallationCommand(appId, appdb.ISTATE_PENDING_CONFIGURE, values, function (error) {
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails(error, location, domainObject, portBindings, data.alternateDomains));
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.BAD_STATE));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
scheduleTask(appId, { oldConfig: getAppConfig(app) }, values, function (error, result) {
if (error && error.reason === AppsError.ALREADY_EXISTS) error = getDuplicateErrorDetails(error.message, location, domainObject, portBindings, data.alternateDomains);
if (error) return callback(error);
scheduleTask(appId, { oldConfig: getAppConfig(app) }, function (error, result) {
if (error) return callback(error);
eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId: appId, app: result });
eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId: appId, app: result });
callback(null, { taskId: result.taskId });
});
callback(null, { taskId: result.taskId });
});
});
});
@@ -897,7 +901,7 @@ function update(appId, data, auditSource, callback) {
get(appId, function (error, app) {
if (error) return callback(error);
if (app.installationState !== appdb.ISTATE_INSTALLED && app.installationState !== appdb.ISTATE_ERROR) return callback(new AppsError(AppsError.BAD_STATE, `Not allowed in this app state : ${app.installationState}`));
if (app.taskId) return callback(new AppsError(AppsError.BAD_STATE, `Not allowed in this app state : ${app.installationState} / ${app.runState}`));
downloadManifest(data.appStoreId, data.manifest, function (error, appStoreId, manifest) {
if (error) return callback(error);
@@ -951,20 +955,15 @@ function update(appId, data, auditSource, callback) {
updateConfig.memoryLimit = updateConfig.manifest.memoryLimit;
}
appdb.setInstallationCommand(appId, appdb.ISTATE_PENDING_UPDATE, { }, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.BAD_STATE)); // might be a bad guess
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
scheduleTask(appId, { updateConfig: updateConfig }, { installationState: appdb.ISTATE_PENDING_UPDATE }, function (error, result) {
if (error) return callback(error);
scheduleTask(appId, { updateConfig: updateConfig }, function (error, result) {
if (error) return callback(error);
eventlog.add(eventlog.ACTION_APP_UPDATE, auditSource, { appId: appId, toManifest: manifest, fromManifest: app.manifest, force: data.force, app: app });
eventlog.add(eventlog.ACTION_APP_UPDATE, auditSource, { appId: appId, toManifest: manifest, fromManifest: app.manifest, force: data.force, app: app });
// clear update indicator, if update fails, it will come back through the update checker
updateChecker.resetAppUpdateInfo(appId);
// clear update indicator, if update fails, it will come back through the update checker
updateChecker.resetAppUpdateInfo(appId);
callback(null, { taskId: result.taskId });
});
callback(null, { taskId: result.taskId });
});
});
});
@@ -1034,7 +1033,7 @@ function restore(appId, data, auditSource, callback) {
get(appId, function (error, app) {
if (error) return callback(error);
if (app.installationState !== appdb.ISTATE_INSTALLED && app.installationState !== appdb.ISTATE_ERROR) return callback(new AppsError(AppsError.BAD_STATE, `Not allowed in this app state : ${app.installationState}`));
if (app.taskId) return callback(new AppsError(AppsError.BAD_STATE, `Not allowed in this app state : ${app.installationState} / ${app.runState}`));
// for empty or null backupId, use existing manifest to mimic a reinstall
var func = data.backupId ? backups.get.bind(null, data.backupId) : function (next) { return next(null, { manifest: app.manifest }); };
@@ -1050,22 +1049,18 @@ function restore(appId, data, auditSource, callback) {
error = checkManifestConstraints(backupInfo.manifest);
if (error) return callback(error);
var values = {
let values = {
installationState: appdb.ISTATE_PENDING_RESTORE,
manifest: backupInfo.manifest
};
appdb.setInstallationCommand(appId, appdb.ISTATE_PENDING_RESTORE, values, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.BAD_STATE)); // might be a bad guess
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
const restoreConfig = data.backupId ? { backupId: data.backupId, backupFormat: backupInfo.format, oldManifest: app.manifest } : null; // when null, apptask simply reinstalls
scheduleTask(appId, { restoreConfig }, values, function (error, result) {
if (error) return callback(error);
const restoreConfig = data.backupId ? { backupId: data.backupId, backupFormat: backupInfo.format, oldManifest: app.manifest } : null; // when null, apptask simply reinstalls
scheduleTask(appId, { restoreConfig }, function (error, result) {
if (error) return callback(error);
eventlog.add(eventlog.ACTION_APP_RESTORE, auditSource, { app: app, backupId: backupInfo.id, fromManifest: app.manifest, toManifest: backupInfo.manifest });
eventlog.add(eventlog.ACTION_APP_RESTORE, auditSource, { app: app, backupId: backupInfo.id, fromManifest: app.manifest, toManifest: backupInfo.manifest });
callback(null, { taskId: result.taskId });
});
callback(null, { taskId: result.taskId });
});
});
});
@@ -1161,7 +1156,7 @@ function clone(appId, data, user, auditSource, callback) {
};
appdb.add(newAppId, app.appStoreId, manifest, location, domain, translatePortBindings(portBindings, manifest), data, function (error) {
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails(error, location, domainObject, portBindings, []));
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails(error.message, location, domainObject, portBindings, []));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
purchaseApp({ appId: newAppId, appstoreId: app.appStoreId, manifestId: manifest.id }, function (error) {
@@ -1169,7 +1164,7 @@ function clone(appId, data, user, auditSource, callback) {
const restoreConfig = { backupId: backupId, backupFormat: backupInfo.format };
scheduleTask(newAppId, { restoreConfig }, function (error, result) {
scheduleTask(newAppId, { restoreConfig }, { installationState: appdb.ISTATE_PENDING_CLONE }, function (error, result) {
if (error) return callback(error);
eventlog.add(eventlog.ACTION_APP_CLONE, auditSource, { appId: newAppId, oldAppId: appId, backupId: backupId, oldApp: app, newApp: result });
@@ -1192,7 +1187,7 @@ function uninstall(appId, auditSource, callback) {
get(appId, function (error, app) {
if (error) return callback(error);
if (app.installationState !== appdb.ISTATE_INSTALLED && app.installationState !== appdb.ISTATE_ERROR) return callback(new AppsError(AppsError.BAD_STATE, `Not allowed in this app state : ${app.installationState}`));
if (app.taskId) return callback(new AppsError(AppsError.BAD_STATE, `Not allowed in this app state : ${app.installationState} / ${app.runState}`));
appstore.unpurchaseApp(appId, { appstoreId: app.appStoreId, manifestId: app.manifest.id }, function (error) {
if (error && error.reason === AppstoreError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND));
@@ -1201,17 +1196,12 @@ function uninstall(appId, auditSource, callback) {
if (error && error.reason === AppstoreError.EXTERNAL_ERROR) return callback(new AppsError(AppsError.EXTERNAL_ERROR, error.message));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
appdb.update(appId, { installationState: appdb.ISTATE_PENDING_UNINSTALL }, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such app'));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
scheduleTask(appId, { /* args */ }, { installationState: appdb.ISTATE_PENDING_UNINSTALL }, function (error, result) {
if (error) return callback(error);
scheduleTask(appId, {}, function (error, result) {
if (error) return callback(error);
eventlog.add(eventlog.ACTION_APP_UNINSTALL, auditSource, { appId: appId, app: result });
eventlog.add(eventlog.ACTION_APP_UNINSTALL, auditSource, { appId: appId, app: result });
callback(null, { taskId: result.taskId });
});
callback(null, { taskId: result.taskId });
});
});
});
@@ -1225,15 +1215,9 @@ function start(appId, callback) {
get(appId, function (error, app) {
if (error) return callback(error);
if (app.installationState !== appdb.ISTATE_INSTALLED) return callback(new AppsError(AppsError.BAD_STATE, `Not allowed in this app state : ${app.installationState}`));
if (app.runState !== appdb.RSTATE_STOPPED) return callback(new AppsError(AppsError.BAD_STATE, `Not allowed in this app state : ${app.installationState}`));
if (app.taskId) return callback(new AppsError(AppsError.BAD_STATE, `Not allowed in this app state : ${app.installationState} / ${app.runState}`));
appdb.setRunCommand(appId, appdb.RSTATE_PENDING_START, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.BAD_STATE)); // might be a bad guess
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
scheduleTask(appId, {}, callback);
});
scheduleTask(appId, { /* args */ }, { runState: appdb.RSTATE_PENDING_START }, callback);
});
}
@@ -1245,15 +1229,9 @@ function stop(appId, callback) {
get(appId, function (error, app) {
if (error) return callback(error);
if (app.installationState !== appdb.ISTATE_INSTALLED) return callback(new AppsError(AppsError.BAD_STATE, `Not allowed in this app state : ${app.installationState}`));
if (app.runState !== appdb.RSTATE_RUNNING) return callback(new AppsError(AppsError.BAD_STATE, `Not allowed in this app state : ${app.installationState}`));
if (app.taskId) return callback(new AppsError(AppsError.BAD_STATE, `Not allowed in this app state : ${app.installationState} / ${app.runState}`));
appdb.setRunCommand(appId, appdb.RSTATE_PENDING_STOP, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.BAD_STATE)); // might be a bad guess
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
scheduleTask(appId, {}, callback);
});
scheduleTask(appId, { /* args */ }, { runState: appdb.RSTATE_PENDING_STOP }, callback);
});
}
@@ -1393,24 +1371,18 @@ function backup(appId, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof callback, 'function');
appdb.exists(appId, function (error, exists) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
if (!exists) return callback(new AppsError(AppsError.NOT_FOUND));
get(appId, function (error, app) {
if (error) return callback(error);
if (app.taskId) return callback(new AppsError(AppsError.BAD_STATE, `Not allowed in this app state : ${app.installationState} / ${app.runState}`));
appdb.setInstallationCommand(appId, appdb.ISTATE_PENDING_BACKUP, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.BAD_STATE)); // might be a bad guess
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
scheduleTask(appId, { /* args */ }, { installationState: appdb.ISTATE_PENDING_BACKUP }, (error, result) => {
if (error) return callback(error);
scheduleTask(appId, { }, (error, result) => {
if (error) return callback(error);
callback(null, { taskId: result.taskId });
});
callback(null, { taskId: result.taskId });
});
});
}
function listBackups(page, perPage, appId, callback) {
assert(typeof page === 'number' && page > 0);
assert(typeof perPage === 'number' && perPage > 0);
@@ -1442,10 +1414,10 @@ function restoreInstalledApps(callback) {
debug(`marking ${app.fqdn} for restore using restore config ${JSON.stringify(restoreConfig)}`);
appdb.setInstallationCommand(app.id, appdb.ISTATE_PENDING_RESTORE, { restoreConfig: restoreConfig }, function (error) {
appdb.update(app.id, { taskId: null }, function (error) { // clear any stale taskId
if (error) debug(`Error marking ${app.fqdn} for restore: ${JSON.stringify(error)}`);
scheduleTask(app.id, { restoreConfig }, () => iteratorDone()); // always succeed
scheduleTask(app.id, { restoreConfig }, { installationState: appdb.ISTATE_PENDING_RESTORE }, () => iteratorDone()); // always succeed
});
});
}, callback);
@@ -1461,10 +1433,10 @@ function configureInstalledApps(callback) {
async.map(apps, function (app, iteratorDone) {
debug(`marking ${app.fqdn} for reconfigure`);
appdb.setInstallationCommand(app.id, appdb.ISTATE_PENDING_CONFIGURE, { }, function (error) {
appdb.update(app.id, { taskId: null }, function (error) { // clear any stale taskId
if (error) debug(`Error marking ${app.fqdn} for reconfigure: ${JSON.stringify(error)}`);
scheduleTask(app.id, { oldConfig: getAppConfig(app) }, () => iteratorDone()); // always succeed
scheduleTask(app.id, { oldConfig: getAppConfig(app) }, { installationState: appdb.ISTATE_PENDING_CONFIGURE }, () => iteratorDone()); // always succeed
});
}, callback);
});