mirror of
https://github.com/Foundry376/Mailspring.git
synced 2025-02-22 07:03:31 +08:00
fix(databse): fix memory leak on DatabaseStore.atomically
Summary: The Promise chain we were creating was never cleared and created a memory leak. We instead use a `PromiseQueue` to cleanup finished promises. Also added several more tests and verified that the memory leak is gone with the Chrome profiler Test Plan: new tests Reviewers: bengotow Reviewed By: bengotow Differential Revision: https://phab.nylas.com/D2184
This commit is contained in:
parent
cfb4142471
commit
ef8e7aaf51
4 changed files with 78 additions and 10 deletions
|
@ -50,6 +50,7 @@
|
||||||
"optimist": "0.4.0",
|
"optimist": "0.4.0",
|
||||||
"pathwatcher": "^4.4.0",
|
"pathwatcher": "^4.4.0",
|
||||||
"property-accessors": "^1",
|
"property-accessors": "^1",
|
||||||
|
"promise-queue": "2.1.1",
|
||||||
"q": "^1.0.1",
|
"q": "^1.0.1",
|
||||||
"raven": "0.7.2",
|
"raven": "0.7.2",
|
||||||
"react": "^0.13.2",
|
"react": "^0.13.2",
|
||||||
|
|
|
@ -343,13 +343,22 @@ describe "DatabaseStore", ->
|
||||||
expect(@performed[1].query).toBe "TEST"
|
expect(@performed[1].query).toBe "TEST"
|
||||||
expect(@performed[2].query).toBe "COMMIT"
|
expect(@performed[2].query).toBe "COMMIT"
|
||||||
|
|
||||||
it "resolves, but doesn't fire a commit on failure", ->
|
it "preserves resolved values", ->
|
||||||
|
waitsForPromise =>
|
||||||
|
DatabaseStore.atomically( =>
|
||||||
|
DatabaseStore._query("TEST")
|
||||||
|
return Promise.resolve("myValue")
|
||||||
|
).then (myValue) =>
|
||||||
|
expect(myValue).toBe "myValue"
|
||||||
|
|
||||||
|
it "always fires a COMMIT, even if the promise fails", ->
|
||||||
waitsForPromise =>
|
waitsForPromise =>
|
||||||
DatabaseStore.atomically( =>
|
DatabaseStore.atomically( =>
|
||||||
throw new Error("BOOO")
|
throw new Error("BOOO")
|
||||||
).catch =>
|
).catch =>
|
||||||
expect(@performed.length).toBe 1
|
expect(@performed.length).toBe 2
|
||||||
expect(@performed[0].query).toBe "BEGIN EXCLUSIVE TRANSACTION"
|
expect(@performed[0].query).toBe "BEGIN EXCLUSIVE TRANSACTION"
|
||||||
|
expect(@performed[1].query).toBe "COMMIT"
|
||||||
|
|
||||||
it "can be called multiple times and get queued", ->
|
it "can be called multiple times and get queued", ->
|
||||||
waitsForPromise =>
|
waitsForPromise =>
|
||||||
|
@ -366,6 +375,64 @@ describe "DatabaseStore", ->
|
||||||
expect(@performed[4].query).toBe "BEGIN EXCLUSIVE TRANSACTION"
|
expect(@performed[4].query).toBe "BEGIN EXCLUSIVE TRANSACTION"
|
||||||
expect(@performed[5].query).toBe "COMMIT"
|
expect(@performed[5].query).toBe "COMMIT"
|
||||||
|
|
||||||
|
it "carries on if one of them fails, but still calls the COMMIT for the failed block", ->
|
||||||
|
caughtError = false
|
||||||
|
DatabaseStore.atomically( => DatabaseStore._query("ONE") )
|
||||||
|
DatabaseStore.atomically( => throw new Error("fail") ).catch ->
|
||||||
|
caughtError = true
|
||||||
|
DatabaseStore.atomically( => DatabaseStore._query("THREE") )
|
||||||
|
advanceClock(100)
|
||||||
|
expect(@performed.length).toBe 8
|
||||||
|
expect(@performed[0].query).toBe "BEGIN EXCLUSIVE TRANSACTION"
|
||||||
|
expect(@performed[1].query).toBe "ONE"
|
||||||
|
expect(@performed[2].query).toBe "COMMIT"
|
||||||
|
expect(@performed[3].query).toBe "BEGIN EXCLUSIVE TRANSACTION"
|
||||||
|
expect(@performed[4].query).toBe "COMMIT"
|
||||||
|
expect(@performed[5].query).toBe "BEGIN EXCLUSIVE TRANSACTION"
|
||||||
|
expect(@performed[6].query).toBe "THREE"
|
||||||
|
expect(@performed[7].query).toBe "COMMIT"
|
||||||
|
expect(caughtError).toBe true
|
||||||
|
|
||||||
|
it "is actually running in series and blocks on never-finishing specs", ->
|
||||||
|
resolver = null
|
||||||
|
DatabaseStore.atomically( -> )
|
||||||
|
advanceClock(100)
|
||||||
|
expect(@performed.length).toBe 2
|
||||||
|
expect(@performed[0].query).toBe "BEGIN EXCLUSIVE TRANSACTION"
|
||||||
|
expect(@performed[1].query).toBe "COMMIT"
|
||||||
|
DatabaseStore.atomically( -> new Promise (resolve, reject) -> resolver = resolve)
|
||||||
|
advanceClock(100)
|
||||||
|
blockedPromiseDone = false
|
||||||
|
DatabaseStore.atomically( -> ).then =>
|
||||||
|
blockedPromiseDone = true
|
||||||
|
advanceClock(100)
|
||||||
|
expect(@performed.length).toBe 3
|
||||||
|
expect(@performed[2].query).toBe "BEGIN EXCLUSIVE TRANSACTION"
|
||||||
|
expect(blockedPromiseDone).toBe false
|
||||||
|
|
||||||
|
# Now that we've made our assertion about blocking, we need to clean up
|
||||||
|
# our test and actually resolve that blocked promise now, otherwise
|
||||||
|
# remaining tests won't run properly.
|
||||||
|
advanceClock(100)
|
||||||
|
resolver()
|
||||||
|
advanceClock(100)
|
||||||
|
expect(blockedPromiseDone).toBe true
|
||||||
|
advanceClock(100)
|
||||||
|
|
||||||
|
it "can be called multiple times and preserve return values", ->
|
||||||
|
waitsForPromise =>
|
||||||
|
v1 = null
|
||||||
|
v2 = null
|
||||||
|
v3 = null
|
||||||
|
Promise.all([
|
||||||
|
DatabaseStore.atomically( -> "a" ).then (val) -> v1 = val
|
||||||
|
DatabaseStore.atomically( -> "b" ).then (val) -> v2 = val
|
||||||
|
DatabaseStore.atomically( -> "c" ).then (val) -> v3 = val
|
||||||
|
]).then =>
|
||||||
|
expect(v1).toBe "a"
|
||||||
|
expect(v2).toBe "b"
|
||||||
|
expect(v3).toBe "c"
|
||||||
|
|
||||||
it "can be called multiple times and get queued", ->
|
it "can be called multiple times and get queued", ->
|
||||||
waitsForPromise =>
|
waitsForPromise =>
|
||||||
DatabaseStore.atomically( -> )
|
DatabaseStore.atomically( -> )
|
||||||
|
|
|
@ -210,7 +210,6 @@ class APMWrapper
|
||||||
if code is 0
|
if code is 0
|
||||||
callback?()
|
callback?()
|
||||||
else
|
else
|
||||||
atom.packages.activatePackage(name) if activateOnFailure
|
|
||||||
error = new Error(errorMessage)
|
error = new Error(errorMessage)
|
||||||
error.stdout = stdout
|
error.stdout = stdout
|
||||||
error.stderr = stderr
|
error.stderr = stderr
|
||||||
|
|
|
@ -8,6 +8,7 @@ Utils = require '../models/utils'
|
||||||
Actions = require '../actions'
|
Actions = require '../actions'
|
||||||
ModelQuery = require '../models/query'
|
ModelQuery = require '../models/query'
|
||||||
NylasStore = require '../../global/nylas-store'
|
NylasStore = require '../../global/nylas-store'
|
||||||
|
PromiseQueue = require 'promise-queue'
|
||||||
DatabaseSetupQueryBuilder = require './database-setup-query-builder'
|
DatabaseSetupQueryBuilder = require './database-setup-query-builder'
|
||||||
PriorityUICoordinator = require '../../priority-ui-coordinator'
|
PriorityUICoordinator = require '../../priority-ui-coordinator'
|
||||||
|
|
||||||
|
@ -468,18 +469,18 @@ class DatabaseStore extends NylasStore
|
||||||
Promise.resolve(data)
|
Promise.resolve(data)
|
||||||
|
|
||||||
atomically: (fn) =>
|
atomically: (fn) =>
|
||||||
@_atomicPromise ?= Promise.resolve()
|
maxConcurrent = 1
|
||||||
@_atomicPromise = @_atomicPromise.then =>
|
maxQueue = Infinity
|
||||||
@_atomically(fn)
|
@_promiseQueue ?= new PromiseQueue(maxConcurrent, maxQueue)
|
||||||
return @_atomicPromise
|
return @_promiseQueue.add(=> @_atomically(fn))
|
||||||
|
|
||||||
_atomically: (fn) ->
|
_atomically: (fn) ->
|
||||||
@_query("BEGIN EXCLUSIVE TRANSACTION")
|
@_query("BEGIN EXCLUSIVE TRANSACTION")
|
||||||
.then =>
|
.then =>
|
||||||
fn()
|
fn()
|
||||||
.then (val) =>
|
.finally (val) => @_query("COMMIT")
|
||||||
@_query("COMMIT").then =>
|
# NOTE: The value that `fn` resolves to is propagated all the way back to
|
||||||
Promise.resolve(val)
|
# the originally caller of `atomically`
|
||||||
|
|
||||||
########################################################################
|
########################################################################
|
||||||
########################### PRIVATE METHODS ############################
|
########################### PRIVATE METHODS ############################
|
||||||
|
|
Loading…
Reference in a new issue