Skip to content

Commit

Permalink
support list op
Browse files Browse the repository at this point in the history
  • Loading branch information
guybedford committed Oct 24, 2024
1 parent a5a8bf5 commit b21086c
Show file tree
Hide file tree
Showing 9 changed files with 469 additions and 242 deletions.
403 changes: 223 additions & 180 deletions integration-tests/js-compute/fixtures/app/src/kv-store.js

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions integration-tests/js-compute/fixtures/app/tests.json
Original file line number Diff line number Diff line change
Expand Up @@ -1080,6 +1080,7 @@
"environments": ["compute"]
},
"GET /includeBytes": {},
"GET /kv-store-e2e/list": {},
"GET /kv-store/exposed-as-global": {},
"GET /kv-store/interface": {},
"GET /kv-store/constructor/called-as-regular-function": {},
Expand Down
138 changes: 90 additions & 48 deletions runtime/fastly/builtins/kv-store.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,11 @@ host_api::ObjectStore kv_store_handle(JSObject *obj) {
return host_api::ObjectStore(val.toInt32());
}

host_api::KVStore kv_store(JSObject *obj) {
JS::Value val = JS::GetReservedSlot(obj, static_cast<uint32_t>(KVStore::Slots::KVStore));
return host_api::KVStore(val.toInt32());
}

bool parse_and_validate_key(JSContext *cx, const char *key, size_t len) {
// If the converted string has a length of 0 then we throw an Error
// because KVStore Keys have to be at-least 1 character.
Expand Down Expand Up @@ -183,11 +188,25 @@ bool parse_and_validate_key(JSContext *cx, const char *key, size_t len) {
return true;
}

} // namespace
bool process_pending_kv_store_list(JSContext *cx, host_api::KVStorePendingList::Handle handle,
JS::HandleObject context, JS::HandleObject promise) {
host_api::KVStorePendingList pending_list(handle);

auto res = pending_list.wait();
if (auto *err = res.to_err()) {
std::string message = std::move(err->message()).value_or("when attempting to fetch resource.");
JS_ReportErrorNumberASCII(cx, FastlyGetErrorMessage, nullptr, JSMSG_KV_STORE_LIST_ERROR,
message.c_str());
return RejectPromiseWithPendingError(cx, promise);
}

JS::ResolvePromise(cx, promise, JS::UndefinedHandleValue);
return true;
}

bool KVStore::process_pending_kv_store_delete(JSContext *cx,
host_api::ObjectStorePendingDelete::Handle handle,
JS::HandleObject context, JS::HandleObject promise) {
bool process_pending_kv_store_delete(JSContext *cx,
host_api::ObjectStorePendingDelete::Handle handle,
JS::HandleObject context, JS::HandleObject promise) {
host_api::ObjectStorePendingDelete pending_delete(handle);

auto res = pending_delete.wait();
Expand All @@ -205,6 +224,40 @@ bool KVStore::process_pending_kv_store_delete(JSContext *cx,
return true;
}

bool process_pending_kv_store_lookup(JSContext *cx,
host_api::ObjectStorePendingLookup::Handle handle,
JS::HandleObject context, JS::HandleObject promise) {
host_api::ObjectStorePendingLookup pending_lookup(handle);

auto res = pending_lookup.wait();

if (auto *err = res.to_err()) {
HANDLE_ERROR(cx, *err);
return RejectPromiseWithPendingError(cx, promise);
}

auto ret = res.unwrap();

// When no entry is found, we are going to resolve the Promise with `null`.
if (!ret.has_value()) {
JS::RootedValue result(cx);
result.setNull();
JS::ResolvePromise(cx, promise, result);
} else {
JS::RootedObject entry(cx, KVStoreEntry::create(cx, ret.value()));
if (!entry) {
return false;
}
JS::RootedValue result(cx);
result.setObject(*entry);
JS::ResolvePromise(cx, promise, result);
}

return true;
}

} // namespace

bool KVStore::delete_(JSContext *cx, unsigned argc, JS::Value *vp) {
METHOD_HEADER_WITH_NAME(1, "delete");

Expand Down Expand Up @@ -234,44 +287,12 @@ bool KVStore::delete_(JSContext *cx, unsigned argc, JS::Value *vp) {
auto handle = res.unwrap();

ENGINE->queue_async_task(
new FastlyAsyncTask(handle, self, result_promise, KVStore::process_pending_kv_store_delete));
new FastlyAsyncTask(handle, self, result_promise, process_pending_kv_store_delete));

args.rval().setObject(*result_promise);
return true;
}

bool KVStore::process_pending_kv_store_lookup(JSContext *cx,
host_api::ObjectStorePendingLookup::Handle handle,
JS::HandleObject context, JS::HandleObject promise) {
host_api::ObjectStorePendingLookup pending_lookup(handle);

auto res = pending_lookup.wait();

if (auto *err = res.to_err()) {
HANDLE_ERROR(cx, *err);
return RejectPromiseWithPendingError(cx, promise);
}

auto ret = res.unwrap();

// When no entry is found, we are going to resolve the Promise with `null`.
if (!ret.has_value()) {
JS::RootedValue result(cx);
result.setNull();
JS::ResolvePromise(cx, promise, result);
} else {
JS::RootedObject entry(cx, KVStoreEntry::create(cx, ret.value()));
if (!entry) {
return false;
}
JS::RootedValue result(cx);
result.setObject(*entry);
JS::ResolvePromise(cx, promise, result);
}

return true;
}

bool KVStore::get(JSContext *cx, unsigned argc, JS::Value *vp) {
METHOD_HEADER(1)

Expand Down Expand Up @@ -300,8 +321,7 @@ bool KVStore::get(JSContext *cx, unsigned argc, JS::Value *vp) {
}
auto handle = res.unwrap();

auto task =
new FastlyAsyncTask(handle, self, result_promise, KVStore::process_pending_kv_store_lookup);
auto task = new FastlyAsyncTask(handle, self, result_promise, process_pending_kv_store_lookup);
ENGINE->queue_async_task(task);

args.rval().setObject(*result_promise);
Expand Down Expand Up @@ -429,7 +449,7 @@ bool KVStore::put(JSContext *cx, unsigned argc, JS::Value *vp) {
bool KVStore::list(JSContext *cx, unsigned argc, JS::Value *vp) {
METHOD_HEADER(0)

if (args.length > 0) {
if (args.length() > 0) {
JS::RootedValue prefix(cx, args.get(0));
if (!prefix.isString()) {
api::throw_error(cx, api::Errors::TypeError, "KVStore.list", "prefix", "be a string");
Expand All @@ -441,19 +461,41 @@ bool KVStore::list(JSContext *cx, unsigned argc, JS::Value *vp) {
}
}

if (args.length > 1) {
JS::RootedValue limit(cx, args.get(1));
uint32_t limit = 0;
if (limit.isDouble()) {
double limit_double = limit.toDouble();
modf(value, &limit) == 0.0;
limit = limit_double;
std::optional<uint32_t> limit;

if (args.length() > 1) {
JS::RootedValue limit_val(cx, args.get(1));
if (limit_val.isDouble()) {
double limit_double = limit_val.toDouble();
double limit_rounded;
modf(limit_double, &limit_rounded);
limit = limit_rounded;
}
if (!limit.isNumber()) {
if (!limit_val.isNumber()) {
api::throw_error(cx, api::Errors::TypeError, "KVStore.list", "limit", "be an integer");
return ReturnPromiseRejectedWithPendingError(cx, args);
}
}

auto res = kv_store(self).list(std::nullopt, limit, std::nullopt, false);
if (auto *err = res.to_err()) {
// Ensure that we throw an exception for all unexpected host errors.
HANDLE_ERROR(cx, *err);
return ReturnPromiseRejectedWithPendingError(cx, args);
}

auto handle = res.unwrap();

JS::RootedObject result_promise(cx, JS::NewPromiseObject(cx, nullptr));
if (!result_promise) {
return ReturnPromiseRejectedWithPendingError(cx, args);
}

auto task = new FastlyAsyncTask(handle, self, result_promise, process_pending_kv_store_list);
ENGINE->queue_async_task(task);

args.rval().setObject(*result_promise);
return true;
}

const JSFunctionSpec KVStore::static_methods[] = {
Expand Down
6 changes: 0 additions & 6 deletions runtime/fastly/builtins/kv-store.h
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,6 @@ class KVStore final : public builtins::BuiltinImpl<KVStore> {
static const unsigned ctor_length = 1;

static bool constructor(JSContext *cx, unsigned argc, JS::Value *vp);
static bool process_pending_kv_store_lookup(JSContext *cx,
host_api::ObjectStorePendingLookup::Handle handle,
JS::HandleObject context, JS::HandleObject promise);
static bool process_pending_kv_store_delete(JSContext *cx,
host_api::ObjectStorePendingDelete::Handle handle,
JS::HandleObject context, JS::HandleObject promise);
};

} // namespace fastly::kv_store
Expand Down
1 change: 1 addition & 0 deletions runtime/fastly/host-api/error_numbers.msg
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ MSG_DEF(JSMSG_KV_STORE_KEY_ACME, 0, JSEXN_TYPEERR,
MSG_DEF(JSMSG_KV_STORE_KEY_RELATIVE, 0, JSEXN_TYPEERR, "KVStore key can not be '.' or '..'")
MSG_DEF(JSMSG_KV_STORE_PUT_CONTENT_STREAM, 0, JSEXN_TYPEERR, "Content-provided streams are not yet supported for streaming into KVStore")
MSG_DEF(JSMSG_KV_STORE_PUT_OVER_30_MB, 0, JSEXN_TYPEERR, "KVStore value can not be more than 30 Megabytes in size")
MSG_DEF(JSMSG_KV_STORE_LIST_ERROR, 1, JSEXN_TYPEERR, "KVStore list: {0}")
MSG_DEF(JSMSG_SECRET_STORE_DOES_NOT_EXIST, 1, JSEXN_TYPEERR, "SecretStore constructor: No SecretStore named '{0}' exists")
MSG_DEF(JSMSG_SECRET_STORE_KEY_EMPTY, 0, JSEXN_TYPEERR, "SecretStore key can not be an empty string")
MSG_DEF(JSMSG_SECRET_STORE_KEY_TOO_LONG, 0, JSEXN_TYPEERR, "SecretStore key can not be more than 255 characters")
Expand Down
6 changes: 4 additions & 2 deletions runtime/fastly/host-api/fastly.h
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,8 @@ typedef struct fastly_host_http_send_error_detail {
uint8_t tls_alert_id;
} fastly_host_http_send_error_detail;

typedef uint32_t fastly_kv_error;

// The values need to match https://docs.rs/fastly-sys/0.10.5/src/fastly_sys/lib.rs.html#111
#define BACKEND_CONFIG_RESERVED (1u << 0)
#define BACKEND_CONFIG_HOST_OVERRIDE (1u << 1)
Expand Down Expand Up @@ -590,10 +592,10 @@ typedef struct __attribute__((aligned(4))) KVDeleteOptions {

typedef struct __attribute__((aligned(4))) KVListOptions {
uint32_t mode;
uint8_t *cursor;
const uint8_t *cursor;
uint32_t cursor_len;
uint32_t limit;
uint8_t *prefix;
const uint8_t *prefix;
uint32_t prefix_len;
} KVListOptions;

Expand Down
126 changes: 126 additions & 0 deletions runtime/fastly/host-api/host_api.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -743,6 +743,45 @@ make_fastly_send_error(fastly::fastly_host_http_send_error_detail &send_error_de
return res;
}

FastlyKVError make_fastly_kv_error(fastly::fastly_kv_error kv_error) {
FastlyKVError err;
switch (kv_error) {
case KV_ERROR_BAD_REQUEST: {
err.detail = FastlyKVError::detail::bad_request;
break;
}
case KV_ERROR_INTERNAL_ERROR: {
err.detail = FastlyKVError::detail::internal_error;
break;
}
case KV_ERROR_NOT_FOUND: {
err.detail = FastlyKVError::detail::not_found;
break;
}
case KV_ERROR_OK: {
err.detail = FastlyKVError::detail::ok;
break;
}
case KV_ERROR_PAYLOAD_TOO_LARGE: {
err.detail = FastlyKVError::detail::payload_too_large;
break;
}
case KV_ERROR_PRECONDITION_FAILED: {
err.detail = FastlyKVError::detail::precondition_failed;
break;
}
case KV_ERROR_TOO_MANY_REQUESTS: {
err.detail = FastlyKVError::detail::too_many_requests;
break;
}
case KV_ERROR_UNINITIALIZED: {
err.detail = FastlyKVError::detail::uninitialized;
break;
}
}
return err;
}

} // namespace

Result<HttpBody> HttpBody::make() {
Expand Down Expand Up @@ -2455,6 +2494,35 @@ Result<uint64_t> CacheHandle::get_hits() {
return res;
}

const std::optional<std::string> FastlyKVError::message() const {
switch (this->detail) {
/// The kv-error-detail struct has not been populated.
case uninitialized:
return "Uninitialized.";
/// There was no kv error.
case ok:
return std::nullopt;
/// Bad request.
case bad_request:
return "Bad request.";
/// KV store entry not found.
case not_found:
return "Not found.";
/// Invalid state for operation.
case precondition_failed:
return "Precondition failed.";
/// Buffer size issues.
case payload_too_large:
return "Payload too large.";
/// Oh no.
case internal_error:
return "Internal error.";
/// Rate limiting
case too_many_requests:
return "Too many requests.";
};
}

const std::optional<std::string> FastlySendError::message() const {
switch (this->tag) {
/// The send-error-detail struct has not been populated.
Expand Down Expand Up @@ -3082,6 +3150,64 @@ Result<KVStore> KVStore::open(std::string_view name) {
return res;
}

Result<KVStorePendingList::Handle> KVStore::list(std::optional<string_view> cursor,
std::optional<uint32_t> limit,
std::optional<string_view> prefix, bool eventual) {
Result<KVStorePendingList::Handle> res;

fastly::KVListOptions list_options{eventual ? KV_LIST_MODE_EVENTUAL : KV_LIST_MODE_STRONG};

uint32_t options_mask = 0;
if (cursor.has_value()) {
options_mask |= KV_LIST_CONFIG_CURSOR;
list_options.cursor = reinterpret_cast<const uint8_t *>(cursor.value().data());
list_options.cursor_len = cursor.value().length();
}
if (limit.has_value()) {
options_mask |= KV_LIST_CONFIG_LIMIT;
list_options.limit = limit.value();
}
if (prefix.has_value()) {
options_mask |= KV_LIST_CONFIG_PREFIX;
list_options.prefix = reinterpret_cast<const uint8_t *>(prefix.value().data());
list_options.prefix_len = prefix.value().length();
}

KVStore::Handle ret;
fastly::fastly_host_error err;
if (!convert_result(fastly::kv_store_list(this->handle, options_mask, &list_options, &ret),
&err)) {
res.emplace_err(err);
} else {
fprintf(stderr, "GOT LOOKUP HANDLE %d\n", ret);
res.emplace(ret);
}
return res;
}

FastlyResult<HttpBody, FastlyKVError> KVStorePendingList::wait() {
FastlyResult<HttpBody, FastlyKVError> res;

fastly::fastly_host_error err;
HttpBody body{};

uint32_t gen_out;
fastly::fastly_kv_error kv_err;
uint8_t *metadata_buf = reinterpret_cast<uint8_t *>(cabi_malloc(HOSTCALL_BUFFER_LEN, 1));
size_t metadata_nwritten;

if (!convert_result(fastly::kv_store_lookup_wait(this->handle, &body.handle, metadata_buf,
HOSTCALL_BUFFER_LEN, &metadata_nwritten,
&gen_out, &kv_err),
&err)) {
res.emplace_err(make_fastly_kv_error(kv_err));
} else {
res.emplace(body);
}

return res;
}

// Result<std::optional<HttpBody>> KVStore::lookup(std::string_view name) {
// Result<std::optional<HttpBody>> res;

Expand Down
Loading

0 comments on commit b21086c

Please sign in to comment.