Skip to content

Commit 12d5bea

Browse files
authored
Port cl/361876089: JniRunnable: Fix a deadlock when Detach() is called from Run() (#330)
1 parent 56c6592 commit 12d5bea

File tree

4 files changed

+148
-58
lines changed

4 files changed

+148
-58
lines changed

firestore/src/android/jni_runnable_android.cc

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#include "firestore/src/android/jni_runnable_android.h"
22

3+
#include "app/src/assert.h"
34
#include "app/src/util_android.h"
45
#include "firestore/src/jni/declaration.h"
56
#include "firestore/src/jni/env.h"
@@ -30,9 +31,7 @@ Method<Task> kRunOnNewThread("runOnNewThread",
3031
Constructor<Object> kConstructor("(J)V");
3132

3233
void NativeRun(JNIEnv* env, jobject java_object, jlong data) {
33-
if (data == 0) {
34-
return;
35-
}
34+
FIREBASE_ASSERT_MESSAGE(data != 0, "NativeRun() invoked with data==0");
3635
reinterpret_cast<JniRunnableBase*>(data)->Run();
3736
}
3837

firestore/src/android/jni_runnable_android.h

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -52,12 +52,11 @@ class JniRunnableBase {
5252
* object's `run()` method will do nothing and complete as if successful.
5353
*
5454
* This method will block until all active invocations of `Run()` have
55-
* completed, and will cause new invocations of the Java `Runnable` object's
56-
* `run()` that occur while this method is blocked to also block until this
57-
* method completes.
55+
* completed.
5856
*
59-
* Calling `Detach()` multiple times is allowed, but invocations after the
60-
* first invocation have no effect.
57+
* This method may be safely invoked multiple times. Subsequent invocations
58+
* have no side effects but will still block while there are active
59+
* invocations of `Run()`.
6160
*/
6261
void Detach(jni::Env& env);
6362

@@ -98,7 +97,8 @@ class JniRunnableBase {
9897
* A proxy for a Java `Runnable` that calls a C++ function.
9998
*
10099
* The template parameter `CallbackT` is typically a lambda or function pointer;
101-
* it can be anything that can be "invoked" with zero arguments.
100+
* it can be anything that can be "invoked" with either zero arguments or one
101+
* argument whose type is `JniRunnableBase&`.
102102
*
103103
* Example:
104104
*
@@ -118,9 +118,25 @@ class JniRunnable : public JniRunnableBase {
118118
JniRunnable(jni::Env& env, CallbackT callback)
119119
: JniRunnableBase(env), callback_(firebase::Move(callback)) {}
120120

121-
void Run() override { callback_(); }
121+
void Run() override { Run(*this, callback_); }
122122

123123
private:
124+
// These two static overloads of `Run()` use SFINAE to invoke the callback
125+
// with zero arguments or with one argument, depending on the signature of the
126+
// callback. If the callback takes one argument then a reference to the
127+
// `JniRunnable` object is specified for that argument.
128+
template <typename JniRunnableType, typename ZeroArgCallback>
129+
static auto Run(JniRunnableType&, ZeroArgCallback callback)
130+
-> decltype(callback()) {
131+
return callback();
132+
}
133+
134+
template <typename JniRunnableType, typename OneArgCallback>
135+
static auto Run(JniRunnableType& runnable, OneArgCallback callback)
136+
-> decltype(callback(runnable)) {
137+
return callback(runnable);
138+
}
139+
124140
CallbackT callback_;
125141
};
126142

firestore/src/tests/android/jni_runnable_android_test.cc

Lines changed: 106 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
#include "firestore/src/android/jni_runnable_android.h"
22

3+
#include "app/memory/atomic.h"
4+
#include "app/src/mutex.h"
35
#include "firestore/src/jni/declaration.h"
46
#include "firestore/src/jni/object.h"
57
#include "firestore/src/jni/ownership.h"
@@ -18,6 +20,7 @@ using jni::Global;
1820
using jni::Local;
1921
using jni::Method;
2022
using jni::Object;
23+
using jni::StaticField;
2124
using jni::StaticMethod;
2225
using jni::Task;
2326
using jni::Throwable;
@@ -27,14 +30,18 @@ Method<Object> kLooperGetThread("getThread", "()Ljava/lang/Thread;");
2730
Method<void> kRunnableRun("run", "()V");
2831
StaticMethod<Object> kCurrentThread("currentThread", "()Ljava/lang/Thread;");
2932
Method<jlong> kThreadGetId("getId", "()J");
33+
Method<Object> kThreadGetState("getState", "()Ljava/lang/Thread$State;");
34+
StaticField<Object> kThreadStateBlocked("BLOCKED", "Ljava/lang/Thread$State;");
3035

3136
class JniRunnableTest : public FirestoreAndroidIntegrationTest {
3237
public:
3338
void SetUp() override {
3439
FirestoreAndroidIntegrationTest::SetUp();
3540
loader().LoadClass("android/os/Looper", kGetMainLooper, kLooperGetThread);
3641
loader().LoadClass("java/lang/Runnable", kRunnableRun);
37-
loader().LoadClass("java/lang/Thread", kCurrentThread, kThreadGetId);
42+
loader().LoadClass("java/lang/Thread", kCurrentThread, kThreadGetId,
43+
kThreadGetState);
44+
loader().LoadClass("java/lang/Thread$State", kThreadStateBlocked);
3845
ASSERT_TRUE(loader().ok());
3946
}
4047
};
@@ -56,6 +63,16 @@ jlong GetMainThreadId(Env& env) {
5663
return env.Call(main_thread, kThreadGetId);
5764
}
5865

66+
/**
67+
* Returns whether or not the given thread is in the "blocked" state.
68+
* See java.lang.Thread.State.BLOCKED.
69+
*/
70+
bool IsThreadBlocked(Env& env, Object& thread) {
71+
Local<Object> actual_state = env.Call(thread, kThreadGetState);
72+
Local<Object> expected_state = env.Get(kThreadStateBlocked);
73+
return Object::Equals(env, expected_state, actual_state);
74+
}
75+
5976
TEST_F(JniRunnableTest, JavaRunCallsCppRun) {
6077
Env env;
6178
bool invoked = false;
@@ -145,6 +162,27 @@ TEST_F(JniRunnableTest, DetachDetachesEvenIfAnExceptionIsPending) {
145162
EXPECT_TRUE(env.ok());
146163
}
147164

165+
// Verify that b/181129657 does not regress; that is, calling `Detach()` from
166+
// `Run()` should not deadlock.
167+
TEST_F(JniRunnableTest, DetachCanBeCalledFromRun) {
168+
Env env;
169+
int run_count = 0;
170+
auto runnable = MakeJniRunnable(env, [&run_count](JniRunnableBase& runnable) {
171+
++run_count;
172+
Env env;
173+
runnable.Detach(env);
174+
});
175+
Local<Object> java_runnable = runnable.GetJavaRunnable();
176+
177+
// Call `run()` twice to verify that the call to `Detach()` successfully
178+
// detaches and the second `run()` invocation does not call C++ `Run()`.
179+
env.Call(java_runnable, kRunnableRun);
180+
env.Call(java_runnable, kRunnableRun);
181+
182+
EXPECT_TRUE(env.ok());
183+
EXPECT_EQ(run_count, 1);
184+
}
185+
148186
TEST_F(JniRunnableTest, DestructionCausesJavaRunToDoNothing) {
149187
Env env;
150188
bool invoked = false;
@@ -191,29 +229,21 @@ TEST_F(JniRunnableTest, RunOnMainThreadTaskFailsIfRunThrowsException) {
191229
}
192230

193231
TEST_F(JniRunnableTest, RunOnMainThreadRunsSynchronouslyFromMainThread) {
194-
class ChainedMainThreadJniRunnable : public JniRunnableBase {
195-
public:
196-
using JniRunnableBase::JniRunnableBase;
197-
198-
void Run() override {
199-
Env env;
200-
EXPECT_EQ(GetCurrentThreadId(env), GetMainThreadId(env));
201-
if (is_nested_call_) {
202-
return;
203-
}
204-
is_nested_call_ = true;
205-
Local<Task> task = RunOnMainThread(env);
206-
EXPECT_TRUE(task.IsComplete(env));
207-
EXPECT_TRUE(task.IsSuccessful(env));
208-
is_nested_call_ = false;
209-
}
210-
211-
private:
212-
bool is_nested_call_ = false;
213-
};
214-
215232
Env env;
216-
ChainedMainThreadJniRunnable runnable(env);
233+
bool is_recursive_call = false;
234+
auto runnable =
235+
MakeJniRunnable(env, [&is_recursive_call](JniRunnableBase& runnable) {
236+
Env env;
237+
EXPECT_EQ(GetCurrentThreadId(env), GetMainThreadId(env));
238+
if (is_recursive_call) {
239+
return;
240+
}
241+
is_recursive_call = true;
242+
Local<Task> task = runnable.RunOnMainThread(env);
243+
EXPECT_TRUE(task.IsComplete(env));
244+
EXPECT_TRUE(task.IsSuccessful(env));
245+
is_recursive_call = false;
246+
});
217247

218248
Local<Task> task = runnable.RunOnMainThread(env);
219249

@@ -252,6 +282,59 @@ TEST_F(JniRunnableTest, RunOnNewThreadTaskFailsIfRunThrowsException) {
252282
EXPECT_TRUE(env.IsSameObject(exception, thrown_exception));
253283
}
254284

285+
TEST_F(JniRunnableTest, DetachReturnsAfterLastRunOnAnotherThreadCompletes) {
286+
Env env;
287+
compat::Atomic<int32_t> runnable1_run_invoke_count;
288+
runnable1_run_invoke_count.store(0);
289+
Mutex detach_thread_mutex;
290+
Global<Object> detach_thread;
291+
292+
auto runnable1 = MakeJniRunnable(
293+
env, [&runnable1_run_invoke_count, &detach_thread, &detach_thread_mutex] {
294+
runnable1_run_invoke_count.fetch_add(1);
295+
Env env;
296+
// Wait for `detach()` to be called and start blocking; then, return to
297+
// allow `detach()` to unblock and do its job.
298+
while (env.ok()) {
299+
MutexLock lock(detach_thread_mutex);
300+
if (detach_thread && IsThreadBlocked(env, detach_thread)) {
301+
break;
302+
}
303+
}
304+
EXPECT_TRUE(env.ok()) << "IsThreadBlocked() failed with an exception";
305+
});
306+
307+
auto runnable2 =
308+
MakeJniRunnable(env, [&runnable1, &detach_thread, &detach_thread_mutex] {
309+
Env env;
310+
{
311+
MutexLock lock(detach_thread_mutex);
312+
detach_thread = env.Call(kCurrentThread);
313+
}
314+
runnable1.Detach(env);
315+
EXPECT_TRUE(env.ok()) << "Detach() failed with an exception";
316+
});
317+
318+
// Wait for the `runnable1.Run()` to start to ensure that the lock is held.
319+
Local<Task> task1 = runnable1.RunOnNewThread(env);
320+
while (true) {
321+
if (runnable1_run_invoke_count.load() != 0) {
322+
break;
323+
}
324+
}
325+
326+
// Start a new thread to call `runnable1.Detach()`.
327+
Local<Task> task2 = runnable2.RunOnNewThread(env);
328+
329+
Await(env, task1);
330+
Await(env, task2);
331+
332+
// Invoke `run()` again and ensure that `Detach()` successfully did its job;
333+
// that is, verify that `Run()` is not invoked.
334+
env.Call(runnable1.GetJavaRunnable(), kRunnableRun);
335+
EXPECT_EQ(runnable1_run_invoke_count.load(), 1);
336+
}
337+
255338
} // namespace
256339
} // namespace firestore
257340
} // namespace firebase

firestore/src_java/com/google/firebase/firestore/internal/cpp/JniRunnable.java

Lines changed: 17 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,11 @@
44
import android.os.Looper;
55
import com.google.android.gms.tasks.Task;
66
import com.google.android.gms.tasks.TaskCompletionSource;
7-
import java.util.concurrent.locks.ReentrantReadWriteLock;
87

98
/** A {@link Runnable} whose {@link #run} method calls a native function. */
109
public final class JniRunnable implements Runnable {
1110

12-
private final ReentrantReadWriteLock.ReadLock readLock;
13-
private final ReentrantReadWriteLock.WriteLock writeLock;
14-
11+
private final Object lock = new Object();
1512
private long data;
1613

1714
/**
@@ -26,29 +23,26 @@ public JniRunnable(long data) {
2623
"data==0 is forbidden because 0 is reserved to indicate that we are detached from the"
2724
+ " C++ function");
2825
}
29-
ReentrantReadWriteLock lock = new ReentrantReadWriteLock(/* fair= */ true);
30-
readLock = lock.readLock();
31-
writeLock = lock.writeLock();
3226
this.data = data;
3327
}
3428

3529
/**
36-
* Invokes the C++ method encapsulated by this object.
30+
* Invokes the C++ function encapsulated by this object.
3731
*
3832
* <p>If {@link #detach} has been invoked then this method does nothing and returns as if
3933
* successful.
40-
*
41-
* <p>This method <em>will</em> block if there is a thread blocked in {@link #detach}; otherwise,
42-
* it will call the C++ function without blocking. This may even result in concurrent/parallel
43-
* calls to the C++ function if {@link #run} is invoked concurrently.
4434
*/
4535
@Override
4636
public void run() {
47-
readLock.lock();
48-
try {
37+
// NOTE: Because of the `synchronized` block below, the native function will not be called
38+
// concurrently. If concurrent invocations are desired, then this class can be modified with a
39+
// more complicated synchronization mechanism.
40+
// e.g. https://gist.github.com/dconeybe/2d95fbc75f88de58a49804df5c55157b
41+
synchronized (lock) {
42+
if (data == 0) {
43+
return;
44+
}
4945
nativeRun(data);
50-
} finally {
51-
readLock.unlock();
5246
}
5347
}
5448

@@ -58,18 +52,16 @@ public void run() {
5852
* <p>After this method returns, all future invocations of {@link #run} will do nothing and return
5953
* as if successful.
6054
*
61-
* <p>This method <em>will</em> block if there are active invocations of {@link #run}. Once all
62-
* active invocations of {@link #run} have completed, then this method will proceed and return
63-
* nearly instantly. Any invocations of {@link #run} that occur while {@link #detach} is blocked
64-
* will also block, allowing the number of active invocations of {@link #run} to eventually reach
65-
* zero and allow this method to proceed.
55+
* <p>This method blocks until all invocations of the native function called from {@link #run}
56+
* complete; therefore, when this method returns it is safe to delete any data that would be
57+
* referenced by the native function.
58+
*
59+
* <p>This method may be safely invoked multiple times. Subsequent invocations have no side
60+
* effects but will still block while there are active invocations of the native function.
6661
*/
6762
public void detach() {
68-
writeLock.lock();
69-
try {
63+
synchronized (lock) {
7064
data = 0;
71-
} finally {
72-
writeLock.unlock();
7365
}
7466
}
7567

0 commit comments

Comments
 (0)