I'm using Firebase in an Android app. The app sets up something like a groupchat and allows users to join. The users get a key and can then connect themselves to the corresponding DatabaseReference. We need a check whether the key is valid. Therefore, when the group is created, the host automatically adds himself to a list of users. Then all new clients can check if there are entries in the list. If the list is empty, the key is invalid.
This means that I need to wait for the completion of a setValue call. Firebase has many callbacks that can tell me about this, but they are quite problematic. Sometimes, they simply aren't called. I've already asked a question about this non-deterministic behaviour here: How to listen for Firebase setValue completion
Now I've found a new problem with those callbacks. I've changed my infrastructure to an asynchronous setup. All interactions are packaged into Callables and submitted to an ExecutorService. The result is a Future<>. Now, if I want to wait for something to complete, I can just wait on that future. Inside of the Future, I still need to use the Firebase callbacks.
The code is in a wrapper class called DBConnection.
Here is my code for creating a new group (party) :
public Future<DBState> createParty() {
// assert entries
assertState(DBState.SignedIn);
// process for state transition
Callable<DBState> creationProcess = new Callable<DBState>() {
@Override
public DBState call() throws Exception {
lock.lock();
try {
// create a new party
ourPartyDatabaseReference = partiesDatabaseReference.push();
usersDatabaseReference = ourPartyDatabaseReference.child("users");
// try every remedy for the missing callbacks
firebaseDatabase.goOnline();
ourPartyDatabaseReference.keepSynced(true);
usersDatabaseReference.keepSynced(true);
// push a value to the users database
// this way the database reference is actually created
// and new users can search for existing users when they connect
// we can only continue after that task has been completed
// add listeners for success and failure and wait for their completion
// TODO: we need information that this task has been finished
// but no callback seems to work
// onSuccess, onCompletion on the task are not reliable
// and the child and value event listeners on the userDatabaseReference are not reliable, too
final CountDownLatch waiter = new CountDownLatch(1);
usersDatabaseReference.addValueEventListener(new ValueEventListener() {
@Override
public void onDataChange(DataSnapshot dataSnapshot) {
waiter.countDown();
}
@Override
public void onCancelled(DatabaseError databaseError) {
waiter.countDown();
}
});
Task addingTask = usersDatabaseReference.child(user.getUid()).setValue(true);
addingTask.addOnSuccessListener(new OnSuccessListener() {
@Override
public void onSuccess(Object o) {
waiter.countDown();
}
});
addingTask.addOnCompleteListener(new OnCompleteListener() {
@Override
public void onComplete(@NonNull Task task) {
waiter.countDown();
}
});
try {
waiter.await();
} catch (InterruptedException ex) {
}
connectToParty();
} finally {
lock.unlock();
}
// if we could connect, we are now DBState.Connected,
// otherwise we are still DBState.SignedIn
return state;
}
};
// start process
return executorService.submit(creationProcess);
}
You can use it like this:
Future<DBState> creationFuture = dbConnection.createParty();
try {
creationFuture.get(TIMEOUT, TimeUnit.MILLISECONDS);
} catch (InterruptedException ex) {
throw new AssertionError("there should be no interrupt");
}catch (TimeoutException ex) {
throw new AssertionError("timeout in party creation");
}catch (ExecutionException ex) {
throw new AssertionError("concurrent execution exception");
}
I've written tests for this.
And in the tests, everything works fine. I've executed the canCreateParty
test at least a dozen times now.
To make sure, that the callbacks work, I've increased the CountDownLatch
to 3 counts and added breakpoints to the countDowns
. Every countDown
is reached.
But at runtime, no callback is ever called. None of the breakpoints are reached, the waiting for the future eventually times out.
The strangest part is: I have the firebase console open right next to the emulator. I can see how new parties are created and users are added. Both for the tests and at runtime, the party creation works just as expected and a new user is added. Why am I getting no callback at runtime ?
The reason is that Firebase always calls its callbacks from the main thread.
The "main" thread in my tests is called something like "junittestrunnerXXX". And Firebase creates a new thread called "main" to call the callbacks.
At runtime, the "main" thread is the actual "main" thread. If I call get() on that, it is blocked for good. Firebase checks if this thread exists and since it already exists and since it is blocked, nothing happens.