-
-
Notifications
You must be signed in to change notification settings - Fork 34.2k
src: add support for AbortSignal in backup method #59333
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
|
Review requested:
|
src/node_sqlite.cc
Outdated
| Local<Function> progressFunc, | ||
| Local<Object> abort_signal = Local<Object>()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
just noticed we have different naming style here. both snake_case and camelcase
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All the parameters apart from progressFunc uses snake_case, I will rename this to be more consistent.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
However, I'm not really sure what to do here. The naming style seems inconsistent across the file. Just do a search for Local<Function> and you'll find both snake_case and camelCase being used.
src/node_sqlite.cc
Outdated
| } | ||
| } | ||
|
|
||
| void AbortBackup() { is_aborted_.store(true); } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this function is not used
src/node_sqlite.cc
Outdated
| } | ||
|
|
||
| if (!signal_v->IsUndefined()) { | ||
| if (!signal_v->IsObject()) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should we check if input is abort signal? just realized that the code is the first time brings user's abort signal into C++
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
node/lib/internal/validators.js
Lines 442 to 450 in 0fd1ecd
| /** @type {validateAbortSignal} */ | |
| const validateAbortSignal = hideStackFrames((signal, name) => { | |
| if (signal !== undefined && | |
| (signal === null || | |
| typeof signal !== 'object' || | |
| !('aborted' in signal))) { | |
| throw new ERR_INVALID_ARG_TYPE(name, 'AbortSignal', signal); | |
| } | |
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is a duplicate check if we have JS code
lib/sqlite.js
Outdated
| if (typeof options.signal !== 'object' || options.signal === null) { | ||
| throw new ERR_INVALID_STATE.TypeError('The "options.signal" argument must be an AbortSignal.'); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we already have abort signal check
node/lib/internal/validators.js
Lines 442 to 450 in 0fd1ecd
| /** @type {validateAbortSignal} */ | |
| const validateAbortSignal = hideStackFrames((signal, name) => { | |
| if (signal !== undefined && | |
| (signal === null || | |
| typeof signal !== 'object' || | |
| !('aborted' in signal))) { | |
| throw new ERR_INVALID_ARG_TYPE(name, 'AbortSignal', signal); | |
| } | |
| }); |
| module.exports = internalBinding('sqlite'); | ||
| const binding = internalBinding('sqlite'); | ||
|
|
||
| function backup(sourceDb, path, options = {}) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should this function all in C++
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was wondering why we would need to send it to the C++ layer if it is not valid. On the other hand, it might look cleaner to leave this file as it is and add everything in C++ instead.
src/node_sqlite.cc
Outdated
| } | ||
|
|
||
| void DoThreadPoolWork() override { | ||
| if (is_aborted_.load()) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
load(std::memory_order_acquire)
src/node_sqlite.cc
Outdated
| Local<Promise::Resolver> resolver = | ||
| Local<Promise::Resolver>::New(env()->isolate(), resolver_); | ||
|
|
||
| if (is_aborted_.load() || backup_status_ == SQLITE_INTERRUPT) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
load(std::memory_order_acquire)
src/node_sqlite.cc
Outdated
| } | ||
| } | ||
|
|
||
| void AbortBackup() { is_aborted_.store(true); } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
store(true, std::memory_order_release)
src/node_sqlite.cc
Outdated
| Local<Value> aborted_value; | ||
| if (signal->Get(env()->context(), aborted_key).ToLocal(&aborted_value)) { | ||
| if (aborted_value->BooleanValue(isolate)) { | ||
| is_aborted_.store(true); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same
src/node_sqlite.cc
Outdated
| ->Set(env()->context(), | ||
| String::NewFromUtf8(isolate, "name").ToLocalChecked(), | ||
| name) | ||
| .ToChecked(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should check the error here
inline MaybeLocal<Object> CreateAbortError(Isolate* isolate,
const char* message = "The operation was aborted") {
Local<String> js_msg;
Local<Object> e;
if (!String::NewFromUtf8(isolate, message).ToLocal(&js_msg) ||
!Exception::Error(js_msg)
->ToObject(isolate->GetCurrentContext())
.ToLocal(&e) ||
e->Set(isolate->GetCurrentContext(),
String::NewFromUtf8(isolate, "name").ToLocalChecked(),
String::NewFromUtf8(isolate, "AbortError").ToLocalChecked())
.IsNothing()) {
return MaybeLocal<Object>();
}
return e;
}
void HandleAbortError(Local<Promise::Resolver> resolver) {
Local<Object> e;
if (!CreateAbortError(env()->isolate()).ToLocal(&e)) {
Finalize();
return;
}
Finalize();
resolver->Reject(env()->context(), e).ToChecked();
}There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
btw, AbortError is in JS side, we should get that class to make consistently
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #59333 +/- ##
==========================================
- Coverage 88.54% 88.50% -0.05%
==========================================
Files 703 703
Lines 208077 208195 +118
Branches 40082 40108 +26
==========================================
+ Hits 184252 184265 +13
- Misses 15848 15961 +113
+ Partials 7977 7969 -8
🚀 New features to boost your workflow:
|
| } \ | ||
| } while (0) | ||
|
|
||
| class AbortError { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@himself65 I have tried to implement this class as you suggested, I hope I have not misunderstood 😂 I have check its js implementation from here: https://github.com/nodejs/node/blob/main/lib/internal/errors.js#L976-L988
|
@himself65 Thank you very much for your review ❤️ I learned a lot from it!
In addition to this, I need to mention that when I ran It is not clear to me what I have done wrong or how to correct this, any pointers will be welcome |
src/node_sqlite.cc
Outdated
| public: | ||
| static MaybeLocal<Object> New( | ||
| Isolate* isolate, | ||
| const char* message = "The operation was aborted", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: I'd consider using std::string_view here
src/node_sqlite.cc
Outdated
|
|
||
| if (error_obj | ||
| ->Set(context, | ||
| String::NewFromUtf8(isolate, "name").ToLocalChecked(), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For fixed constants, you'll probably want to use FIXED_ONE_BYTE_STRING()
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do we not already have a env->name_string()?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, just in general, avoid the use of the ToLocalChecked() in these.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do we not already have a
env->name_string()?
Yes, in this specific case we should prefer that 👍 My comment was more generally meant for the rest of the PR.
Also, just in general, avoid the use of the
ToLocalChecked()in these.
Yeah, it's a bit odd, for creating fixed-length strings this is generally fine but that's really a special case and it's probably better if we use something like ToV8Value() and add proper error handling, yes, if only it's because then we're sticking to a pattern that makes sense when used everywhere.
| validateString(path, 'options.headers.host'); | ||
| validateAbortSignal(options.signal, 'options.signal'); | ||
| if (options.signal?.aborted) { | ||
| return PromiseReject(new AbortError(undefined, { cause: options.signal.reason })); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One issue I can see with this, which does not need to be addressed now, is that this will mean we have two AbortError types, one defined in JavaScript another defined in C++. They'll both be named AbortError but won't be the same class so instanceof checks between the two won't work. Not a big deal but it might be good to leave a todo somewhere for someone later to reconcile the two into a single implementation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for pointing this out, I wasn't sure if I understood this correctly when I was told in a previous comment! I'd like to follow up on this after landing the current PR. It definitely makes sense to reconcile the two AbortError implementations so instanceof works consistently from js.
Would you happen to have any pointers on the preferred approach for reusing the js native AbortError in C++?
I have put the comment here: https://github.com/nodejs/node/pull/59333/files#diff-dd810db4fe69364c3a67a274e0725f386040c0fd1dcfade7093f23c8514328aeR119-R120
This adds support for passing an AbortSignal to the database backup function. If the signal is aborted during the backup process, the operation is interrupted and a rejection is returned with an AbortError. The signal is optionally passed through the options object. Internally, the signal is stored and periodically checked between backup steps to respond quickly to abort requests. This improves integration with modern web APIs and aligns with how abortable operations are handled elsewhere in the Node.js ecosystem. Fixes: nodejs#58888
|
Following up to see what the next steps might be here. I’m happy to continue iterating on the changes, or if it makes more sense for a more experienced C++ contributor to pick it up, I’m fine with that too. |
This adds support for passing an AbortSignal to the database backup function. If the signal is aborted during the backup process, the operation is interrupted and a rejection is returned with an AbortError.
The signal is optionally passed through the options object. Internally, the signal is stored and periodically checked between backup steps to respond quickly to abort requests.
This improves integration with modern web APIs and aligns with how abortable operations are handled elsewhere in the Node.js ecosystem.
Fixes: #58888