1

I am building an iOS game. Each game room is constructed from two users. After two users get matched, I'm displaying a "waiting for respond" timer on both devices: they need to click am "I'm Ready" button within 8 second or both of them will get kicked from the room + deleted from Firebase.

The correct state of the users (before each of them clicked on the "I'm Ready Button"):

Parent
      Matches
             User 1
                   opponent : User2
                   state : "NotReady"
             User 2
                   opponent : User1
                   state : "NotReady"

Critical note - The timer on both devices time difference is +-2 seconds, In other words - once device timer is going to end before the other one

When a user press the "I'm Ready" button i'm updating the state : "userReady",and check if the other user is ready too (observing its state value).

If both users are userReady - game on.

PROBLEM

so we have already cleared that in 100% of the cases i have a small time difference between both devices. BUT, if for instance ,

User1 clicked I'm Ready button. So now User2 got an ChildUpdate event, and as far as he knows - User2 is completely ready to play.

User1 timer is ending first(fact), so when his timer will finish, User2 timer will remain 1 seconds. NOW, on User1 time just reached zero, so he get kicked out of the room, and send a removeValue event on each of the Users nodes. While this is happening, at this very small "gap",(between the zero time of User1 timer ending,User2 clock show's 1 sec(fact) - and he press the ready button. than he thinks that User1 is ready to play, and he as well - than the game starts playing.

Final Results -

Both players got deleted from Firebase

User1 is out of the room

User2 starts the game(and he think he is an opponent)

How can i solve this end-case scenario?, I have already tried calling startGame function only when the "UpdateChild state" is done, but it still gets in, maybe because the order for updateChild,and removeValue"?

Any suggestions? And BIG thank you Firebase team for reaching how all the time!!!

Frank van Puffelen
  • 565,676
  • 79
  • 828
  • 807
Roi Mulia
  • 5,626
  • 11
  • 54
  • 105
  • 2
    For others looking for data consistency docs, see [event guarantees here](https://www.firebase.com/docs/web/guide/retrieving-data.html#section-event-guarantees) – Kato Jan 13 '16 at 23:47
  • Hey @Kato. Thank you for your respond.Already read this(a few times) hehe, Still haven't figured a solution.. – Roi Mulia Jan 13 '16 at 23:50
  • That's unrelated to your question, but the title (data consistency) is a common topic, so hopefully helpful for others. – Kato Jan 13 '16 at 23:52
  • @Kato , Got it - thanks. – Roi Mulia Jan 14 '16 at 00:00
  • For a next question: please include code for what you've already tried. It's much easier to respond to (and reason about) what you're asking if you show what you've already done. – Frank van Puffelen Jan 14 '16 at 04:21
  • Hey @FrankvanPuffelen. I thought my code were irrelevant, i will try implement Kato answer, if i'll get in trouble i'll post an update - Thanks! – Roi Mulia Jan 14 '16 at 04:24
  • Since Kato wrote code in the answer that you asked for, it was not irrelevant. – Frank van Puffelen Jan 14 '16 at 04:44

1 Answers1

3

It doesn't make sense that User 1 would accept and then expire anyway. User 2 should be the one expiring if the time limit is reached and he hasn't accepted yet.

To prevent this, you're looking for transactions. When you would "expire" a user, use a transaction to do so, so that there are no data conflicts.

var ref = new Firebase("https://<YOUR-FIREBASE-APP>.firebaseio.com/");

ref.child('Parent/Matches').child(user1).transaction(function(currentValue) {
    if( currentValue && currentValue.state === 'NotReady' ) {
       return null; // delete the user
    }
    else {
       return undefined; // abort the transaction; status changed while we were attempting to remove it
    }
});

Possibly correct in swift:

var ref = Firebase(url: "https://<YOUR-FIREBASE-APP>.firebaseio.com/Parent/Matches/<user id>")

upvotesRef.runTransactionBlock({
    (currentData:FMutableData!) in
    if currentData && currentData.value["state"] != "NotReady" {
        return FTransactionResult.successWithValue(NSNull)
    }
    return FTransactionResult.abort();
});

Furthermore, you could simplify things and reduce the opportunity for chaos by not deleting/expiring the records until both users have reached their accept time limit, and then doing both at once in a transaction.

Kato
  • 40,352
  • 6
  • 119
  • 149
  • One quick comment before i'm deep reading - I have this kind of architecture - Lobby category and Match category. If User1 clicked ready but the times go off . I'm deleting the nodes at the match cat' , and immediately create new node the Lobby and search for opponent (just as clarification). I'll read the answer deeply and tell if further – Roi Mulia Jan 13 '16 at 23:56
  • Okay, First of all thank you! Secondly - two quest on this. Is it possible to translate it to ios(swift better, but objective c works too)? If no, no harsh hehe. Second. " doesn't make sense that User 1 would accept and then expire anyway". But in case the time went to 0(i'm showing the timer so user will trigger the i'm ready button), Its lack of design and user exprience to keep the users at the room(even if time is set to zero). I'm counting on Firebase server to keep the difference *top* 2 seconds, so even 6 sec to press the i'm ready button sounds fair. I just want to avoid the small gaps – Roi Mulia Jan 14 '16 at 00:00
  • iOS transactions are [covered here](https://www.firebase.com/docs/ios/guide/saving-data.html#section-transactions). I'll make an attempt to add this to my answer. – Kato Jan 14 '16 at 00:04
  • Thanks Kato - your help is priceless..! Could you break down the transaction state? ' if( currentValue && currentValue.state === 'NotReady' ) {' , I'm not entirely understand the if statement. I'm using the transaction to update the state of the user? Or to delete it? Sorry for the confusion – Roi Mulia Jan 14 '16 at 00:08
  • Thanks Kato! I will let you know how the implementations goes – Roi Mulia Jan 14 '16 at 04:23