Skip to content
This repository was archived by the owner on Jun 26, 2020. It is now read-only.

Commit 8bc2ec5

Browse files
committed
[client-app] (deltas P4) Fixup DeltaStramingConnection + retry on close
Summary: This commit completely refactors `DeltaStreamingConnection`, notably introducing the following changes: - Right now, `AccountDeltaConnection` establishes both delta connections to the cloud api and to the `client-sync` database (K2). This class is meant to disapper in favor of splitting into two different classes meant for syncing with the n1Cloud api and the local database. Specifically, `DeltaStreamingConnection`'s only responsibility is now to connect to the n1Cloud API and establish an http streaming connection for metadata deltas, etc. This class no longer unecessarily inherits from `NylasLongConnection`, which removes a lot of unecessary callback indirection. - The statuses of the n1Cloud delta streaming connections are now stored in as JSONBlobs in edgehill.db under new keys. This commit ensures that the data is correctly migrated from the old key (`NylasSyncWorker:<account_id>`). - The `DeltaStreamingConnection` now correctly retries when closed or errors. This logic previously existed, but was removed during the K2 transition: https://github.com/nylas/nylas-mail/blob/n1-pro/internal_packages/worker-sync/lib/nylas-sync-worker.coffee#L67 - Delta connection retries now backoff using the `ExponentialBackoffScheduler` - Attempt to restore the delta connection whenever the app comes back online Depends on D4119 Test Plan: manual + planned unit tests in upcoming diff Reviewers: halla, mark, evan, spang Reviewed By: evan Differential Revision: https://phab.nylas.com/D4120
1 parent b4d3da1 commit 8bc2ec5

File tree

6 files changed

+182
-48
lines changed

6 files changed

+182
-48
lines changed

packages/client-app/internal_packages/deltas/lib/account-delta-connection.es6

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,14 @@ const BASE_RETRY_DELAY = 1000;
3535
*/
3636
export default class AccountDeltaConnection {
3737

38+
3839
constructor(account) {
40+
// TODO This class is in the process of being ripped apart, and replaced by
41+
// DeltaStreamingConnection, and will disappear in
42+
// the next diff, but for the purposes of making this diff smaller, I
43+
// haven't removed it yet.
44+
this._n1CloudConn = new DeltaStreamingConnection(account)
45+
3946
this._state = { deltaCursors: {}, deltaStatus: {} }
4047
this.retryDelay = BASE_RETRY_DELAY;
4148
this._writeStateDebounced = _.debounce(this._writeState, 100)
@@ -77,6 +84,7 @@ export default class AccountDeltaConnection {
7784

7885
start = () => {
7986
try {
87+
this._n1CloudConn.start()
8088
this._refreshingCaches.map(c => c.start());
8189
_.map(this._deltaStreams, s => s.start())
8290
} catch (err) {
@@ -92,11 +100,7 @@ export default class AccountDeltaConnection {
92100

93101
_setupDeltaStreams = (account) => {
94102
const localSync = new DeltaStreamingInMemoryConnection(account.id, this._deltaStreamOpts("localSync"));
95-
96-
const n1Cloud = new DeltaStreamingConnection(N1CloudAPI,
97-
account.id, this._deltaStreamOpts("n1Cloud"));
98-
99-
return {localSync, n1Cloud};
103+
return {localSync};
100104
}
101105

102106
_deltaStreamOpts = (streamName) => {

packages/client-app/internal_packages/deltas/lib/delta-streaming-connection.es6

Lines changed: 161 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,179 @@
11
import _ from 'underscore'
2-
import {NylasLongConnection, DatabaseStore} from 'nylas-exports'
3-
4-
class DeltaStreamingConnection extends NylasLongConnection {
5-
constructor(api, accountId, opts = {}) {
6-
// TODO FYI this whole class is changing in an upcoming diff
7-
opts.api = api
8-
opts.accountId = accountId
9-
opts.throttleResultsInterval = 1000
10-
opts.closeIfDataStopsInterval = 15 * 1000
11-
12-
// Update cursor when deltas received
13-
opts.onResuls = (deltas = []) => {
14-
if (opts.onDeltas) opts.onDeltas(deltas, {source: "n1Cloud"});
15-
const last = _.last(deltas);
16-
if (last && last.cursor) {
17-
this._setCursor(last.cursor)
2+
import {ExponentialBackoffScheduler} from 'isomorphic-core'
3+
import {
4+
Actions,
5+
Account,
6+
APIError,
7+
N1CloudAPI,
8+
DatabaseStore,
9+
OnlineStatusStore,
10+
NylasLongConnection,
11+
} from 'nylas-exports';
12+
import DeltaProcessor from './delta-processor'
13+
14+
15+
const MAX_RETRY_DELAY = 5 * 60 * 1000; // 5 minutes
16+
const BASE_RETRY_DELAY = 1000;
17+
18+
class DeltaStreamingConnection {
19+
constructor(account) {
20+
this._account = account
21+
this._state = {cursor: null, status: null}
22+
this._longConnection = null
23+
this._writeStateDebounced = _.debounce(this._writeState, 100)
24+
this._unsubscribers = []
25+
this._backoffScheduler = new ExponentialBackoffScheduler({
26+
baseDelay: BASE_RETRY_DELAY,
27+
maxDelay: MAX_RETRY_DELAY,
28+
})
29+
30+
this._setupListeners()
31+
NylasEnv.onBeforeUnload = (readyToUnload) => {
32+
this._writeState().finally(readyToUnload)
33+
}
34+
}
35+
36+
start() {
37+
try {
38+
const {cursor = 0} = this._state
39+
this._longConnection = new NylasLongConnection({
40+
api: N1CloudAPI,
41+
accountId: this._account.id,
42+
path: `/delta/streaming?cursor=${cursor}`,
43+
throttleResultsInterval: 1000,
44+
closeIfDataStopsInterval: 15 * 1000,
45+
onError: this._onError,
46+
onResults: this._onResults,
47+
onStatusChanged: this._onStatusChanged,
48+
})
49+
this._longConnection.start()
50+
} catch (err) {
51+
this._onError(err)
52+
}
53+
}
54+
55+
restart() {
56+
try {
57+
this._restarting = true
58+
this.close();
59+
this._disposeListeners()
60+
this._setupListeners()
61+
this.start();
62+
} finally {
63+
this._restarting = false
64+
}
65+
}
66+
67+
close() {
68+
this._disposeListeners()
69+
this._longConnection.close()
70+
}
71+
72+
end() {
73+
this._disposeListeners()
74+
this._longConnection.end()
75+
}
76+
77+
async loadStateFromDatabase() {
78+
let json = await DatabaseStore.findJSONBlob(`DeltaStreamingConnectionStatus:${this._account.id}`)
79+
80+
if (!json) {
81+
// Migrate from old storage key
82+
const oldState = await DatabaseStore.findJSONBlob(`NylasSyncWorker:${this._account.id}`)
83+
if (!oldState) { return; }
84+
const {deltaCursors = {}, deltaStatus = {}} = oldState
85+
json = {
86+
cursor: deltaCursors.n1Cloud || null,
87+
status: deltaStatus.n1Cloud || null,
1888
}
1989
}
20-
super(opts)
2190

22-
this._onError = opts.onError || (() => {})
91+
if (!json) { return }
92+
this._state = json;
93+
}
2394

24-
const {getCursor, setCursor} = opts
25-
this._getCursor = getCursor
26-
this._setCursor = setCursor
95+
_setupListeners() {
96+
this._unsubscribers = [
97+
Actions.retryDeltaConnection.listen(this.restart, this),
98+
OnlineStatusStore.listen(this._onOnlineStatusChanged, this),
99+
]
27100
}
28101

29-
_deltaStreamingPath(cursor) {
30-
return `/delta/streaming?cursor=${cursor}`
102+
_disposeListeners() {
103+
this._unsubscribers.forEach(usub => usub())
104+
this._unsubscribers = []
31105
}
32106

33-
onError(err = {}) {
107+
_writeState() {
108+
return DatabaseStore.inTransaction(t =>
109+
t.persistJSONBlob(`DeltaStreamingConnectionStatus:${this._account.id}`, this._state)
110+
);
111+
}
112+
113+
_setCursor = (cursor) => {
114+
this._state.cursor = cursor;
115+
this._writeStateDebounced();
116+
}
117+
118+
_onOnlineStatusChanged = () => {
119+
if (OnlineStatusStore.isOnline()) {
120+
this.restart()
121+
}
122+
}
123+
124+
_onStatusChanged = (status) => {
125+
if (this._restarting) { return; }
126+
this._state.status = status;
127+
this._writeStateDebounced();
128+
const {Closed, Connected} = NylasLongConnection.Status
129+
if (status === Connected) {
130+
this._backoffScheduler.reset()
131+
}
132+
if (status === Closed) {
133+
setTimeout(() => this.restart(), this._backoffScheduler.nextDelay());
134+
}
135+
}
136+
137+
_onResults = (deltas = []) => {
138+
this._backoffScheduler.reset()
139+
140+
const last = _.last(deltas);
141+
if (last && last.cursor) {
142+
this._setCursor(last.cursor)
143+
}
144+
DeltaProcessor.process(deltas, {source: 'n1Cloud'})
145+
}
146+
147+
_onError = (err = {}) => {
34148
if (err.message && err.message.includes('Invalid cursor')) {
35-
const error = new Error('Delta Connection: Cursor is invalid. Need to blow away local cache.');
149+
// TODO is this still necessary?
150+
const error = new Error('DeltaStreamingConnection: Cursor is invalid. Need to blow away local cache.');
36151
NylasEnv.reportError(error)
37152
this._setCursor(0)
38153
DatabaseStore._handleSetupError(error)
154+
return
39155
}
40-
this._onError(err)
41-
}
42156

43-
start() {
44-
this._path = this._deltaStreamingPath(this._getCursor() || 0)
45-
super.start()
157+
if (err instanceof APIError && err.statusCode === 401) {
158+
Actions.updateAccount(this._account.id, {
159+
syncState: Account.SYNC_STATE_AUTH_FAILED,
160+
syncError: err.toJSON(),
161+
})
162+
}
163+
164+
err.message = `Error connecting to delta stream: ${err.message}`
165+
const ignorableStatusCodes = [
166+
0, // When errors like ETIMEDOUT, ECONNABORTED or ESOCKETTIMEDOUT occur from the client
167+
404, // Don't report not-founds
168+
408, // Timeout error code
169+
429, // Too many requests
170+
]
171+
if (!ignorableStatusCodes.includes(err.statusCode)) {
172+
NylasEnv.reportError(err)
173+
}
174+
this.close()
175+
176+
setTimeout(() => this.restart(), this._backoffScheduler.nextDelay());
46177
}
47178
}
48179

packages/client-app/internal_packages/deltas/spec/account-delta-connection-spec.coffee

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ _ = require 'underscore'
33
DeltaStreamingConnection = require('../lib/delta-streaming-connection').default
44
AccountDeltaConnection = require('../lib/account-delta-connection').default
55

6+
# TODO these are badly out of date, we need to rewrite them
67
xdescribe "AccountDeltaConnection", ->
78
beforeEach ->
89
@apiRequests = []

packages/client-app/internal_packages/worker-ui/lib/developer-bar-store.coffee

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ class DeveloperBarStore extends NylasStore
9191
_onDeltaConnectionStatusChanged: ->
9292
@_longPollStates = {}
9393
_.forEach DeltaConnectionStatusStore.getDeltaConnectionStates(), (state, accountId) =>
94-
@_longPollStates[accountId] = state.deltaStatus
94+
@_longPollStates[accountId] = state.status
9595
@trigger()
9696

9797
_onLongPollDeltas: (deltas) ->

packages/client-app/internal_packages/worker-ui/lib/developer-bar.cjsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,9 +70,9 @@ class DeveloperBar extends React.Component
7070
</div>
7171

7272
_renderDeltaStates: =>
73-
_.map @state.longPollStates, (deltaStatus, accountId) =>
73+
_.map @state.longPollStates, (status, accountId) =>
7474
<div className="delta-state-wrap" key={accountId} >
75-
<div title={"Account #{accountId} - Cloud State: #{deltaStatus?.n1Cloud}"} key={"#{accountId}-n1Cloud"} className={"activity-status-bubble state-" + deltaStatus?.n1Cloud}></div>
75+
<div title={"Account #{accountId} - Cloud State: #{status}"} key={"#{accountId}-n1Cloud"} className={"activity-status-bubble state-" + status}></div>
7676
</div>
7777

7878
_sectionContent: =>

packages/client-app/src/flux/stores/delta-connection-status-store.es6

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,8 @@ import DatabaseStore from './database-store'
1212
* The sync state for any given account has the following shape:
1313
*
1414
* {
15-
* deltaCursors: {
16-
* localSync,
17-
* n1Cloud,
18-
* },
19-
* deltaStatus: {
20-
* localSync,
21-
* n1Cloud,
22-
* },
15+
* cursor: 0,
16+
* status: 'connected',
2317
* }
2418
*
2519
*/
@@ -44,9 +38,13 @@ class DeltaConnectionStatusStore extends NylasStore {
4438
_setupAccountSubscriptions(accountIds) {
4539
accountIds.forEach((accountId) => {
4640
if (this._accountSubscriptions.has(accountId)) { return; }
47-
const query = DatabaseStore.findJSONBlob(`NylasSyncWorker:${accountId}`)
41+
const query = DatabaseStore.findJSONBlob(`DeltaStreamingConnectionStatus:${accountId}`)
4842
const sub = Rx.Observable.fromQuery(query)
49-
.subscribe((json) => this._updateState(accountId, json))
43+
.subscribe((json) => {
44+
// We need to copy `json` otherwise the query observable will mutate
45+
// the reference to that object
46+
this._updateState(accountId, {...json})
47+
})
5048
this._accountSubscriptions.set(accountId, sub)
5149
})
5250
}

0 commit comments

Comments
 (0)