Skip to content

Commit 68dfec7

Browse files
Jake ChampionJakeChampion
authored andcommitted
fix: refactor our async task implementation to be a generic AsyncTask class instead of separate implementations for each async operation
This implementation enables our event loop to no longer have to be aware of all the different async tasks and how to process them, instead it understands AsyncTask and to call the `process` function contained within an AsyncTask. A side-effect of this refactoring is that KVStore.prototype.lookup and KVStore.prototype.delete now work when being called multiple times in parallel - previously this would not work as the KVStore class contained a slot for the Promise of the async work, which meant if you called either method more than once, the previous method's Promise would have been overwritten within the Slot. The new implementation creates a new AsyncTask instance for each lookup and each delete invocation.
1 parent 29764e2 commit 68dfec7

File tree

8 files changed

+242
-203
lines changed

8 files changed

+242
-203
lines changed

integration-tests/js-compute/fixtures/app/src/kv-store.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -833,6 +833,38 @@ import { routes } from "./routes.js";
833833
if (error) { return error }
834834
return pass()
835835
});
836+
837+
routes.set("/kv-store/get/multiple-lookups-at-once", async () => {
838+
let store = createValidStore()
839+
let key1 = `key-exists-${Math.random()}`;
840+
await store.put(key1, '1hello1')
841+
let key2 = `key-exists-${Math.random()}`;
842+
await store.put(key2, '2hello2')
843+
let key3 = `key-exists-${Math.random()}`;
844+
await store.put(key3, '3hello3')
845+
let key4 = `key-exists-${Math.random()}`;
846+
await store.put(key4, '4hello4')
847+
let key5 = `key-exists-${Math.random()}`;
848+
await store.put(key5, '5hello5')
849+
let results = await Promise.all([
850+
store.get(key1),
851+
store.get(key2),
852+
store.get(key3),
853+
store.get(key4),
854+
store.get(key5),
855+
]);
856+
let error = assert(await results[0].text(), '1hello1', `await results[0].text()`)
857+
if (error) { return error }
858+
error = assert(await results[1].text(), '2hello2', `await results[1].text()`)
859+
if (error) { return error }
860+
error = assert(await results[2].text(), '3hello3', `await results[2].text()`)
861+
if (error) { return error }
862+
error = assert(await results[3].text(), '4hello4', `await results[3].text()`)
863+
if (error) { return error }
864+
error = assert(await results[4].text(), '5hello5', `await results[4].text()`)
865+
if (error) { return error }
866+
return pass()
867+
});
836868
}
837869
}
838870

runtime/js-compute-runtime/builtins/kv-store.cpp

Lines changed: 19 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -183,44 +183,17 @@ bool parse_and_validate_key(JSContext *cx, const char *key, size_t len) {
183183

184184
} // namespace
185185

186-
bool KVStore::has_pending_delete_handle(JSObject *self) {
187-
MOZ_ASSERT(KVStore::is_instance(self));
188-
189-
JS::Value handle_val =
190-
JS::GetReservedSlot(self, static_cast<uint32_t>(Slots::PendingDeleteHandle));
191-
return handle_val.isInt32() &&
192-
handle_val.toInt32() != host_api::ObjectStorePendingDelete::invalid;
193-
}
194-
195-
host_api::ObjectStorePendingDelete KVStore::pending_delete_handle(JSObject *self) {
196-
MOZ_ASSERT(KVStore::is_instance(self));
197-
host_api::ObjectStorePendingDelete res;
198-
199-
JS::Value handle_val =
200-
JS::GetReservedSlot(self, static_cast<uint32_t>(Slots::PendingDeleteHandle));
201-
if (handle_val.isInt32()) {
202-
res = host_api::ObjectStorePendingDelete(handle_val.toInt32());
203-
}
204-
205-
return res;
206-
}
207-
208-
bool KVStore::process_pending_kv_store_delete(JSContext *cx, JS::HandleObject self) {
209-
MOZ_ASSERT(KVStore::is_instance(self));
210-
211-
auto pending_promise_value =
212-
JS::GetReservedSlot(self, static_cast<uint32_t>(Slots::PendingDeletePromise));
213-
MOZ_ASSERT(pending_promise_value.isObject());
214-
JS::RootedObject result_promise(cx, &pending_promise_value.toObject());
215-
216-
auto res = builtins::KVStore::pending_delete_handle(self).wait();
186+
bool KVStore::process_pending_kv_store_delete(JSContext *cx, int32_t handle,
187+
JS::HandleObject context, JS::HandleObject promise) {
188+
host_api::ObjectStorePendingDelete pending_lookup(handle);
217189

190+
auto res = pending_lookup.wait();
218191
if (auto *err = res.to_err()) {
219192
HANDLE_ERROR(cx, *err);
220-
return RejectPromiseWithPendingError(cx, result_promise);
193+
return RejectPromiseWithPendingError(cx, promise);
221194
}
222195

223-
JS::ResolvePromise(cx, result_promise, JS::UndefinedHandleValue);
196+
JS::ResolvePromise(cx, promise, JS::UndefinedHandleValue);
224197

225198
return true;
226199
}
@@ -253,45 +226,26 @@ bool KVStore::delete_(JSContext *cx, unsigned argc, JS::Value *vp) {
253226
}
254227
auto ret = res.unwrap();
255228

256-
JS::SetReservedSlot(self, static_cast<uint32_t>(Slots::PendingDeleteHandle),
257-
JS::Int32Value(ret.handle));
258-
JS::SetReservedSlot(self, static_cast<uint32_t>(Slots::PendingDeletePromise),
259-
JS::ObjectValue(*result_promise));
229+
auto task = core::AsyncTask::create(cx, ret.handle, self, result_promise,
230+
KVStore::process_pending_kv_store_delete);
260231

261-
if (!core::EventLoop::queue_async_task(self)) {
232+
if (!core::EventLoop::queue_async_task(task)) {
262233
return ReturnPromiseRejectedWithPendingError(cx, args);
263234
}
264235

265236
args.rval().setObject(*result_promise);
266237
return true;
267238
}
268239

269-
host_api::ObjectStorePendingLookup KVStore::pending_lookup_handle(JSObject *self) {
270-
MOZ_ASSERT(KVStore::is_instance(self));
271-
host_api::ObjectStorePendingLookup res;
272-
273-
JS::Value handle_val =
274-
JS::GetReservedSlot(self, static_cast<uint32_t>(Slots::PendingLookupHandle));
275-
if (handle_val.isInt32()) {
276-
res = host_api::ObjectStorePendingLookup(handle_val.toInt32());
277-
}
278-
279-
return res;
280-
}
281-
282-
bool KVStore::process_pending_kv_store_lookup(JSContext *cx, JS::HandleObject self) {
283-
MOZ_ASSERT(KVStore::is_instance(self));
284-
285-
auto pending_promise_value =
286-
JS::GetReservedSlot(self, static_cast<uint32_t>(Slots::PendingLookupPromise));
287-
MOZ_ASSERT(pending_promise_value.isObject());
288-
JS::RootedObject result_promise(cx, &pending_promise_value.toObject());
240+
bool KVStore::process_pending_kv_store_lookup(JSContext *cx, int32_t handle,
241+
JS::HandleObject context, JS::HandleObject promise) {
242+
host_api::ObjectStorePendingLookup pending_lookup(handle);
289243

290-
auto res = builtins::KVStore::pending_lookup_handle(self).wait();
244+
auto res = pending_lookup.wait();
291245

292246
if (auto *err = res.to_err()) {
293247
HANDLE_ERROR(cx, *err);
294-
return RejectPromiseWithPendingError(cx, result_promise);
248+
return RejectPromiseWithPendingError(cx, promise);
295249
}
296250

297251
auto ret = res.unwrap();
@@ -300,15 +254,15 @@ bool KVStore::process_pending_kv_store_lookup(JSContext *cx, JS::HandleObject se
300254
if (!ret.has_value()) {
301255
JS::RootedValue result(cx);
302256
result.setNull();
303-
JS::ResolvePromise(cx, result_promise, result);
257+
JS::ResolvePromise(cx, promise, result);
304258
} else {
305259
JS::RootedObject entry(cx, KVStoreEntry::create(cx, ret.value()));
306260
if (!entry) {
307261
return false;
308262
}
309263
JS::RootedValue result(cx);
310264
result.setObject(*entry);
311-
JS::ResolvePromise(cx, result_promise, result);
265+
JS::ResolvePromise(cx, promise, result);
312266
}
313267

314268
return true;
@@ -342,12 +296,10 @@ bool KVStore::get(JSContext *cx, unsigned argc, JS::Value *vp) {
342296
}
343297
auto ret = res.unwrap();
344298

345-
JS::SetReservedSlot(self, static_cast<uint32_t>(Slots::PendingLookupHandle),
346-
JS::Int32Value(ret.handle));
347-
JS::SetReservedSlot(self, static_cast<uint32_t>(Slots::PendingLookupPromise),
348-
JS::ObjectValue(*result_promise));
299+
auto task = core::AsyncTask::create(cx, ret.handle, self, result_promise,
300+
KVStore::process_pending_kv_store_lookup);
349301

350-
if (!core::EventLoop::queue_async_task(self)) {
302+
if (!core::EventLoop::queue_async_task(std::move(task))) {
351303
return ReturnPromiseRejectedWithPendingError(cx, args);
352304
}
353305

runtime/js-compute-runtime/builtins/kv-store.h

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,6 @@ class KVStore final : public BuiltinImpl<KVStore> {
3939
static constexpr const char *class_name = "KVStore";
4040
enum class Slots {
4141
KVStore,
42-
PendingLookupPromise,
43-
PendingLookupHandle,
44-
PendingDeletePromise,
45-
PendingDeleteHandle,
4642
Count,
4743
};
4844
static const JSFunctionSpec static_methods[];
@@ -54,11 +50,10 @@ class KVStore final : public BuiltinImpl<KVStore> {
5450

5551
static bool init_class(JSContext *cx, JS::HandleObject global);
5652
static bool constructor(JSContext *cx, unsigned argc, JS::Value *vp);
57-
static host_api::ObjectStorePendingLookup pending_lookup_handle(JSObject *self);
58-
static bool process_pending_kv_store_lookup(JSContext *cx, JS::HandleObject self);
59-
static host_api::ObjectStorePendingDelete pending_delete_handle(JSObject *self);
60-
static bool process_pending_kv_store_delete(JSContext *cx, JS::HandleObject self);
61-
static bool has_pending_delete_handle(JSObject *self);
53+
static bool process_pending_kv_store_lookup(JSContext *cx, int32_t handle,
54+
JS::HandleObject context, JS::HandleObject promise);
55+
static bool process_pending_kv_store_delete(JSContext *cx, int32_t handle,
56+
JS::HandleObject context, JS::HandleObject promise);
6257
};
6358

6459
} // namespace builtins

runtime/js-compute-runtime/builtins/request-response.cpp

Lines changed: 108 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
#include "builtins/request-response.h"
2-
32
#include "builtins/cache-override.h"
43
#include "builtins/client-info.h"
54
#include "builtins/fastly.h"
65
#include "builtins/fetch-event.h"
76
#include "builtins/kv-store.h"
87
#include "builtins/native-stream-source.h"
8+
#include "builtins/shared/dom-exception.h"
99
#include "builtins/shared/url.h"
1010
#include "builtins/transform-stream.h"
1111
#include "core/encode.h"
@@ -29,8 +29,68 @@
2929
namespace builtins {
3030

3131
namespace {
32+
bool error_stream_controller_with_pending_exception(JSContext *cx, JS::HandleObject controller) {
33+
JS::RootedValue exn(cx);
34+
if (!JS_GetPendingException(cx, &exn))
35+
return false;
36+
JS_ClearPendingException(cx);
37+
38+
JS::RootedValueArray<1> args(cx);
39+
args[0].set(exn);
40+
JS::RootedValue r(cx);
41+
return JS::Call(cx, controller, "error", args, &r);
42+
}
3243
constexpr size_t HANDLE_READ_CHUNK_SIZE = 8192;
3344

45+
bool process_body_read(JSContext *cx, int32_t handle, JS::HandleObject context,
46+
JS::HandleObject promise) {
47+
MOZ_ASSERT(context);
48+
JS::RootedObject streamSource(cx, context);
49+
MOZ_ASSERT(builtins::NativeStreamSource::is_instance(streamSource));
50+
host_api::HttpBody body(handle);
51+
JS::RootedObject owner(cx, builtins::NativeStreamSource::owner(streamSource));
52+
JS::RootedObject controller(cx, builtins::NativeStreamSource::controller(streamSource));
53+
54+
auto read_res = body.read(HANDLE_READ_CHUNK_SIZE);
55+
if (auto *err = read_res.to_err()) {
56+
HANDLE_ERROR(cx, *err);
57+
return error_stream_controller_with_pending_exception(cx, controller);
58+
}
59+
60+
auto &chunk = read_res.unwrap();
61+
if (chunk.len == 0) {
62+
JS::RootedValue r(cx);
63+
return JS::Call(cx, controller, "close", JS::HandleValueArray::empty(), &r);
64+
}
65+
66+
// We don't release control of chunk's data until after we've checked that the array buffer
67+
// allocation has been successful, as that ensures that the return path frees chunk automatically
68+
// when necessary.
69+
JS::RootedObject buffer(
70+
cx, JS::NewArrayBufferWithContents(cx, chunk.len, chunk.ptr.get(),
71+
JS::NewArrayBufferOutOfMemory::CallerMustFreeMemory));
72+
if (!buffer) {
73+
return error_stream_controller_with_pending_exception(cx, controller);
74+
}
75+
76+
// At this point `buffer` has taken full ownership of the chunk's data.
77+
std::ignore = chunk.ptr.release();
78+
79+
JS::RootedObject byte_array(cx, JS_NewUint8ArrayWithBuffer(cx, buffer, 0, chunk.len));
80+
if (!byte_array) {
81+
return false;
82+
}
83+
84+
JS::RootedValueArray<1> enqueue_args(cx);
85+
enqueue_args[0].setObject(*byte_array);
86+
JS::RootedValue r(cx);
87+
if (!JS::Call(cx, controller, "enqueue", enqueue_args, &r)) {
88+
return error_stream_controller_with_pending_exception(cx, controller);
89+
}
90+
91+
return true;
92+
}
93+
3494
// https://fetch.spec.whatwg.org/#concept-method-normalize
3595
// Returns `true` if the method name was normalized, `false` otherwise.
3696
bool normalize_http_method(char *method) {
@@ -120,6 +180,39 @@ bool enqueue_internal_method(JSContext *cx, JS::HandleObject receiver,
120180

121181
} // namespace
122182

183+
bool RequestOrResponse::process_pending_request(JSContext *cx, int32_t handle,
184+
JS::HandleObject context,
185+
JS::HandleObject promise) {
186+
MOZ_ASSERT(builtins::Request::is_instance(context));
187+
host_api::HttpPendingReq pending(handle);
188+
auto res = pending.wait();
189+
if (auto *err = res.to_err()) {
190+
std::string message = std::move(err->message()).value_or("when attempting to fetch resource.");
191+
builtins::DOMException::raise(cx, message, "NetworkError");
192+
return RejectPromiseWithPendingError(cx, promise);
193+
}
194+
195+
auto [response_handle, body] = res.unwrap();
196+
JS::RootedObject response_instance(cx, JS_NewObjectWithGivenProto(cx, &builtins::Response::class_,
197+
builtins::Response::proto_obj));
198+
if (!response_instance) {
199+
return false;
200+
}
201+
202+
bool is_upstream = true;
203+
bool is_grip_upgrade = false;
204+
JS::RootedObject response(cx,
205+
builtins::Response::create(cx, response_instance, response_handle, body,
206+
is_upstream, is_grip_upgrade, nullptr));
207+
if (!response) {
208+
return false;
209+
}
210+
211+
builtins::RequestOrResponse::set_url(response, builtins::RequestOrResponse::url(context));
212+
JS::RootedValue response_val(cx, JS::ObjectValue(*response));
213+
return JS::ResolvePromise(cx, promise, response_val);
214+
}
215+
123216
bool RequestOrResponse::is_instance(JSObject *obj) {
124217
return Request::is_instance(obj) || Response::is_instance(obj) || KVStoreEntry::is_instance(obj);
125218
}
@@ -814,8 +907,15 @@ bool RequestOrResponse::body_source_pull_algorithm(JSContext *cx, JS::CallArgs a
814907
// (This deadlock happens in automated tests, but admittedly might not happen
815908
// in real usage.)
816909

817-
if (!core::EventLoop::queue_async_task(source))
910+
JS::RootedObject self(cx, &args.thisv().toObject());
911+
JS::RootedObject owner(cx, builtins::NativeStreamSource::owner(self));
912+
auto task = core::AsyncTask::create(
913+
cx, builtins::RequestOrResponse::body_handle(owner).async_handle().handle, source, nullptr,
914+
process_body_read);
915+
916+
if (!core::EventLoop::queue_async_task(task)) {
818917
return false;
918+
}
819919

820920
args.rval().setUndefined();
821921
return true;
@@ -862,7 +962,12 @@ bool RequestOrResponse::body_reader_then_handler(JSContext *cx, JS::HandleObject
862962
}
863963

864964
if (Request::is_instance(body_owner)) {
865-
if (!core::EventLoop::queue_async_task(body_owner)) {
965+
JS::RootedObject promise(cx, Request::response_promise(body_owner));
966+
auto task = core::AsyncTask::create(
967+
cx, builtins::Request::pending_handle(body_owner).async_handle().handle, body_owner,
968+
promise, RequestOrResponse::process_pending_request);
969+
970+
if (!core::EventLoop::queue_async_task(task)) {
866971
return false;
867972
}
868973
}

runtime/js-compute-runtime/builtins/request-response.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ class RequestOrResponse final {
3636
static void set_manual_framing_headers(JSContext *cx, JSObject *obj, JS::HandleValue url);
3737
static bool body_unusable(JSContext *cx, JS::HandleObject body);
3838
static bool extract_body(JSContext *cx, JS::HandleObject self, JS::HandleValue body_val);
39+
static bool process_pending_request(JSContext *cx, int32_t handle, JS::HandleObject context,
40+
JS::HandleObject promise);
3941

4042
/**
4143
* Returns the RequestOrResponse's Headers if it has been reified, nullptr if

0 commit comments

Comments
 (0)