mirror of
https://github.com/Foundry376/Mailspring.git
synced 2025-01-15 12:39:28 +08:00
[local-sync] syncback(Part 1): Refactor syncback-task-helpers
Summary: Instead of exposing helper functions, make this a class to hold the shared state of the db, account, and logger required to run any syncback inside an account sync loop. Test Plan: manual Reviewers: mark, spang, halla, evan Reviewed By: spang, halla, evan Differential Revision: https://phab.nylas.com/D3893
This commit is contained in:
parent
dbb404ccba
commit
1e74be3b94
4 changed files with 205 additions and 173 deletions
|
@ -1,7 +1,7 @@
|
||||||
const Joi = require('joi');
|
const Joi = require('joi');
|
||||||
const Utils = require('../../shared/utils');
|
const Utils = require('../../shared/utils');
|
||||||
const SyncbackTaskFactory = require('../../local-sync-worker/syncback-task-factory');
|
const SyncbackTaskFactory = require('../../local-sync-worker/syncback-task-factory');
|
||||||
const {runSyncbackTask} = require('../../local-sync-worker/syncback-task-helpers');
|
const SyncbackTaskRunner = require('../../local-sync-worker/syncback-task-runner').default;
|
||||||
const {createAndReplyWithSyncbackRequest} = require('../route-helpers');
|
const {createAndReplyWithSyncbackRequest} = require('../route-helpers');
|
||||||
|
|
||||||
|
|
||||||
|
@ -39,10 +39,14 @@ module.exports = (server) => {
|
||||||
|
|
||||||
const sendTask = SyncbackTaskFactory.create(account, syncbackRequest)
|
const sendTask = SyncbackTaskFactory.create(account, syncbackRequest)
|
||||||
const db = await request.getAccountDatabase()
|
const db = await request.getAccountDatabase()
|
||||||
await runSyncbackTask({
|
const runner = new SyncbackTaskRunner({
|
||||||
|
db,
|
||||||
|
account,
|
||||||
|
logger: request.logger.child(),
|
||||||
|
})
|
||||||
|
await runner.runSyncbackTask({
|
||||||
task: sendTask,
|
task: sendTask,
|
||||||
runTask: (t) => t.run(db),
|
runTask: (t) => t.run(db),
|
||||||
logger: request.logger.child(),
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -69,10 +73,14 @@ module.exports = (server) => {
|
||||||
|
|
||||||
const sendTask = SyncbackTaskFactory.create(account, syncbackRequest)
|
const sendTask = SyncbackTaskFactory.create(account, syncbackRequest)
|
||||||
const db = await request.getAccountDatabase()
|
const db = await request.getAccountDatabase()
|
||||||
await runSyncbackTask({
|
const runner = new SyncbackTaskRunner({
|
||||||
|
db,
|
||||||
|
account,
|
||||||
|
logger: request.logger.child(),
|
||||||
|
})
|
||||||
|
await runner.runSyncbackTask({
|
||||||
task: sendTask,
|
task: sendTask,
|
||||||
runTask: (t) => t.run(db),
|
runTask: (t) => t.run(db),
|
||||||
logger: request.logger.child(),
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -14,7 +14,7 @@ const {
|
||||||
const Interruptible = require('../shared/interruptible')
|
const Interruptible = require('../shared/interruptible')
|
||||||
const SyncMetricsReporter = require('./sync-metrics-reporter');
|
const SyncMetricsReporter = require('./sync-metrics-reporter');
|
||||||
const SyncTaskFactory = require('./sync-task-factory');
|
const SyncTaskFactory = require('./sync-task-factory');
|
||||||
const {getNewSyncbackTasks, markInProgressTasksAsFailed, runSyncbackTask} = require('./syncback-task-helpers');
|
const SyncbackTaskRunner = require('./syncback-task-runner').default;
|
||||||
const LocalSyncDeltaEmitter = require('./local-sync-delta-emitter').default
|
const LocalSyncDeltaEmitter = require('./local-sync-delta-emitter').default
|
||||||
|
|
||||||
|
|
||||||
|
@ -28,13 +28,18 @@ class SyncWorker {
|
||||||
this._conn = null;
|
this._conn = null;
|
||||||
this._account = account;
|
this._account = account;
|
||||||
this._currentTask = null
|
this._currentTask = null
|
||||||
|
this._mailListenerConn = null
|
||||||
this._interruptible = new Interruptible()
|
this._interruptible = new Interruptible()
|
||||||
this._localDeltas = new LocalSyncDeltaEmitter(db, account.id)
|
this._localDeltas = new LocalSyncDeltaEmitter(db, account.id)
|
||||||
this._mailListenerConn = null
|
this._logger = global.Logger.forAccount(account)
|
||||||
|
this._syncbackTaskRunner = new SyncbackTaskRunner({
|
||||||
|
db,
|
||||||
|
account,
|
||||||
|
logger: this._logger,
|
||||||
|
})
|
||||||
|
|
||||||
this._startTime = Date.now()
|
this._startTime = Date.now()
|
||||||
this._lastSyncTime = null
|
this._lastSyncTime = null
|
||||||
this._logger = global.Logger.forAccount(account)
|
|
||||||
this._interrupted = false
|
this._interrupted = false
|
||||||
this._syncInProgress = false
|
this._syncInProgress = false
|
||||||
this._stopped = false
|
this._stopped = false
|
||||||
|
@ -416,7 +421,7 @@ class SyncWorker {
|
||||||
yield this._ensureMailListenerConnection();
|
yield this._ensureMailListenerConnection();
|
||||||
|
|
||||||
// Step 1: Mark all "INPROGRESS" tasks as failed.
|
// Step 1: Mark all "INPROGRESS" tasks as failed.
|
||||||
await markInProgressTasksAsFailed({db: this._db})
|
await this._syncbackTaskRunner.markInProgressTasksAsFailed()
|
||||||
yield // Yield to allow interruption
|
yield // Yield to allow interruption
|
||||||
|
|
||||||
// Step 2: Run any available syncback tasks
|
// Step 2: Run any available syncback tasks
|
||||||
|
@ -426,12 +431,11 @@ class SyncWorker {
|
||||||
// (e.g. marking as unread or starred). We need to listen to that event for
|
// (e.g. marking as unread or starred). We need to listen to that event for
|
||||||
// when updates are performed from another mail client, but ignore
|
// when updates are performed from another mail client, but ignore
|
||||||
// them when they are caused from within N1 to prevent unecessary interrupts
|
// them when they are caused from within N1 to prevent unecessary interrupts
|
||||||
const tasks = yield getNewSyncbackTasks({db: this._db, account: this._account})
|
const tasks = yield this._syncbackTaskRunner.getNewSyncbackTasks()
|
||||||
this._shouldIgnoreInboxFlagUpdates = true
|
this._shouldIgnoreInboxFlagUpdates = true
|
||||||
for (const task of tasks) {
|
for (const task of tasks) {
|
||||||
const {shouldRetry} = await runSyncbackTask({
|
const {shouldRetry} = await this._syncbackTaskRunner.runSyncbackTask({
|
||||||
task,
|
task,
|
||||||
logger: this._logger,
|
|
||||||
runTask: (t) => this._conn.runOperation(t),
|
runTask: (t) => this._conn.runOperation(t),
|
||||||
})
|
})
|
||||||
if (shouldRetry) {
|
if (shouldRetry) {
|
||||||
|
|
|
@ -1,161 +0,0 @@
|
||||||
const {Actions} = require('nylas-exports')
|
|
||||||
const {IMAPErrors} = require('isomorphic-core')
|
|
||||||
const SyncbackTaskFactory = require('./syncback-task-factory');
|
|
||||||
|
|
||||||
const MAX_TASK_RETRIES = 2
|
|
||||||
|
|
||||||
// TODO NOTE! These are the tasks we exclude from the sync loop. This should be
|
|
||||||
// refactored later.
|
|
||||||
export const SendTaskTypes = ['SendMessage', 'SendMessagePerRecipient']
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a list of at most 100 Syncback requests, sorted by creation date
|
|
||||||
* (older first) and by how they affect message IMAP uids.
|
|
||||||
*
|
|
||||||
* We want to make sure that we run the tasks that affect IMAP uids last, and
|
|
||||||
* that we don't run 2 tasks that will affect the same set of UIDS together,
|
|
||||||
* i.e. without running a sync loop in between them.
|
|
||||||
*
|
|
||||||
* For example, if there's a task to change the labels of a message, and also
|
|
||||||
* a task to move that message to another folder, we need to run the label
|
|
||||||
* change /first/, otherwise the message would be moved and it would receive a
|
|
||||||
* new IMAP uid, and then attempting to change labels with an old uid would
|
|
||||||
* fail.
|
|
||||||
*
|
|
||||||
* TODO NOTE: This function excludes Send tasks because these are run outside fo the
|
|
||||||
* sync loop for performance reasons.
|
|
||||||
*/
|
|
||||||
export async function getNewSyncbackTasks({db, account} = {}) {
|
|
||||||
const {SyncbackRequest, Message} = db;
|
|
||||||
|
|
||||||
const ensureSentFolderTasks = await SyncbackRequest.findAll({
|
|
||||||
limit: 100,
|
|
||||||
where: {type: ['EnsureMessageInSentFolder'], status: 'NEW'},
|
|
||||||
order: [['createdAt', 'ASC']],
|
|
||||||
})
|
|
||||||
.map((req) => SyncbackTaskFactory.create(account, req))
|
|
||||||
const tasks = await SyncbackRequest.findAll({
|
|
||||||
limit: 100,
|
|
||||||
where: {type: {$notIn: [...SendTaskTypes, 'EnsureMessageInSentFolder']}, status: 'NEW'},
|
|
||||||
order: [['createdAt', 'ASC']],
|
|
||||||
})
|
|
||||||
.map((req) => SyncbackTaskFactory.create(account, req))
|
|
||||||
|
|
||||||
if (ensureSentFolderTasks.length === 0 && tasks.length === 0) { return [] }
|
|
||||||
|
|
||||||
const tasksToProcess = [
|
|
||||||
...ensureSentFolderTasks,
|
|
||||||
...tasks.filter(t => !t.affectsImapMessageUIDs()),
|
|
||||||
]
|
|
||||||
const tasksAffectingUIDs = tasks.filter(t => t.affectsImapMessageUIDs())
|
|
||||||
|
|
||||||
const changeFolderTasks = tasksAffectingUIDs.filter(t =>
|
|
||||||
t.description() === 'RenameFolder' || t.description() === 'DeleteFolder'
|
|
||||||
)
|
|
||||||
if (changeFolderTasks.length > 0) {
|
|
||||||
// If we are renaming or deleting folders, those are the only tasks we
|
|
||||||
// want to process before executing any other tasks that may change uids.
|
|
||||||
// These operations may not change the uids of their messages, but we
|
|
||||||
// can't guarantee it, so to make sure, we will just run these.
|
|
||||||
const affectedFolderIds = new Set()
|
|
||||||
changeFolderTasks.forEach((task) => {
|
|
||||||
const {props: {folderId}} = task.syncbackRequestObject()
|
|
||||||
if (folderId && !affectedFolderIds.has(folderId)) {
|
|
||||||
tasksToProcess.push(task)
|
|
||||||
affectedFolderIds.add(folderId)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return tasksToProcess
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise, make sure that we don't process more than 1 task that will affect
|
|
||||||
// the UID of the same message
|
|
||||||
const affectedMessageIds = new Set()
|
|
||||||
for (const task of tasksAffectingUIDs) {
|
|
||||||
const {props: {messageId, threadId}} = task.syncbackRequestObject()
|
|
||||||
if (messageId) {
|
|
||||||
if (!affectedMessageIds.has(messageId)) {
|
|
||||||
tasksToProcess.push(task)
|
|
||||||
affectedMessageIds.add(messageId)
|
|
||||||
}
|
|
||||||
} else if (threadId) {
|
|
||||||
const messageIds = await Message.findAll({where: {threadId}}).map(m => m.id)
|
|
||||||
const shouldIncludeTask = messageIds.every(id => !affectedMessageIds.has(id))
|
|
||||||
if (shouldIncludeTask) {
|
|
||||||
tasksToProcess.push(task)
|
|
||||||
messageIds.forEach(id => affectedMessageIds.add(id))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return tasksToProcess
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function markInProgressTasksAsFailed({db} = {}) {
|
|
||||||
// We use a very limited type of two-phase commit: before we start
|
|
||||||
// running a syncback task, we mark it as "in progress". If something
|
|
||||||
// happens during the syncback (the worker window crashes, or the power
|
|
||||||
// goes down), the task won't succeed or fail.
|
|
||||||
// We absolutely never want to retry such a task, so we mark it as failed
|
|
||||||
// at the next sync iteration. We use this function for that.
|
|
||||||
const {SyncbackRequest} = db;
|
|
||||||
const inProgressTasks = await SyncbackRequest.findAll({
|
|
||||||
// TODO this is a hack
|
|
||||||
// NOTE: We exclude SendTaskTypes because they are run outside of the sync loop
|
|
||||||
where: {type: {$notIn: SendTaskTypes}, status: 'INPROGRESS'},
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const inProgress of inProgressTasks) {
|
|
||||||
inProgress.status = 'FAILED';
|
|
||||||
inProgress.error = new Error('Lingering task in progress was marked as failed')
|
|
||||||
await inProgress.save();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO JUAN! remove this uglyness that is runTask
|
|
||||||
export async function runSyncbackTask({task, runTask, logger = console} = {}) {
|
|
||||||
const before = new Date();
|
|
||||||
const syncbackRequest = task.syncbackRequestObject();
|
|
||||||
let shouldRetry = false
|
|
||||||
|
|
||||||
logger.log(`🔃 📤 ${task.description()}`, syncbackRequest.props)
|
|
||||||
try {
|
|
||||||
// Before anything, mark the task as in progress. This allows
|
|
||||||
// us to not run the same task twice.
|
|
||||||
syncbackRequest.status = "INPROGRESS";
|
|
||||||
await syncbackRequest.save();
|
|
||||||
|
|
||||||
// TODO `runTask` is a hack to allow tasks to be executed outside the
|
|
||||||
// context of an imap connection, specifically to allow running send tasks
|
|
||||||
// outside of the sync loop. This should be solved in a better way or
|
|
||||||
// probably refactored when we implement the sync scheduler
|
|
||||||
const responseJSON = await runTask(task)
|
|
||||||
syncbackRequest.status = "SUCCEEDED";
|
|
||||||
syncbackRequest.responseJSON = responseJSON || {};
|
|
||||||
|
|
||||||
const after = new Date();
|
|
||||||
logger.log(`🔃 📤 ${task.description()} Succeeded (${after.getTime() - before.getTime()}ms)`)
|
|
||||||
} catch (error) {
|
|
||||||
const after = new Date();
|
|
||||||
const {numRetries = 0} = syncbackRequest.props
|
|
||||||
|
|
||||||
if (error instanceof IMAPErrors.RetryableError && numRetries < MAX_TASK_RETRIES) {
|
|
||||||
Actions.recordUserEvent('Retrying syncback task', {numRetries})
|
|
||||||
shouldRetry = true
|
|
||||||
// We save this in `props` to avoid a db migration
|
|
||||||
syncbackRequest.props = Object.assign({}, syncbackRequest.props, {
|
|
||||||
numRetries: numRetries + 1,
|
|
||||||
})
|
|
||||||
syncbackRequest.status = "NEW";
|
|
||||||
logger.warn(`🔃 📤 ${task.description()} Failed with retryable error, retrying in next loop (${after.getTime() - before.getTime()}ms)`, {syncbackRequest: syncbackRequest.toJSON(), error})
|
|
||||||
} else {
|
|
||||||
error.message = `Syncback Task Failed: ${error.message}`
|
|
||||||
syncbackRequest.error = error;
|
|
||||||
syncbackRequest.status = "FAILED";
|
|
||||||
NylasEnv.reportError(error);
|
|
||||||
logger.error(`🔃 📤 ${task.description()} Failed (${after.getTime() - before.getTime()}ms)`, {syncbackRequest: syncbackRequest.toJSON(), error})
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
await syncbackRequest.save();
|
|
||||||
}
|
|
||||||
return {shouldRetry}
|
|
||||||
}
|
|
|
@ -0,0 +1,181 @@
|
||||||
|
const {Actions} = require('nylas-exports')
|
||||||
|
const {IMAPErrors} = require('isomorphic-core')
|
||||||
|
const SyncbackTaskFactory = require('./syncback-task-factory');
|
||||||
|
|
||||||
|
const MAX_TASK_RETRIES = 2
|
||||||
|
|
||||||
|
// TODO NOTE! These are the tasks we exclude from the sync loop. This should be
|
||||||
|
// refactored later.
|
||||||
|
export const SendTaskTypes = ['SendMessage', 'SendMessagePerRecipient']
|
||||||
|
|
||||||
|
class SyncbackTaskRunner {
|
||||||
|
|
||||||
|
constructor({db, account, logger} = {}) {
|
||||||
|
if (!db) {
|
||||||
|
throw new Error('SyncbackTaskRunner: need to pass db')
|
||||||
|
}
|
||||||
|
if (!account) {
|
||||||
|
throw new Error('SyncbackTaskRunner: need to pass account')
|
||||||
|
}
|
||||||
|
if (!logger) {
|
||||||
|
throw new Error('SyncbackTaskRunner: need to pass logger')
|
||||||
|
}
|
||||||
|
this._db = db
|
||||||
|
this._account = account
|
||||||
|
this._logger = logger
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a list of at most 100 Syncback requests, sorted by creation date
|
||||||
|
* (older first) and by how they affect message IMAP uids.
|
||||||
|
*
|
||||||
|
* We want to make sure that we run the tasks that affect IMAP uids last, and
|
||||||
|
* that we don't run 2 tasks that will affect the same set of UIDS together,
|
||||||
|
* i.e. without running a sync loop in between them.
|
||||||
|
*
|
||||||
|
* For example, if there's a task to change the labels of a message, and also
|
||||||
|
* a task to move that message to another folder, we need to run the label
|
||||||
|
* change /first/, otherwise the message would be moved and it would receive a
|
||||||
|
* new IMAP uid, and then attempting to change labels with an old uid would
|
||||||
|
* fail.
|
||||||
|
*
|
||||||
|
* TODO NOTE: This function excludes Send tasks because these are run outside fo the
|
||||||
|
* sync loop for performance reasons.
|
||||||
|
*/
|
||||||
|
async getNewSyncbackTasks() {
|
||||||
|
const {SyncbackRequest, Message} = this._db;
|
||||||
|
|
||||||
|
const ensureSentFolderTasks = await SyncbackRequest.findAll({
|
||||||
|
limit: 100,
|
||||||
|
where: {type: ['EnsureMessageInSentFolder'], status: 'NEW'},
|
||||||
|
order: [['createdAt', 'ASC']],
|
||||||
|
})
|
||||||
|
.map((req) => SyncbackTaskFactory.create(this._account, req))
|
||||||
|
const tasks = await SyncbackRequest.findAll({
|
||||||
|
limit: 100,
|
||||||
|
where: {type: {$notIn: [...SendTaskTypes, 'EnsureMessageInSentFolder']}, status: 'NEW'},
|
||||||
|
order: [['createdAt', 'ASC']],
|
||||||
|
})
|
||||||
|
.map((req) => SyncbackTaskFactory.create(this._account, req))
|
||||||
|
|
||||||
|
if (ensureSentFolderTasks.length === 0 && tasks.length === 0) { return [] }
|
||||||
|
|
||||||
|
const tasksToProcess = [
|
||||||
|
...ensureSentFolderTasks,
|
||||||
|
...tasks.filter(t => !t.affectsImapMessageUIDs()),
|
||||||
|
]
|
||||||
|
const tasksAffectingUIDs = tasks.filter(t => t.affectsImapMessageUIDs())
|
||||||
|
|
||||||
|
const changeFolderTasks = tasksAffectingUIDs.filter(t =>
|
||||||
|
t.description() === 'RenameFolder' || t.description() === 'DeleteFolder'
|
||||||
|
)
|
||||||
|
if (changeFolderTasks.length > 0) {
|
||||||
|
// If we are renaming or deleting folders, those are the only tasks we
|
||||||
|
// want to process before executing any other tasks that may change uids.
|
||||||
|
// These operations may not change the uids of their messages, but we
|
||||||
|
// can't guarantee it, so to make sure, we will just run these.
|
||||||
|
const affectedFolderIds = new Set()
|
||||||
|
changeFolderTasks.forEach((task) => {
|
||||||
|
const {props: {folderId}} = task.syncbackRequestObject()
|
||||||
|
if (folderId && !affectedFolderIds.has(folderId)) {
|
||||||
|
tasksToProcess.push(task)
|
||||||
|
affectedFolderIds.add(folderId)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return tasksToProcess
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, make sure that we don't process more than 1 task that will affect
|
||||||
|
// the UID of the same message
|
||||||
|
const affectedMessageIds = new Set()
|
||||||
|
for (const task of tasksAffectingUIDs) {
|
||||||
|
const {props: {messageId, threadId}} = task.syncbackRequestObject()
|
||||||
|
if (messageId) {
|
||||||
|
if (!affectedMessageIds.has(messageId)) {
|
||||||
|
tasksToProcess.push(task)
|
||||||
|
affectedMessageIds.add(messageId)
|
||||||
|
}
|
||||||
|
} else if (threadId) {
|
||||||
|
const messageIds = await Message.findAll({where: {threadId}}).map(m => m.id)
|
||||||
|
const shouldIncludeTask = messageIds.every(id => !affectedMessageIds.has(id))
|
||||||
|
if (shouldIncludeTask) {
|
||||||
|
tasksToProcess.push(task)
|
||||||
|
messageIds.forEach(id => affectedMessageIds.add(id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return tasksToProcess
|
||||||
|
}
|
||||||
|
|
||||||
|
async markInProgressTasksAsFailed() {
|
||||||
|
// We use a very limited type of two-phase commit: before we start
|
||||||
|
// running a syncback task, we mark it as "in progress". If something
|
||||||
|
// happens during the syncback (the worker window crashes, or the power
|
||||||
|
// goes down), the task won't succeed or fail.
|
||||||
|
// We absolutely never want to retry such a task, so we mark it as failed
|
||||||
|
// at the next sync iteration. We use this function for that.
|
||||||
|
const {SyncbackRequest} = this._db;
|
||||||
|
const inProgressTasks = await SyncbackRequest.findAll({
|
||||||
|
// TODO this is a hack
|
||||||
|
// NOTE: We exclude SendTaskTypes because they are run outside of the sync loop
|
||||||
|
where: {type: {$notIn: SendTaskTypes}, status: 'INPROGRESS'},
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const inProgress of inProgressTasks) {
|
||||||
|
inProgress.status = 'FAILED';
|
||||||
|
inProgress.error = new Error('Lingering task in progress was marked as failed')
|
||||||
|
await inProgress.save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO JUAN! remove this uglyness that is runTask
|
||||||
|
async runSyncbackTask({task, runTask} = {}) {
|
||||||
|
const before = new Date();
|
||||||
|
const syncbackRequest = task.syncbackRequestObject();
|
||||||
|
let shouldRetry = false
|
||||||
|
|
||||||
|
this._logger.log(`🔃 📤 ${task.description()}`, syncbackRequest.props)
|
||||||
|
try {
|
||||||
|
// Before anything, mark the task as in progress. This allows
|
||||||
|
// us to not run the same task twice.
|
||||||
|
syncbackRequest.status = "INPROGRESS";
|
||||||
|
await syncbackRequest.save();
|
||||||
|
|
||||||
|
// TODO `runTask` is a hack to allow tasks to be executed outside the
|
||||||
|
// context of an imap connection, specifically to allow running send tasks
|
||||||
|
// outside of the sync loop. This should be solved in a better way or
|
||||||
|
// probably refactored when we implement the sync scheduler
|
||||||
|
const responseJSON = await runTask(task)
|
||||||
|
syncbackRequest.status = "SUCCEEDED";
|
||||||
|
syncbackRequest.responseJSON = responseJSON || {};
|
||||||
|
|
||||||
|
const after = new Date();
|
||||||
|
this._logger.log(`🔃 📤 ${task.description()} Succeeded (${after.getTime() - before.getTime()}ms)`)
|
||||||
|
} catch (error) {
|
||||||
|
const after = new Date();
|
||||||
|
const {numRetries = 0} = syncbackRequest.props
|
||||||
|
|
||||||
|
if (error instanceof IMAPErrors.RetryableError && numRetries < MAX_TASK_RETRIES) {
|
||||||
|
Actions.recordUserEvent('Retrying syncback task', {numRetries})
|
||||||
|
shouldRetry = true
|
||||||
|
// We save this in `props` to avoid a db migration
|
||||||
|
syncbackRequest.props = Object.assign({}, syncbackRequest.props, {
|
||||||
|
numRetries: numRetries + 1,
|
||||||
|
})
|
||||||
|
syncbackRequest.status = "NEW";
|
||||||
|
this._logger.warn(`🔃 📤 ${task.description()} Failed with retryable error, retrying in next loop (${after.getTime() - before.getTime()}ms)`, {syncbackRequest: syncbackRequest.toJSON(), error})
|
||||||
|
} else {
|
||||||
|
error.message = `Syncback Task Failed: ${error.message}`
|
||||||
|
syncbackRequest.error = error;
|
||||||
|
syncbackRequest.status = "FAILED";
|
||||||
|
NylasEnv.reportError(error);
|
||||||
|
this._logger.error(`🔃 📤 ${task.description()} Failed (${after.getTime() - before.getTime()}ms)`, {syncbackRequest: syncbackRequest.toJSON(), error})
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
await syncbackRequest.save();
|
||||||
|
}
|
||||||
|
return {shouldRetry}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SyncbackTaskRunner
|
Loading…
Reference in a new issue