Merge branch 'master' of github.com:nylas/K2

This commit is contained in:
Annie 2016-07-11 18:30:19 -07:00
commit c5dce43da7
13 changed files with 122 additions and 52 deletions

View file

@ -7,6 +7,7 @@
"bluebird": "3.x.x", "bluebird": "3.x.x",
"bunyan": "1.8.0", "bunyan": "1.8.0",
"bunyan-cloudwatch": "2.0.0", "bunyan-cloudwatch": "2.0.0",
"bunyan-prettystream": "^0.1.3",
"lerna": "2.0.0-beta.23", "lerna": "2.0.0-beta.23",
"mysql": "^2.11.1", "mysql": "^2.11.1",
"newrelic": "^1.28.1", "newrelic": "^1.28.1",
@ -27,8 +28,7 @@
"sqlite3": "https://github.com/bengotow/node-sqlite3/archive/bengotow/usleep-v3.1.4.tar.gz" "sqlite3": "https://github.com/bengotow/node-sqlite3/archive/bengotow/usleep-v3.1.4.tar.gz"
}, },
"scripts": { "scripts": {
"start": "pm2 kill && pm2 start ./pm2-dev.yml --watch && pm2 logs --raw | bunyan -o short", "start": "pm2 start ./pm2-dev.yml --no-daemon",
"logs": "pm2 logs --raw | bunyan -o short",
"stop": "pm2 kill", "stop": "pm2 kill",
"postinstall": "lerna bootstrap" "postinstall": "lerna bootstrap"
}, },

View file

@ -14,6 +14,10 @@ const {DatabaseConnector, SchedulerUtils, Logger} = require(`nylas-core`);
global.Promise = require('bluebird'); global.Promise = require('bluebird');
global.Logger = Logger.createLogger('nylas-k2-api') global.Logger = Logger.createLogger('nylas-k2-api')
const onUnhandledError = (err) => global.Logger.fatal(err, 'Unhandled error')
process.on('uncaughtException', onUnhandledError)
process.on('unhandledRejection', onUnhandledError)
const server = new Hapi.Server({ const server = new Hapi.Server({
connections: { connections: {
router: { router: {

View file

@ -1,4 +1,5 @@
const Serialization = require('../serialization'); const Serialization = require('../serialization');
const {DatabaseConnector} = require('nylas-core');
module.exports = (server) => { module.exports = (server) => {
server.route({ server.route({
@ -21,4 +22,28 @@ module.exports = (server) => {
reply(Serialization.jsonStringify(account)); reply(Serialization.jsonStringify(account));
}, },
}); });
server.route({
method: 'DELETE',
path: '/account',
config: {
description: 'Deletes the current account and all data from the Nylas Cloud.',
notes: 'Notes go here',
tags: ['accounts'],
validate: {
params: {
},
},
},
handler: (request, reply) => {
const account = request.auth.credentials;
account.destroy().then((saved) =>
DatabaseConnector.destroyAccountDatabase(saved.id).then(() =>
reply(Serialization.jsonStringify({status: 'success'}))
)
).catch((err) => {
reply(err).code(500);
})
},
});
}; };

View file

@ -79,11 +79,7 @@ class DatabaseConnector {
const dbname = `a-${accountId}`; const dbname = `a-${accountId}`;
if (process.env.DB_HOSTNAME) { if (process.env.DB_HOSTNAME) {
const sequelize = new Sequelize(null, process.env.DB_USERNAME, process.env.DB_PASSWORD, { const sequelize = this._sequelizePoolForDatabase(null);
host: process.env.DB_HOSTNAME,
dialect: "mysql",
logging: false,
})
return sequelize.authenticate().then(() => return sequelize.authenticate().then(() =>
sequelize.query(`CREATE DATABASE \`${dbname}\``) sequelize.query(`CREATE DATABASE \`${dbname}\``)
); );
@ -91,6 +87,18 @@ class DatabaseConnector {
return Promise.resolve() return Promise.resolve()
} }
destroyAccountDatabase(accountId) {
const dbname = `a-${accountId}`;
if (process.env.DB_HOSTNAME) {
const sequelize = this._sequelizePoolForDatabase(null);
return sequelize.authenticate().then(() =>
sequelize.query(`CREATE DATABASE \`${dbname}\``)
);
}
fs.removeFileSync(path.join(STORAGE_DIR, `${dbname}.sqlite`));
return Promise.resolve()
}
_sequelizeForShared() { _sequelizeForShared() {
const sequelize = this._sequelizePoolForDatabase(`shared`); const sequelize = this._sequelizePoolForDatabase(`shared`);
const modelsPath = path.join(__dirname, 'models/shared'); const modelsPath = path.join(__dirname, 'models/shared');

View file

@ -17,7 +17,11 @@ module.exports = (db, sequelize) => {
}); });
} }
}) })
// TODO delete account from redis sequelize.addHook("afterDestroy", ({dataValues, $modelOptions}) => {
// sequelize.addHook("afterDelete", ({dataValues, $modelOptions}) => { if ($modelOptions.name.singular === 'account') {
// }) PubsubConnector.notifyAccount(dataValues.id, {
type: MessageTypes.ACCOUNT_DELETED,
});
}
})
} }

View file

@ -270,9 +270,11 @@ class IMAPConnection extends EventEmitter {
} }
end() { end() {
if (this._imap) {
this._imap.end();
this._imap = null;
}
this._queue = []; this._queue = [];
this._imap.end();
this._imap = null;
this._connectPromise = null; this._connectPromise = null;
} }

View file

@ -1,21 +1,30 @@
const os = require('os');
const bunyan = require('bunyan') const bunyan = require('bunyan')
const createCWStream = require('bunyan-cloudwatch') const createCWStream = require('bunyan-cloudwatch')
const PrettyStream = require('bunyan-prettystream');
const NODE_ENV = process.env.NODE_ENV || 'unknown' const NODE_ENV = process.env.NODE_ENV || 'unknown'
function getLogStreams(name, env) { function getLogStreams(name, env) {
if (env === 'development') {
const prettyStdOut = new PrettyStream();
prettyStdOut.pipe(process.stdout);
const stdoutStream = {
type: 'raw',
level: 'debug',
stream: prettyStdOut,
}
return [stdoutStream]
}
const stdoutStream = { const stdoutStream = {
stream: process.stdout, stream: process.stdout,
level: 'info', level: 'info',
} }
if (env === 'development') {
return [stdoutStream]
}
const cloudwatchStream = { const cloudwatchStream = {
stream: createCWStream({ stream: createCWStream({
logGroupName: `k2-${env}`, logGroupName: `k2-${env}`,
logStreamName: `${name}-${env}`, logStreamName: `${name}-${env}-${os.hostname()}`,
cloudWatchLogsOptions: { cloudWatchLogsOptions: {
region: 'us-east-1', region: 'us-east-1',
}, },

View file

@ -1,5 +1,6 @@
module.exports = { module.exports = {
ACCOUNT_CREATED: "ACCOUNT_CREATED", ACCOUNT_CREATED: "ACCOUNT_CREATED",
ACCOUNT_UPDATED: "ACCOUNT_UPDATED", ACCOUNT_UPDATED: "ACCOUNT_UPDATED",
ACCOUNT_DELETED: "ACCOUNT_DELETED",
SYNCBACK_REQUESTED: "SYNCBACK_REQUESTED", SYNCBACK_REQUESTED: "SYNCBACK_REQUESTED",
} }

View file

@ -4,17 +4,36 @@ const ReactDOM = window.ReactDOM;
class SyncGraph extends React.Component { class SyncGraph extends React.Component {
componentDidMount() { componentDidMount() {
this.drawGraph();
}
componentDidUpdate() {
this.drawGraph(true); this.drawGraph(true);
} }
drawGraph(isUpdate) { componentDidUpdate() {
this.drawGraph(false);
}
drawGraph(isInitial) {
const now = Date.now(); const now = Date.now();
const config = SyncGraph.config; const config = SyncGraph.config;
const context = ReactDOM.findDOMNode(this).getContext('2d'); const node = ReactDOM.findDOMNode(this);
const context = node.getContext('2d');
if (isInitial) {
const totalHeight = config.height + config.labelFontSize + config.labelTopMargin;
node.width = config.width * 2;
node.height = totalHeight * 2;
node.style.width = `${config.width}px`;
node.style.height = `${totalHeight}px`;
context.scale(2, 2);
// Axis labels
context.fillStyle = config.labelColor;
context.font = `${config.labelFontSize}px sans-serif`;
const fontY = config.height + config.labelFontSize + config.labelTopMargin;
const nowText = "now";
const nowWidth = context.measureText(nowText).width;
context.fillText(nowText, config.width - nowWidth - 1, fontY);
context.fillText("-30m", 1, fontY);
}
// Background // Background
// (This hides any previous data points, so we don't have to clear the canvas) // (This hides any previous data points, so we don't have to clear the canvas)
@ -25,6 +44,7 @@ class SyncGraph extends React.Component {
const pxPerSec = config.width / config.timeLength; const pxPerSec = config.width / config.timeLength;
context.strokeStyle = config.dataColor; context.strokeStyle = config.dataColor;
context.beginPath(); context.beginPath();
for (const syncTimeMs of this.props.syncTimestamps) { for (const syncTimeMs of this.props.syncTimestamps) {
const secsAgo = (now - syncTimeMs) / 1000; const secsAgo = (now - syncTimeMs) / 1000;
const pxFromRight = secsAgo * pxPerSec; const pxFromRight = secsAgo * pxPerSec;
@ -43,17 +63,6 @@ class SyncGraph extends React.Component {
context.lineTo(px, config.height); context.lineTo(px, config.height);
} }
context.stroke(); context.stroke();
// Axis labels
if (!isUpdate) { // only draw these on the initial render
context.fillStyle = config.labelColor;
context.font = `${config.labelFontSize}px sans-serif`;
const fontY = config.height + config.labelFontSize + config.labelTopMargin;
const nowText = "now";
const nowWidth = context.measureText(nowText).width;
context.fillText(nowText, config.width - nowWidth - 1, fontY);
context.fillText("-30m", 1, fontY);
}
} }
render() { render() {
@ -82,7 +91,7 @@ SyncGraph.config = {
labelTopMargin: 2, labelTopMargin: 2,
labelColor: 'black', labelColor: 'black',
backgroundColor: 'black', backgroundColor: 'black',
dataColor: 'blue', dataColor: '#43a1ff',
} }
SyncGraph.propTypes = { SyncGraph.propTypes = {

View file

@ -5,6 +5,10 @@ const SyncProcessManager = require('./sync-process-manager');
global.Logger = Logger.createLogger('nylas-k2-sync') global.Logger = Logger.createLogger('nylas-k2-sync')
const onUnhandledError = (err) => global.Logger.fatal(err, 'Unhandled error')
process.on('uncaughtException', onUnhandledError)
process.on('unhandledRejection', onUnhandledError)
const manager = new SyncProcessManager(); const manager = new SyncProcessManager();
DatabaseConnector.forShared().then((db) => { DatabaseConnector.forShared().then((db) => {

View file

@ -61,13 +61,13 @@ class SyncProcessManager {
updateHeartbeat() { updateHeartbeat() {
const key = HEARTBEAT_FOR(IDENTITY); const key = HEARTBEAT_FOR(IDENTITY);
const client = PubsubConnector.broadcastClient(); const client = PubsubConnector.broadcastClient();
client.setAsync(key, Date.now()).then(() => client.setAsync(key, Date.now())
client.expireAsync(key, HEARTBEAT_EXPIRES) .then(() => client.expireAsync(key, HEARTBEAT_EXPIRES))
).then(() => .then(() => {
this._logger.info({ this._logger.info({
accounts_syncing_count: Object.keys(this._workers).length, accounts_syncing_count: Object.keys(this._workers).length,
}, "ProcessManager: 💘") }, "ProcessManager: 💘")
) })
} }
onSigInt() { onSigInt() {
@ -180,6 +180,7 @@ class SyncProcessManager {
return; return;
} }
this._logger.info({account_id: accountId}, `ProcessManager: Starting worker for Account`) this._logger.info({account_id: accountId}, `ProcessManager: Starting worker for Account`)
this._workers[account.id] = new SyncWorker(account, db, () => { this._workers[account.id] = new SyncWorker(account, db, () => {
this.removeWorkerForAccountId(accountId) this.removeWorkerForAccountId(accountId)
}); });

View file

@ -27,7 +27,6 @@ class SyncWorker {
this._logger = global.Logger.forAccount(account) this._logger = global.Logger.forAccount(account)
this._syncTimer = null; this._syncTimer = null;
this._expirationTimer = null;
this._destroyed = false; this._destroyed = false;
this.syncNow({reason: 'Initial'}); this.syncNow({reason: 'Initial'});
@ -37,6 +36,8 @@ class SyncWorker {
} }
cleanup() { cleanup() {
clearTimeout(this._syncTimer);
this._syncTimer = null;
this._destroyed = true; this._destroyed = true;
this._listener.dispose(); this._listener.dispose();
this.closeConnection() this.closeConnection()
@ -51,13 +52,19 @@ class SyncWorker {
_onMessage(msg) { _onMessage(msg) {
const {type} = JSON.parse(msg); const {type} = JSON.parse(msg);
switch (type) { switch (type) {
case MessageTypes.ACCOUNT_UPDATED:
this._onAccountUpdated(); break;
case MessageTypes.SYNCBACK_REQUESTED:
this.syncNow({reason: 'Syncback Action Queued'}); break;
case MessageTypes.ACCOUNT_CREATED: case MessageTypes.ACCOUNT_CREATED:
// No other processing currently required for account creation // No other processing currently required for account creation
break; break;
case MessageTypes.ACCOUNT_UPDATED:
this._onAccountUpdated();
break;
case MessageTypes.ACCOUNT_DELETED:
this.cleanup();
this._onExpired();
break;
case MessageTypes.SYNCBACK_REQUESTED:
this.syncNow({reason: 'Syncback Action Queued'});
break;
default: default:
this._logger.error({message: msg}, 'SyncWorker: Invalid message') this._logger.error({message: msg}, 'SyncWorker: Invalid message')
} }
@ -208,7 +215,7 @@ class SyncWorker {
const now = Date.now(); const now = Date.now();
const syncGraphTimeLength = 60 * 30; // 30 minutes, should be the same as SyncGraph.config.timeLength const syncGraphTimeLength = 60 * 30; // 30 minutes, should be the same as SyncGraph.config.timeLength
let lastSyncCompletions = [...this._account.lastSyncCompletions] let lastSyncCompletions = [].concat(this._account.lastSyncCompletions)
lastSyncCompletions = [now, ...lastSyncCompletions] lastSyncCompletions = [now, ...lastSyncCompletions]
while (now - lastSyncCompletions[lastSyncCompletions.length - 1] > 1000 * syncGraphTimeLength) { while (now - lastSyncCompletions[lastSyncCompletions.length - 1] > 1000 * syncGraphTimeLength) {
lastSyncCompletions.pop(); lastSyncCompletions.pop();

View file

@ -1,8 +1,4 @@
apps: apps:
- script : redis-server
name : redis
- script : packages/nylas-api/app.js - script : packages/nylas-api/app.js
watch : ["packages"] watch : ["packages"]
name : api name : api