Skip to content

Firebase Callbacks Should Run on Main Thread #600

Open
@dginovker

Description

@dginovker

Hello Firebase!

Our team has been using Firebase with Unity for over 2 years now to enhance our mobile games, and we've built great infrastructure around it. Unfortunately, that also means we've had our fair share of difficult-to-diagnose libc and segabrt crashes, whose symptoms usually lead to closed Github issues - A repeating point of frustration. Doing a retrospect on many of our crashes in 2021 and 2022 found that many of the issues we faced internally, as well as many issues we see on this repo, actually come from the fact the Unity API is not thread-safe and Firebase callbacks usually happen off the main thread.

Let's look at some examples:

Firebase.FirebaseApp.CheckDependenciesAsync().ContinueWith(checkTask => {
  // Peek at the status and see if we need to try to fix dependencies.
  Firebase.DependencyStatus status = checkTask.Result;
  if (status != Firebase.DependencyStatus.Available) {
    return Firebase.FirebaseApp.FixDependenciesAsync().ContinueWith(t => {
      return Firebase.FirebaseApp.CheckDependenciesAsync();
    }).Unwrap();
  } else {
    return checkTask;
  }
}).Unwrap().ContinueWith(task => {
  dependencyStatus = task.Result;
  if (dependencyStatus == Firebase.DependencyStatus.Available) {
    // TODO: Continue with Firebase initialization.
  } else {
    Debug.LogError(
      "Could not resolve all Firebase dependencies: " + dependencyStatus);
  }
});

This code is taken directly from the docs people use to get started. TheDebug.LogError call might lead a developer into thinking all is normal, but Debug is one of the only thread-safe Unity APIs out there. When they start to make non-thread safe callbacks and initializations, they invite all sorts of race conditions or even crashes that go undetected while in the Editor.

public void Start() {
  Firebase.Messaging.FirebaseMessaging.TokenReceived += OnTokenReceived;
  Firebase.Messaging.FirebaseMessaging.MessageReceived += OnMessageReceived;
}

public void OnTokenReceived(object sender, Firebase.Messaging.TokenReceivedEventArgs token) {
  UnityEngine.Debug.Log("Received Registration Token: " + token.Token);
}

public void OnMessageReceived(object sender, Firebase.Messaging.MessageReceivedEventArgs e) {
  UnityEngine.Debug.Log("Received a new message from: " + e.Message.From);
}

This is an example from the Firebase Messaging docs, and it's even more sinister, because there is no indication OnMessageReceived is being run off the main thread. As users of the SDK, oftentimes our code starts out simple, with the original developer blissfully unaware of the impending doom when a colleague modifies a random function that's part of the callback. With Messaging in particular (which doesn't work on Editor), the don't even know they've created a (SIGABRT) crash unless they happen to test this code path on device!

Multithreaded code is great for performance, but it should be an opt-in experience. Experienced developers know the risks and design decisions that come with incorporating multithreaded code, but juniors developers can struggle when thrown in with very little guidance.

There are 3 main problems that arise from off main thread callbacks:

  • There's a fundamental best-practices juxtaposition where Firebase for Unity SDK gives the user non-main thread callbacks, but Unity isn't thread safe
    • i.e. Unhandled exceptions in threads do not get caught and displayed by the Unity Editor, an issue further amplified by how unhandled exceptions work differently on Android and iOS
  • The written documentation and Unity for Firebase video guides very sparsely note the multithreaded callbacks
  • Firebase Unity issues are constantly filed, but go stale due to the minimal viable reproduction cases not crashing when non-thread safe proprietary methods aren't called

There's a lot of great features that Firebase for Unity provides, and we continuously look forwards to expanding our usage of the SDK. We hope this feedback can help provide some insight into your users and their points of frustration, or perhaps even help a passer-by realize why their code has gone wrong 😄. While these issues are mostly behind our team for now, we hope they can be resolved upstream to prevent future users from going through the same pain.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions