[client-app] Detect when the worker window is unavailable

Summary:
Periodically ping client-sync's /health endpoint and store the latest
sync activity. If we get an ECONNREFUSED error, the worker window is
unavailable. Report the last known activity and restart the worker
window. Part of T7681.

Test Plan: manual, specs

Reviewers: evan, mark, juan

Reviewed By: juan

Differential Revision: https://phab.nylas.com/D4263
This commit is contained in:
Halla Moore 2017-03-27 15:34:14 -07:00
parent 619c69a522
commit 73f0f3eeda
4 changed files with 135 additions and 0 deletions

View file

@ -0,0 +1,9 @@
import SyncHealthChecker from './sync-health-checker'
export function activate() {
SyncHealthChecker.start()
}
export function deactivate() {
SyncHealthChecker.stop()
}

View file

@ -0,0 +1,111 @@
import {ipcRenderer} from 'electron'
import {AccountStore, Actions, NylasAPI, NylasAPIRequest} from 'nylas-exports'
const CHECK_HEALTH_INTERVAL = 5 * 60 * 1000;
class SyncHealthChecker {
constructor() {
this._lastSyncActivity = null
this._interval = null
}
start() {
if (this._interval) {
console.warn('SyncHealthChecker has already been started')
} else {
this._interval = setInterval(this._checkSyncHealth, CHECK_HEALTH_INTERVAL)
}
}
stop() {
clearInterval(this._interval)
this._interval = null
}
// This is a separate function so the request can be manipulated in the specs
_buildRequest = () => {
return new NylasAPIRequest({
api: NylasAPI,
options: {
accountId: AccountStore.accounts()[0].id,
path: `/health`,
},
});
}
_checkSyncHealth = async () => {
try {
const request = this._buildRequest()
const response = await request.run()
this._lastSyncActivity = response
} catch (err) {
if (/ECONNREFUSED/i.test(err.toString())) {
this._onWorkerWindowUnavailable()
} else {
err.message = `Error checking sync health: ${err.message}`
NylasEnv.reportError(err)
}
}
}
_onWorkerWindowUnavailable() {
let extraData = {};
// Extract data that we want to report. We'll report the entire
// _lastSyncActivity object, but it'll probably be useful if we can segment
// by the data in the oldest or newest entry, so we report those as
// individual values too.
const lastActivityEntries = Object.entries(this._lastSyncActivity || {})
if (lastActivityEntries.length > 0) {
const times = lastActivityEntries.map((entry) => entry[1].time)
const now = Date.now()
const maxTime = Math.max(...times)
const mostRecentEntry = lastActivityEntries.find((entry) => entry[1].time === maxTime)
const [mostRecentActivityAccountId, {
activity: mostRecentActivity,
time: mostRecentActivityTime,
}] = mostRecentEntry;
const mostRecentDuration = now - mostRecentActivityTime
const minTime = Math.min(...times)
const leastRecentEntry = lastActivityEntries.find((entry) => entry[1].time === minTime)
const [leastRecentActivityAccountId, {
activity: leastRecentActivity,
time: leastRecentActivityTime,
}] = leastRecentEntry;
const leastRecentDuration = now - leastRecentActivityTime
extraData = {
mostRecentActivity,
mostRecentActivityTime,
mostRecentActivityAccountId,
mostRecentDuration,
leastRecentActivity,
leastRecentActivityTime,
leastRecentActivityAccountId,
leastRecentDuration,
}
}
NylasEnv.reportError(new Error('Worker window was unavailable'), {
// This information isn't as useful in Sentry, but include it here until
// the data is actually sent to Mixpanel. (See the TODO below)
lastActivityPerAccount: this._lastSyncActivity,
...extraData,
})
// TODO: This doesn't make it to Mixpanel because our analytics process
// lives in the worker window. We should move analytics to the main process.
// https://phab.nylas.com/T8029
Actions.recordUserEvent('Worker Window Unavailable', {
lastActivityPerAccount: this._lastSyncActivity,
...extraData,
})
console.log(`Detected worker window was unavailable. Restarting it.`, this._lastSyncActivity)
ipcRenderer.send('ensure-worker-window')
}
}
export default new SyncHealthChecker()

View file

@ -0,0 +1,11 @@
{
"name": "sync-health-checker",
"version": "0.1.0",
"main": "./lib/main",
"description": "Periodically ping the sync process to ensure it's running",
"license": "GPL-3.0",
"private": true,
"engines": {
"nylas": "*"
}
}

View file

@ -465,6 +465,10 @@ export default class Application extends EventEmitter {
}
});
ipcMain.on('ensure-worker-window', () => {
this.windowManager.ensureWindow(WindowManager.WORK_WINDOW)
})
ipcMain.on('inline-style-parse', (event, {html, key}) => {
const juice = require('juice');
let out = null;