Skip to content

Commit e5c2539

Browse files
committed
Fixup deadlock when hammering futures of a certain type
1 parent 5177a30 commit e5c2539

File tree

2 files changed

+42
-15
lines changed

2 files changed

+42
-15
lines changed

cpp/includes/UniffiCallInvoker.h

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@
55
*/
66
#pragma once
77
#include <ReactCommon/CallInvoker.h>
8-
#include <future>
8+
#include <condition_variable>
99
#include <jsi/jsi.h>
1010
#include <memory>
11+
#include <mutex>
1112
#include <thread>
1213

1314
namespace uniffi_runtime {
@@ -57,23 +58,26 @@ class UniffiCallInvoker {
5758
if (std::this_thread::get_id() == threadId_) {
5859
func(rt);
5960
} else {
60-
std::promise<void> promise;
61-
auto future = promise.get_future();
61+
std::mutex mtx;
62+
std::condition_variable cv;
63+
bool done = false;
6264
// The runtime argument was added to CallFunc in
6365
// https://github.com/facebook/react-native/pull/43375
6466
//
65-
// Once that is released, there will be a deprecation period.
66-
//
67-
// Any time during the deprecation period, we can switch `&rt`
68-
// from being a captured variable to being an argument, i.e.
69-
// commenting out one line, and uncommenting the other.
70-
std::function<void()> wrapper = [&func, &promise, &rt]() {
71-
// react::CallFunc wrapper = [&func, &promise](jsi::Runtime &rt) {
67+
// This can be changed once that change is released.
68+
// react::CallFunc wrapper = [&func, &mtx, &cv, &done](jsi::Runtime &rt) {
69+
std::function<void()> wrapper = [&func, &rt, &mtx, &cv, &done]() {
7270
func(rt);
73-
promise.set_value();
71+
{
72+
std::lock_guard<std::mutex> lock(mtx);
73+
done = true;
74+
}
75+
cv.notify_one();
7476
};
7577
callInvoker_->invokeAsync(std::move(wrapper));
76-
future.wait();
78+
79+
std::unique_lock<std::mutex> lock(mtx);
80+
cv.wait(lock, [&done] { return done; });
7781
}
7882
}
7983
};

typescript/src/async-rust-call.ts

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,12 @@ type PollFunc = (
3333
handle: UniffiHandle,
3434
) => void;
3535

36+
// Calls setTimeout and then resolves the promise.
37+
// This may be used as a simple yield.
38+
export async function delayPromise(delayMs: number): Promise<void> {
39+
return new Promise((resolve) => setTimeout(resolve, delayMs));
40+
}
41+
3642
/**
3743
* This method calls an asynchronous method on the Rust side.
3844
*
@@ -71,7 +77,7 @@ export async function uniffiRustCallAsync<F, T>(
7177
pollResult = await pollRust((handle) => {
7278
pollFunc(rustFuture, uniffiFutureContinuationCallback, handle);
7379
});
74-
} while (pollResult != UNIFFI_RUST_FUTURE_POLL_READY);
80+
} while (pollResult !== UNIFFI_RUST_FUTURE_POLL_READY);
7581

7682
// Now it's ready, all we need to do is pick up the result (and error).
7783
return liftFunc(
@@ -85,7 +91,7 @@ export async function uniffiRustCallAsync<F, T>(
8591
// #RUST_TASK_CANCELLATION: the unused `cancelFunc` function should be exposed
8692
// to client code in order for clients to be able to cancel the running Rust task.
8793
} finally {
88-
freeFunc(rustFuture);
94+
setTimeout(() => freeFunc(rustFuture), 0);
8995
}
9096
}
9197

@@ -110,7 +116,24 @@ const uniffiFutureContinuationCallback: UniffiRustFutureContinuationCallback = (
110116
pollResult: number,
111117
) => {
112118
const resolve = UNIFFI_RUST_FUTURE_RESOLVER_MAP.remove(handle);
113-
resolve(pollResult);
119+
if (pollResult === UNIFFI_RUST_FUTURE_POLL_READY) {
120+
resolve(pollResult);
121+
} else {
122+
// From https://github.com/mozilla/uniffi-rs/pull/1837/files#diff-8a28c9cf1245b4f714d406ea4044d68e1000099928eaca1afb504ccbc008fe9fR35-R37
123+
//
124+
// > WARNING: the call to [rust_future_poll] must be scheduled to happen soon after the callback is
125+
// > called, but not inside the callback itself. If [rust_future_poll] is called inside the
126+
// > callback, some futures will deadlock and our scheduler code might as well.
127+
//
128+
// This delay is to ensure that `uniffiFutureContinuationCallback` returns before the next poll, i.e.
129+
// so that the next poll is outside of this callback.
130+
//
131+
// The length of the delay seems to be significant (at least in tests which hammer a network).
132+
// I would like to understand this more: I am still seeing deadlocks when this drops below its current
133+
// delay, but these maybe related to a different issue, as alluded to in
134+
// https://github.com/mozilla/uniffi-rs/pull/1901
135+
setTimeout(() => resolve(pollResult), 20);
136+
}
114137
};
115138

116139
// For testing only.

0 commit comments

Comments
 (0)