Firebase Realtime Database Returning Stale Results
May 18, 2021
Continuing my series of posts about issues I hit as a first time mobile game developer today I’m writing about an issue I found surprisingly little about on the internet. Maybe it’ll mostly be a moot point in the near future since Google is pushing Cloud Firestore over Realtime Database but I spent long enough banging my head against the wall that I hope this helps someone else.
The problem
I’m developing a game in Unity and decided to use Google Firebase to handle things like authentication, storing data, etc… When I went to go build out my database to store users and game state I found there are two options:
I can’t really remember why I picked the Realtime Database (maybe it was the preferred option when I started?) but I did.
I created the database, populated it with information and then wrote some code that looked like:
class DatabaseManager {public async Task<DbModels.v1.User> GetActiveUsers(FirebaseUser fbUser){...return await FirebaseDatabase.DefaultInstance.GetReference($"users/{fbUser.UserId}/activeGames").GetValueAsync().ContinueWith(task => {if (task.IsCompleted){DataSnapshot snapshot = task.Result;var json = snapshot.GetRawJsonValue();return (List<Game>)JsonUtility.FromJson(json, typeof(List<Game>));}...});}}
The issue
The problem was sometimes I would get back an empty list even though I could log in to the Firebase console and SEE there was data there. I did a lot of Google searching to try to find others with the same issue to no avail until FINALLY I found a post somewhere mentioning that the Realtime Database API might initially return a cached response when querying a value. The API of course contacts the server afterwards to get updated values but they’re never sent along if you’re just using GetValueAsync()
. What made it tough to diagnose was this wasn’t consistent behavior.
Getting a cached response was of course what was happening when I was seeing data that didn’t correspond with what I knew was on the server. I assumed GetValueAsync()
would always query the server for the latest value but the Realtime Database API really expects you go keep a listener on a value and respond to updates.
One solution
Obviously the quickest solution was to stop assuming I’d be receiving the lastest value when I query the database and instead setup a listener that isn’t immediately torn down.
Since I was going to have to set up a listener to get all data updates anyway I decided to set it up on the user (’users/<id>
’) rather than ’users/<id>/activeGames
’ so I could get other updates as well.
But when should we start listening to a user’s data? It made sense for me to do so right after the user login. So first I added a user login event to my AuthManager
class and then subscribed to the event while constructing my DatabaseManager:
class DatabaseManager{...public DatabaseManager(AuthManager authManager){authManager.UserLoggedIn += UserLoggedInHandler;}private void UserLoggedInHandler(object sender, UserLoggedInEventArgs e){FirebaseDatabase.DefaultInstance.GetReference($"users/{e.LoggedInUser.UserId}").ValueChanged += UserValueChangedHandler;}}
And then expose an ActiveGamesUpdated
event:
public class ActiveGamesEventArgs{public ActiveGamesEventArgs(List<Game> games) { Games = games; }public List<Game> Games { get; }}public class DatabaseManager{public System.Action<object, ActiveGamesEventArgs> ActiveGamesUpdated { get; internal set; }public void AttachActiveGameEventListener(Action<object, ActiveGamesEventArgs> handler) {ActiveGamesUpdated += handler;// send existing stateif (_latestActiveGameEventArgs != null){handler(this, _latestActiveGameEventArgs);}}private void UserValueChangedHandler(object sender, ValueChangedEventArgs args){if (args.DatabaseError != null){Debug.LogError(args.DatabaseError.Message);return;}string json = args.Snapshot.GetRawJsonValue();User user = (User)JsonUtility.FromJson(json, typeof(User));_latestActiveGameEventArgs = new ActiveGamesEventArgs(user.activeGames);this.ActiveGamesUpdated.Invoke(this, _latestActiveGameEventArgs);}}
The solution ended up being quite a bit more boilerplate than just calling GetValueAsync()
but at least I get the correct value.
I’m currently looking at whether Firebase Cloud acts in the same way or not hoping that I can move away from the an event driven architecture the Realtime Database forced on me because it just doesn’t fit what I need for my app.