refactor(database): Open only one sqlite connection, run requests from Browser process

Summary: This diff is the beginning of a larger refactor to move lots of stuff to the Browser process!

Test Plan: No tests

Reviewers: evan

Reviewed By: evan

Differential Revision: https://review.inboxapp.com/D1156
This commit is contained in:
Ben Gotow 2015-02-06 14:47:10 -08:00
parent e4889b390f
commit a54f4ed3ba
2 changed files with 92 additions and 35 deletions

View file

@ -70,6 +70,7 @@ class AtomApplication
@pidsToOpenWindows = {}
@mainWindow = null
@windows = []
@databases = {}
@autoUpdateManager = new AutoUpdateManager(@version)
@applicationMenu = new ApplicationMenu(@version)
@ -94,6 +95,64 @@ class AtomApplication
for urlToOpen in (urlsToOpen || [])
@openUrl({urlToOpen})
prepareDatabaseInterface: ->
return @dblitePromise if @dblitePromise
# configure a listener that watches for incoming queries over IPC,
# executes them, and returns the responses to the remote renderer processes
ipc.on 'database-query', (event, {databasePath, queryKey, query, values}) =>
db = @databases[databasePath]
done = (err, result) ->
unless err
runtime = db.lastQueryTime()
if runtime > 250
console.log("Query #{queryKey}: #{query} took #{runtime}msec")
event.sender.send('database-result', {queryKey, err, result})
return done(new Error("Database not prepared.")) unless db
if query[0..5] is 'SELECT'
db.query(query, values, null, done)
else
db.query(query, values, done)
# return a promise that resolves after we've configured dblite for our platform
return @dblitePromise = new Promise (resolve, reject) =>
dblite = require('../../vendor/dblite-custom').withSQLite('3.8.6+')
vendor = @resourcePath + "/vendor"
if process.platform is 'win32'
dblite.bin = "#{vendor}/sqlite3-win32.exe"
resolve(dblite)
else if process.platform is 'linux'
exec "uname -a", (err, stdout, stderr) ->
arch = if stdout.toString().indexOf('x86_64') is -1 then "32" else "64"
dblite.bin = "#{vendor}/sqlite3-linux-#{arch}"
resolve(dblite)
else if process.platform is 'darwin'
dblite.bin = "#{vendor}/sqlite3-darwin"
resolve(dblite)
prepareDatabase: (databasePath, callback) ->
@prepareDatabaseInterface().then (dblite) =>
# Avoid opening a new connection to an existing database
return callback() if @databases[databasePath]
# Create a new database for the requested path
db = dblite(databasePath)
# By default, dblite stops all query execution when a query returns an error.
# We want to propogate those errors out, but still allow queries to be made.
db.ignoreErrors = true
@databases[databasePath] = db
# Tell the person who requested the database that they can begin making queries
callback()
teardownDatabase: (databasePath, callback) ->
@databases[databasePath]?.close()
delete @databases[databasePath]
fs.unlink(databasePath, callback)
# Public: Removes the {AtomWindow} from the global window list.
removeWindow: (window) ->
@windows.splice @windows.indexOf(window), 1

View file

@ -1,5 +1,6 @@
Reflux = require 'reflux'
async = require 'async'
remote = require 'remote'
_ = require 'underscore-plus'
Actions = require '../actions'
Model = require '../models/model'
@ -10,10 +11,35 @@ ModelQuery = require '../models/query'
fs = require 'fs-plus'
path = require 'path'
exec = require('child_process').exec
ipc = require 'ipc'
silent = atom.getLoadSettings().isSpec
verbose = false
# DatabaseConnection is a small shim for making database queries. Queries
# are actually executed in the Browser process and eventually, we'll move
# more and more of this class there.
class DatabaseProxy
constructor: (@databasePath) ->
@windowId = remote.getCurrentWindow().id
@queryCallbacks = {}
@queryId = 0
ipc.on 'database-result', ({queryKey, err, result}) =>
@queryCallbacks[queryKey](err, result) if @queryCallbacks[queryKey]
delete @queryCallbacks[queryKey]
@
query: (query, values, callback) ->
@queryId += 1
queryKey = "#{@windowId}-#{@queryId}"
@queryCallbacks[queryKey] = callback if callback
ipc.send('database-query', {@databasePath, queryKey, query, values})
# DatabasePromiseTransaction converts the callback syntax of the Database
# into a promise syntax with nice features like serial execution of many
# queries in the same promise.
class DatabasePromiseTransaction
constructor: (@_db, @_resolve, @_reject) ->
@_running = 0
@ -22,17 +48,9 @@ class DatabasePromiseTransaction
# Wrap any user-provided success callback in one that checks query time
callback = (err, result) =>
if err
if err.message.indexOf('database is locked') != -1
alert('Database lock error. You need to restart Edgehill (this is a known issue.)')
console.log("Query #{query}, #{JSON.stringify(values)} failed #{err.message}")
queryFailure(err) if queryFailure
@_reject(err)
else
runtime = @_db.lastQueryTime()
if (runtime > 250 or verbose) and not silent
console.log("Query: #{query} took #{runtime}msec")
querySuccess(result) if querySuccess
# The user can attach things to the finish promise to run code after
# the completion of all pending queries in the transaction. We fire
@ -43,17 +61,13 @@ class DatabasePromiseTransaction
@_resolve(result)
@_running += 1
if query[0..5] == 'SELECT'
@_db.query(query, values || [], null, callback)
else
@_db.query(query, values || [], callback)
@_db.query(query, values || [], callback)
executeInSeries: (queries) ->
async.eachSeries queries
, (query, callback) =>
@execute(query, [], -> callback())
, (err) =>
console.log(err) if err
@_resolve()
@ -83,26 +97,10 @@ DatabaseStore = Reflux.createStore
for key, klass of classMap
callback(klass) if klass.attributes
prepareSqlite: (callback) ->
dblite = require('../../../vendor/dblite-custom').withSQLite('3.8.6+')
vendor = atom.getLoadSettings().resourcePath + "/vendor"
if process.platform is 'win32'
dblite.bin = "#{vendor}/sqlite3-win32.exe"
callback(dblite)
else if process.platform is 'linux'
exec "uname -a", (err, stdout, stderr) ->
arch = if stdout.toString().indexOf('x86_64') is -1 then "32" else "64"
dblite.bin = "#{vendor}/sqlite3-linux-#{arch}"
callback(dblite)
else if process.platform is 'darwin'
dblite.bin = "#{vendor}/sqlite3-darwin"
callback(dblite)
openDatabase: (options = {createTables: false}) ->
@prepareSqlite (dblite) =>
# Open the database
database = dblite(@_dbPath)
app = remote.getGlobal('atomApplication')
app.prepareDatabase @_dbPath, =>
database = new DatabaseProxy(@_dbPath)
if options.createTables
# Initialize the database and setup our schema. Note that we try to do this every
@ -124,9 +122,9 @@ DatabaseStore = Reflux.createStore
@_db = database
teardownDatabase: (callback) ->
@_db?.close()
@_db = null
fs.unlink @_dbPath, (err) =>
app = remote.getGlobal('atomApplication')
app.teardownDatabase @_dbPath, =>
@_db = null
@trigger({})
callback()