Skip to content
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

memory leak and already borrowed fixes are in experimental now #2424

Merged
merged 2 commits into from
Jan 5, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Binary file modified canister_templates/experimental.wasm
Binary file not shown.
Binary file modified canister_templates/stable.wasm
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"expectedErrors": [
"get_balance failed: MalformedAddress",
"get_utxos failed: MalformedAddress",
"send_transaction failed: MalformedTransaction"
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ candid = "0.10.11"
candid_parser = "0.1.2"
ic-stable-structures = "0.6.7"
open_value_sharing = { path = "../open_value_sharing" }
scopeguard = "1.2.0"
slotmap = "=1.0.7"
sha2 = "0.10.8"
serde = "1.0.217"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
use wasmedge_quickjs::{AsObject, Context, JsFn, JsValue};

use crate::{run_event_loop, RUNTIME};
use crate::run_event_loop;

pub struct NativeFunction;
impl JsFn for NativeFunction {
fn call(_context: &mut Context, _this_val: JsValue, argv: &[JsValue]) -> JsValue {
fn call(context: &mut Context, _this_val: JsValue, argv: &[JsValue]) -> JsValue {
let promise_id = if let JsValue::String(js_string) = argv.get(0).unwrap() {
js_string.to_string()
} else {
Expand Down Expand Up @@ -38,84 +38,100 @@ impl JsFn for NativeFunction {
};
let payment: u128 = payment_string.parse().unwrap();

let mut context_clone = context.clone();

ic_cdk::spawn(async move {
let mut context_clone_cleanup = context_clone.clone();

// My understanding of how this works
// scopeguard will execute its closure at the end of the scope
// After a successful or unsuccessful cross-canister call (await point)
// the closure will run, cleaning up the global promise callbacks
// Even during a trap, the IC will ensure that the closure runs in its own call
// thus allowing us to recover from a trap and persist that state
let _cleanup = scopeguard::guard((), |_| {
let global = context_clone_cleanup.get_global();

let reject_id = format!("_reject_{}", promise_id);
let resolve_id = format!("_resolve_{}", promise_id);

let reject_callbacks = global.get("_azleRejectCallbacks");
let resolve_callbacks = global.get("_azleResolveCallbacks");

reject_callbacks.to_obj().unwrap().delete(&reject_id);
resolve_callbacks.to_obj().unwrap().delete(&resolve_id);
});

let call_result =
ic_cdk::api::call::call_raw128(canister_id, &method, &args_raw, payment).await;

RUNTIME.with(|runtime| {
let mut runtime = runtime.borrow_mut();
let runtime = runtime.as_mut().unwrap();

runtime.run_with_context(|context| {
let global = context.get_global();

let (should_resolve, js_value) = match &call_result {
Ok(candid_bytes) => {
let candid_bytes_js_value: JsValue =
context.new_array_buffer(candid_bytes).into();

(true, candid_bytes_js_value)
}
Err(err) => {
let err_js_value: JsValue = context
.new_error(&format!(
"Rejection code {rejection_code}, {error_message}",
rejection_code = (err.0 as i32).to_string(),
error_message = err.1
))
.into();

(false, err_js_value)
}
};

if should_resolve {
let resolve = global
.get("_azleResolveIds")
.to_obj()
.unwrap()
.get(format!("_resolve_{promise_id}").as_str())
.to_function()
.unwrap();

let result = resolve.call(&[js_value.clone()]);

// TODO error handling is mostly done in JS right now
// TODO we would really like wasmedge-quickjs to add
// TODO good error info to JsException and move error handling
// TODO out of our own code
match &result {
wasmedge_quickjs::JsValue::Exception(js_exception) => {
js_exception.dump_error();
panic!("TODO needs error info");
}
_ => run_event_loop(context),
};
} else {
let reject = global
.get("_azleRejectIds")
.to_obj()
.unwrap()
.get(format!("_reject_{promise_id}").as_str())
.to_function()
.unwrap();

let result = reject.call(&[js_value.clone()]);

// TODO error handling is mostly done in JS right now
// TODO we would really like wasmedge-quickjs to add
// TODO good error info to JsException and move error handling
// TODO out of our own code
match &result {
wasmedge_quickjs::JsValue::Exception(js_exception) => {
js_exception.dump_error();
panic!("TODO needs error info");
}
_ => run_event_loop(context),
};
let global = context_clone.get_global();

let (should_resolve, js_value) = match &call_result {
Ok(candid_bytes) => {
let candid_bytes_js_value: JsValue =
context_clone.new_array_buffer(candid_bytes).into();

(true, candid_bytes_js_value)
}
Err(err) => {
let err_js_value: JsValue = context_clone
.new_error(&format!(
"Rejection code {rejection_code}, {error_message}",
rejection_code = (err.0 as i32).to_string(),
error_message = err.1
))
.into();

(false, err_js_value)
}
};

if should_resolve {
let resolve = global
.get("_azleResolveCallbacks")
.to_obj()
.unwrap()
.get(format!("_resolve_{promise_id}").as_str())
.to_function()
.unwrap();

let result = resolve.call(&[js_value.clone()]);

// TODO error handling is mostly done in JS right now
// TODO we would really like wasmedge-quickjs to add
// TODO good error info to JsException and move error handling
// TODO out of our own code
match &result {
wasmedge_quickjs::JsValue::Exception(js_exception) => {
js_exception.dump_error();
panic!("TODO needs error info");
}
});
});
_ => run_event_loop(&mut context_clone),
};
} else {
let reject = global
.get("_azleRejectCallbacks")
.to_obj()
.unwrap()
.get(format!("_reject_{promise_id}").as_str())
.to_function()
.unwrap();

let result = reject.call(&[js_value.clone()]);

// TODO error handling is mostly done in JS right now
// TODO we would really like wasmedge-quickjs to add
// TODO good error info to JsException and move error handling
// TODO out of our own code
match &result {
wasmedge_quickjs::JsValue::Exception(js_exception) => {
js_exception.dump_error();
panic!("TODO needs error info");
}
_ => run_event_loop(&mut context_clone),
};
}
});

JsValue::UnDefined
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,8 @@ fn cleanup(ctx: Ctx, promise_id: &str) -> Result<(), Box<dyn Error>> {

let globals = ctx.clone().globals();

let reject_ids = globals.get::<_, Object>("_azleRejectIds")?;
let resolve_ids = globals.get::<_, Object>("_azleResolveIds")?;
let reject_ids = globals.get::<_, Object>("_azleRejectCallbacks")?;
let resolve_ids = globals.get::<_, Object>("_azleResolveCallbacks")?;

reject_ids.remove(&reject_id)?;
resolve_ids.remove(&resolve_id)?;
Expand Down Expand Up @@ -147,9 +147,9 @@ fn get_resolve_or_reject_global_object(
let globals = ctx.globals();

if should_resolve {
Ok(globals.get("_azleResolveIds")?)
Ok(globals.get("_azleResolveCallbacks")?)
} else {
Ok(globals.get("_azleRejectIds")?)
Ok(globals.get("_azleRejectCallbacks")?)
}
}

Expand Down
36 changes: 11 additions & 25 deletions src/lib/experimental/ic/call_raw.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,42 +37,28 @@ export function callRaw(
const globalResolveId = `_resolve_${promiseId}`;
const globalRejectId = `_reject_${promiseId}`;

// TODO perhaps we should be more robust
// TODO for example, we can keep the time with these
// TODO if they are over a certain amount old we can delete them
globalThis._azleResolveIds[globalResolveId] = (
globalThis._azleResolveCallbacks[globalResolveId] = (
bytes: ArrayBuffer
): void => {
resolve(new Uint8Array(bytes));

delete globalThis._azleResolveIds[globalResolveId];
delete globalThis._azleRejectIds[globalRejectId];
};

globalThis._azleRejectIds[globalRejectId] = (error: any): void => {
globalThis._azleRejectCallbacks[globalRejectId] = (
error: any
): void => {
reject(error);

delete globalThis._azleResolveIds[globalResolveId];
delete globalThis._azleRejectIds[globalRejectId];
};

const canisterIdBytes = canisterId.toUint8Array().buffer;
const argsRawBuffer = argsRaw.buffer;
const paymentString = payment.toString();

// TODO consider finally, what if deletion goes wrong
try {
globalThis._azleIcExperimental.callRaw(
promiseId,
canisterIdBytes,
method,
argsRawBuffer,
paymentString
);
} catch (error) {
delete globalThis._azleResolveIds[globalResolveId];
delete globalThis._azleRejectIds[globalRejectId];
throw error;
}
globalThis._azleIcExperimental.callRaw(
promiseId,
canisterIdBytes,
method,
argsRawBuffer,
paymentString
);
});
}
10 changes: 6 additions & 4 deletions src/lib/stable/globals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,11 @@ declare global {
// eslint-disable-next-line no-var
var _azleRecordBenchmarks: boolean;
// eslint-disable-next-line no-var
var _azleRejectIds: { [key: string]: (err: any) => void };
var _azleRejectCallbacks: { [key: string]: (err: any) => void };
// eslint-disable-next-line no-var
var _azleResolveIds: { [key: string]: (buf: Uint8Array) => void };
var _azleResolveCallbacks: {
[key: string]: (buf: Uint8Array | ArrayBuffer) => void;
};
// eslint-disable-next-line no-var
var _azleTimerCallbacks: { [key: string]: () => void };
}
Expand All @@ -54,9 +56,9 @@ if (globalThis._azleInsideCanister === true) {

globalThis._azleIcTimers = {};

globalThis._azleRejectIds = {};
globalThis._azleRejectCallbacks = {};

globalThis._azleResolveIds = {};
globalThis._azleResolveCallbacks = {};

globalThis.TextDecoder = TextDecoder;
globalThis.TextEncoder = TextEncoder;
Expand Down
6 changes: 4 additions & 2 deletions src/lib/stable/ic_apis/call.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export async function call<Args extends any[] | undefined, Return = any>(
const returnTypeIdl = options?.returnIdlType;
const raw = options?.raw;

globalThis._azleResolveIds[globalResolveId] = (
globalThis._azleResolveCallbacks[globalResolveId] = (
result: Uint8Array | ArrayBuffer
): void => {
if (raw !== undefined) {
Expand All @@ -45,7 +45,9 @@ export async function call<Args extends any[] | undefined, Return = any>(
}
};

globalThis._azleRejectIds[globalRejectId] = (error: any): void => {
globalThis._azleRejectCallbacks[globalRejectId] = (
error: any
): void => {
reject(error);
};

Expand Down
Loading