Skip to content

Commit 88d0f2f

Browse files
committed
add backup api suitable for calling incrementally
This is based on TryGhost#883 It creates a backup object that can be kept around so that the backup can be performed in leisurely steps, with plenty of opportunity for interruption.
1 parent a9dc5a3 commit 88d0f2f

File tree

8 files changed

+703
-1
lines changed

8 files changed

+703
-1
lines changed

binding.gyp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
]
3333
],
3434
"sources": [
35+
"src/backup.cc",
3536
"src/database.cc",
3637
"src/node_sqlite3.cc",
3738
"src/statement.cc"

lib/sqlite3.js

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,11 @@ sqlite3.cached = {
5959

6060
var Database = sqlite3.Database;
6161
var Statement = sqlite3.Statement;
62+
var Backup = sqlite3.Backup;
6263

6364
inherits(Database, EventEmitter);
6465
inherits(Statement, EventEmitter);
66+
inherits(Backup, EventEmitter);
6567

6668
// Database#prepare(sql, [bind1, bind2, ...], [callback])
6769
Database.prototype.prepare = normalizeMethod(function(statement, params) {
@@ -99,6 +101,17 @@ Database.prototype.map = normalizeMethod(function(statement, params) {
99101
return this;
100102
});
101103

104+
// Database#backup(filename, [callback])
105+
// Database#backup(filename, destName, sourceName, filenameIsDest, [callback])
106+
Database.prototype.backup = function() {
107+
if (arguments.length <= 2) {
108+
// By default, we write the main database out to the named file.
109+
return new Backup(this, arguments[0], 'main', 'main', true, arguments[1]);
110+
} else {
111+
return new Backup(this, arguments[0], arguments[1], arguments[2], arguments[3], arguments[4]);
112+
}
113+
};
114+
102115
Statement.prototype.map = function() {
103116
var params = Array.prototype.slice.call(arguments);
104117
var callback = params.pop();
@@ -165,7 +178,8 @@ sqlite3.verbose = function() {
165178
'each',
166179
'map',
167180
'close',
168-
'exec'
181+
'exec',
182+
'backup',
169183
].forEach(function (name) {
170184
trace.extendTrace(Database.prototype, name);
171185
});
@@ -181,6 +195,12 @@ sqlite3.verbose = function() {
181195
].forEach(function (name) {
182196
trace.extendTrace(Statement.prototype, name);
183197
});
198+
[
199+
'step',
200+
'finish',
201+
].forEach(function (name) {
202+
trace.extendTrace(Backup.prototype, name);
203+
});
184204
isVerbose = true;
185205
}
186206

src/backup.cc

Lines changed: 322 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,322 @@
1+
#include <string.h>
2+
#include <node.h>
3+
#include <node_buffer.h>
4+
#include <node_version.h>
5+
6+
#include "macros.h"
7+
#include "database.h"
8+
#include "backup.h"
9+
10+
using namespace node_sqlite3;
11+
12+
Nan::Persistent<FunctionTemplate> Backup::constructor_template;
13+
14+
15+
NAN_MODULE_INIT(Backup::Init) {
16+
Nan::HandleScope scope;
17+
18+
Local<FunctionTemplate> t = Nan::New<FunctionTemplate>(New);
19+
20+
t->InstanceTemplate()->SetInternalFieldCount(1);
21+
t->SetClassName(Nan::New("Backup").ToLocalChecked());
22+
23+
Nan::SetPrototypeMethod(t, "step", Step);
24+
Nan::SetPrototypeMethod(t, "finish", Finish);
25+
26+
constructor_template.Reset(t);
27+
Nan::Set(target, Nan::New("Backup").ToLocalChecked(),
28+
Nan::GetFunction(t).ToLocalChecked());
29+
}
30+
31+
void Backup::Process() {
32+
if (finished && !queue.empty()) {
33+
return CleanQueue();
34+
}
35+
36+
while (inited && !locked && !queue.empty()) {
37+
Call* call = queue.front();
38+
queue.pop();
39+
40+
call->callback(call->baton);
41+
delete call;
42+
}
43+
}
44+
45+
void Backup::Schedule(Work_Callback callback, Baton* baton) {
46+
if (finished) {
47+
queue.push(new Call(callback, baton));
48+
CleanQueue();
49+
}
50+
else if (!inited || locked) {
51+
queue.push(new Call(callback, baton));
52+
}
53+
else {
54+
callback(baton);
55+
}
56+
}
57+
58+
template <class T> void Backup::Error(T* baton) {
59+
Nan::HandleScope scope;
60+
61+
Backup* backup = baton->backup;
62+
// Fail hard on logic errors.
63+
assert(backup->status != 0);
64+
EXCEPTION(backup->message, backup->status, exception);
65+
66+
Local<Function> cb = Nan::New(baton->callback);
67+
68+
if (!cb.IsEmpty() && cb->IsFunction()) {
69+
Local<Value> argv[] = { exception };
70+
TRY_CATCH_CALL(backup->handle(), cb, 1, argv);
71+
}
72+
else {
73+
Local<Value> argv[] = { Nan::New("error").ToLocalChecked(), exception };
74+
EMIT_EVENT(backup->handle(), 2, argv);
75+
}
76+
}
77+
78+
void Backup::CleanQueue() {
79+
Nan::HandleScope scope;
80+
81+
if (inited && !queue.empty()) {
82+
// This backup has already been initialized and is now finished.
83+
// Fire error for all remaining items in the queue.
84+
EXCEPTION("Backup is already finished", SQLITE_MISUSE, exception);
85+
Local<Value> argv[] = { exception };
86+
bool called = false;
87+
88+
// Clear out the queue so that this object can get GC'ed.
89+
while (!queue.empty()) {
90+
Call* call = queue.front();
91+
queue.pop();
92+
93+
Local<Function> cb = Nan::New(call->baton->callback);
94+
95+
if (inited && !cb.IsEmpty() &&
96+
cb->IsFunction()) {
97+
TRY_CATCH_CALL(handle(), cb, 1, argv);
98+
called = true;
99+
}
100+
101+
// We don't call the actual callback, so we have to make sure that
102+
// the baton gets destroyed.
103+
delete call->baton;
104+
delete call;
105+
}
106+
107+
// When we couldn't call a callback function, emit an error on the
108+
// Backup object.
109+
if (!called) {
110+
Local<Value> info[] = { Nan::New("error").ToLocalChecked(), exception };
111+
EMIT_EVENT(handle(), 2, info);
112+
}
113+
}
114+
else while (!queue.empty()) {
115+
// Just delete all items in the queue; we already fired an event when
116+
// initializing the backup failed.
117+
Call* call = queue.front();
118+
queue.pop();
119+
120+
// We don't call the actual callback, so we have to make sure that
121+
// the baton gets destroyed.
122+
delete call->baton;
123+
delete call;
124+
}
125+
}
126+
127+
NAN_METHOD(Backup::New) {
128+
if (!info.IsConstructCall()) {
129+
return Nan::ThrowTypeError("Use the new operator to create new Backup objects");
130+
}
131+
132+
int length = info.Length();
133+
134+
if (length <= 0 || !Database::HasInstance(info[0])) {
135+
return Nan::ThrowTypeError("Database object expected");
136+
}
137+
else if (length <= 1 || !info[1]->IsString()) {
138+
return Nan::ThrowTypeError("Filename expected");
139+
}
140+
else if (length <= 2 || !info[2]->IsString()) {
141+
return Nan::ThrowTypeError("Source database name expected");
142+
}
143+
else if (length <= 3 || !info[3]->IsString()) {
144+
return Nan::ThrowTypeError("Destination database name expected");
145+
}
146+
else if (length <= 4 || !info[4]->IsBoolean()) {
147+
return Nan::ThrowTypeError("Direction flag expected");
148+
}
149+
else if (length > 5 && !info[5]->IsUndefined() && !info[5]->IsFunction()) {
150+
return Nan::ThrowTypeError("Callback expected");
151+
}
152+
153+
Database* db = Nan::ObjectWrap::Unwrap<Database>(info[0].As<Object>());
154+
Local<String> filename = Local<String>::Cast(info[1]);
155+
Local<String> sourceName = Local<String>::Cast(info[2]);
156+
Local<String> destName = Local<String>::Cast(info[3]);
157+
Local<Boolean> filenameIsDest = Local<Boolean>::Cast(info[4]);
158+
159+
Nan::ForceSet(info.This(), Nan::New("filename").ToLocalChecked(), filename, ReadOnly);
160+
Nan::ForceSet(info.This(), Nan::New("sourceName").ToLocalChecked(), sourceName, ReadOnly);
161+
Nan::ForceSet(info.This(), Nan::New("destName").ToLocalChecked(), destName, ReadOnly);
162+
Nan::ForceSet(info.This(), Nan::New("filenameIsDest").ToLocalChecked(), filenameIsDest, ReadOnly);
163+
164+
Backup* backup = new Backup(db);
165+
backup->Wrap(info.This());
166+
167+
InitializeBaton* baton = new InitializeBaton(db, Local<Function>::Cast(info[5]), backup);
168+
baton->filename = std::string(*Nan::Utf8String(filename));
169+
baton->sourceName = std::string(*Nan::Utf8String(sourceName));
170+
baton->destName = std::string(*Nan::Utf8String(destName));
171+
baton->filenameIsDest = Nan::To<bool>(filenameIsDest).FromJust();
172+
db->Schedule(Work_BeginInitialize, baton);
173+
174+
info.GetReturnValue().Set(info.This());
175+
}
176+
177+
void Backup::Work_BeginInitialize(Database::Baton* baton) {
178+
assert(baton->db->open);
179+
baton->db->pending++;
180+
int status = uv_queue_work(uv_default_loop(),
181+
&baton->request, Work_Initialize, (uv_after_work_cb)Work_AfterInitialize);
182+
assert(status == 0);
183+
}
184+
185+
void Backup::Work_Initialize(uv_work_t* req) {
186+
BACKUP_INIT(InitializeBaton);
187+
188+
// In case stepping fails, we use a mutex to make sure we get the associated
189+
// error message.
190+
sqlite3_mutex* mtx = sqlite3_db_mutex(baton->db->_handle);
191+
sqlite3_mutex_enter(mtx);
192+
193+
backup->status = sqlite3_open(baton->filename.c_str(), &backup->db2);
194+
195+
if (backup->status == SQLITE_OK) {
196+
backup->_handle = sqlite3_backup_init(
197+
baton->filenameIsDest ? backup->db2 : baton->db->_handle,
198+
baton->destName.c_str(),
199+
baton->filenameIsDest ? baton->db->_handle : backup->db2,
200+
baton->sourceName.c_str());
201+
}
202+
203+
if (backup->status != SQLITE_OK) {
204+
backup->message = std::string(sqlite3_errmsg(backup->db2));
205+
sqlite3_close(backup->db2);
206+
backup->db2 = NULL;
207+
}
208+
209+
sqlite3_mutex_leave(mtx);
210+
}
211+
212+
void Backup::Work_AfterInitialize(uv_work_t* req) {
213+
Nan::HandleScope scope;
214+
215+
BACKUP_INIT(InitializeBaton);
216+
217+
if (backup->status != SQLITE_OK) {
218+
Error(baton);
219+
backup->Finish();
220+
}
221+
else {
222+
backup->inited = true;
223+
Local<Function> cb = Nan::New(baton->callback);
224+
if (!cb.IsEmpty() && cb->IsFunction()) {
225+
Local<Value> argv[] = { Nan::Null() };
226+
TRY_CATCH_CALL(backup->handle(), cb, 1, argv);
227+
}
228+
}
229+
BACKUP_END();
230+
}
231+
232+
NAN_METHOD(Backup::Step) {
233+
Backup* backup = Nan::ObjectWrap::Unwrap<Backup>(info.This());
234+
235+
REQUIRE_ARGUMENT_INTEGER(0, pages);
236+
OPTIONAL_ARGUMENT_FUNCTION(1, callback);
237+
238+
Baton* baton = new StepBaton(backup, callback, pages);
239+
backup->Schedule(Work_BeginStep, baton);
240+
info.GetReturnValue().Set(info.This());
241+
}
242+
243+
void Backup::Work_BeginStep(Baton* baton) {
244+
BACKUP_BEGIN(Step);
245+
}
246+
247+
void Backup::Work_Step(uv_work_t* req) {
248+
BACKUP_INIT(StepBaton);
249+
backup->status = sqlite3_backup_step(backup->_handle, baton->pages);
250+
if (backup->status == SQLITE_OK || backup->status == SQLITE_DONE) {
251+
baton->remaining = sqlite3_backup_remaining(backup->_handle);
252+
baton->pagecount = sqlite3_backup_pagecount(backup->_handle);
253+
}
254+
}
255+
256+
void Backup::Work_AfterStep(uv_work_t* req) {
257+
Nan::HandleScope scope;
258+
259+
BACKUP_INIT(StepBaton);
260+
261+
if (backup->status != SQLITE_OK && backup->status != SQLITE_DONE) {
262+
Error(baton);
263+
}
264+
else {
265+
// Fire callbacks.
266+
Local<Function> cb = Nan::New(baton->callback);
267+
if (!cb.IsEmpty() && cb->IsFunction()) {
268+
Nan::Set(backup->handle(), Nan::New("remaining").ToLocalChecked(), Nan::New<Number>(baton->remaining));
269+
Nan::Set(backup->handle(), Nan::New("pageCount").ToLocalChecked(), Nan::New<Number>(baton->pagecount));
270+
Local<Value> argv[] = { Nan::Null(), Nan::New(backup->status == SQLITE_DONE) };
271+
TRY_CATCH_CALL(backup->handle(), cb, 2, argv);
272+
}
273+
}
274+
275+
BACKUP_END();
276+
}
277+
278+
NAN_METHOD(Backup::Finish) {
279+
Backup* backup = Nan::ObjectWrap::Unwrap<Backup>(info.This());
280+
281+
OPTIONAL_ARGUMENT_FUNCTION(0, callback);
282+
283+
Baton* baton = new Baton(backup, callback);
284+
backup->Schedule(Work_BeginFinish, baton);
285+
info.GetReturnValue().Set(info.This());
286+
}
287+
288+
void Backup::Work_BeginFinish(Baton* baton) {
289+
BACKUP_BEGIN(Finish);
290+
}
291+
292+
void Backup::Work_Finish(uv_work_t* req) {
293+
BACKUP_INIT(Baton);
294+
sqlite3_backup_finish(backup->_handle);
295+
backup->_handle = NULL;
296+
if (backup->db2) {
297+
sqlite3_close(backup->db2);
298+
backup->db2 = NULL;
299+
}
300+
}
301+
302+
void Backup::Work_AfterFinish(uv_work_t* req) {
303+
Nan::HandleScope scope;
304+
305+
BACKUP_INIT(Baton);
306+
backup->Finish();
307+
308+
// Fire callback in case there was one.
309+
Local<Function> cb = Nan::New(baton->callback);
310+
if (!cb.IsEmpty() && cb->IsFunction()) {
311+
TRY_CATCH_CALL(backup->handle(), cb, 0, NULL);
312+
}
313+
314+
BACKUP_END();
315+
}
316+
317+
void Backup::Finish() {
318+
assert(!finished);
319+
finished = true;
320+
CleanQueue();
321+
db->Unref();
322+
}

0 commit comments

Comments
 (0)