Firebase callbacks work during testing but not dur

2019-08-26 02:18发布

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 ?

1条回答
Root(大扎)
2楼-- · 2019-08-26 02:29

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.

查看更多
登录 后发表回答