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

View file

@ -14,6 +14,10 @@ const {DatabaseConnector, SchedulerUtils, Logger} = require(`nylas-core`);
global.Promise = require('bluebird');
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({
connections: {
router: {

View file

@ -1,4 +1,5 @@
const Serialization = require('../serialization');
const {DatabaseConnector} = require('nylas-core');
module.exports = (server) => {
server.route({
@ -21,4 +22,28 @@ module.exports = (server) => {
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}`;
if (process.env.DB_HOSTNAME) {
const sequelize = new Sequelize(null, process.env.DB_USERNAME, process.env.DB_PASSWORD, {
host: process.env.DB_HOSTNAME,
dialect: "mysql",
logging: false,
})
const sequelize = this._sequelizePoolForDatabase(null);
return sequelize.authenticate().then(() =>
sequelize.query(`CREATE DATABASE \`${dbname}\``)
);
@ -91,6 +87,18 @@ class DatabaseConnector {
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() {
const sequelize = this._sequelizePoolForDatabase(`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("afterDelete", ({dataValues, $modelOptions}) => {
// })
sequelize.addHook("afterDestroy", ({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() {
if (this._imap) {
this._imap.end();
this._imap = null;
}
this._queue = [];
this._imap.end();
this._imap = null;
this._connectPromise = null;
}

View file

@ -1,21 +1,30 @@
const os = require('os');
const bunyan = require('bunyan')
const createCWStream = require('bunyan-cloudwatch')
const PrettyStream = require('bunyan-prettystream');
const NODE_ENV = process.env.NODE_ENV || 'unknown'
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 = {
stream: process.stdout,
level: 'info',
}
if (env === 'development') {
return [stdoutStream]
}
const cloudwatchStream = {
stream: createCWStream({
logGroupName: `k2-${env}`,
logStreamName: `${name}-${env}`,
logStreamName: `${name}-${env}-${os.hostname()}`,
cloudWatchLogsOptions: {
region: 'us-east-1',
},

View file

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

View file

@ -4,17 +4,36 @@ const ReactDOM = window.ReactDOM;
class SyncGraph extends React.Component {
componentDidMount() {
this.drawGraph();
}
componentDidUpdate() {
this.drawGraph(true);
}
drawGraph(isUpdate) {
componentDidUpdate() {
this.drawGraph(false);
}
drawGraph(isInitial) {
const now = Date.now();
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
// (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;
context.strokeStyle = config.dataColor;
context.beginPath();
for (const syncTimeMs of this.props.syncTimestamps) {
const secsAgo = (now - syncTimeMs) / 1000;
const pxFromRight = secsAgo * pxPerSec;
@ -43,17 +63,6 @@ class SyncGraph extends React.Component {
context.lineTo(px, config.height);
}
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() {
@ -82,7 +91,7 @@ SyncGraph.config = {
labelTopMargin: 2,
labelColor: 'black',
backgroundColor: 'black',
dataColor: 'blue',
dataColor: '#43a1ff',
}
SyncGraph.propTypes = {

View file

@ -5,6 +5,10 @@ const SyncProcessManager = require('./sync-process-manager');
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();
DatabaseConnector.forShared().then((db) => {

View file

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

View file

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

View file

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