Data Consistency on very close events in Firebase - ios

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!!!

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.

Related

Why does .share() have no effect on cold sources (autoconnect vs. refCount)?

Flux<Integer> shared = Flux.just(1, 2).share();
shared.subscribe(System.out::println);
shared.subscribe(System.out::println);
Since share() turns the flux into a hot one, I expect the first subscriber to get all values and the second one to get none, since the stream has completed at the time of subscription. But the output is the same as without share: 1 2 1 2, but it should be just 1 2.
When I replace share() with publish.autoconnect() it works as expected. Why is that?
The answer is simple, but it took me a while to figure it out.
share() is a shortcut for publish().refCount(). refCount() is like autoConnect() except for one additional feature: It disconnects when all subscribers have cancelled or - and that's the situation here - the stream has completed.
The first shared.subscribe creates a subscription (via share) to the original flux. All values are emitted immediately, the stream completes, the subscription is cancelled.
Since there is no subscription now, the second shared.subscribe again creates a subscription and the stream starts again, from the beginning.
autoConnect, however, does not cancel the subscription. If you use it instead of refCount the subscription to the original flux remains, but because the stream has completed, any additional subscriber won't receive any values.

GameKit, GKGameSession how to add players to the session and send data?

I created a session, shared it to another player and now I want to start a game.
I can see a session with two players on both devices. So, it looks like we're ready to game but I need to change connected status before the game and I can do this on both devices.
But... when I do this on device A, I see that user A is connected and user B isn't. And after that, when I repeat the process on device B, I see vice versa situation. B is connected and A is not.
Here is my code that connect player and send the data:
session.setConnectionState(.connected) { (error) in
if let err = error {
assertionFailure(err.localizedDescription)
}
else {
print("NC:",session.players(with: .notConnected))
print(" C:",session.players(with: .connected))
let m = MoveTransfer(move:1) // test struct to send/receive data
session.send(m.data(), with: .reliable) { (error) in
if let err = error {
assertionFailure(err.localizedDescription)
}
}
}
}
I'm getting error:
The requested operation could not be completed because there are no recipients connected to the session
By the way, I'm unable to change connected state on the simulator (iCloud is logged in).
I forgot to mention, that I'm working on a turn based game.
Edit
Tried again and now after several iterations I got this:
I have both players connected to session. But send data still doesn't work.
here is console output:
NC: [] // not connected array and connected array below
C: [<GKCloudPlayer: 0x17402e700>, id: playerID1, name: Player1,<GKCloudPlayer: 0x17402e900>, id: playerID2, name: Player2]
fatal error: The requested operation could not be completed because there are no recipients connected to the session.
Got this on two real devices.
I was able to send data and receive it on the other device. I used loadSessions function to load all session (I think that loadSession by id would do the trick too).
I did the following, D1 is a device 1 and D2 device 2:
1. D1: load all sessions and set player state to connected
2. D2: load all sessions and set player state to connected
3. D1: load all sessions again and set player state to connected
4. D1 || D2: send data
5. D2 || D1: data 256 bytes from player: <GKCloudPlayer: 0x1c022b380>, id: playerID, name: (null)
Although, I wasn't able to transfer data back and forth on both devices since if we'll add step 6. that sends data from device that just received it, we'll get an error: The requested operation could not be completed because there are no recipients connected to the session.
I do not sure what is the reason of it but I stop my efforts on this stage, since I think now, that I don't need to be connected to session to play turn based game.
I found this "new" iOS 10 APIs abandoned. By Apple and by devs consequently. If you one of those who still trying to use it drop me a note at my twitter (link in my bio) if you want to discuss GKGameSession and related topics.

FEventType.ChildAdded event only fired once

my firebase data structure looks like the following
user
|__{user_id}
|__userMatch
|__{userMatchId}
|__createdAt: <UNIX time in milliseconds>
I'm trying to listen for the child added event under userMatch since a particular given time. Here's my swift code:
func listenForNewUserMatches(since: NSDate) -> UInt? {
NSLog("listenForNewUserMatches since: \(since)")
var handle:UInt?
let userMatchRef = usersRef.childByAppendingPath("\(user.objectId!)/userMatch")
var query = userMatchRef.queryOrderedByChild("createdAt");
query = query.queryStartingAtValue(since.timeIntervalSince1970 * 1000)
handle = query.observeEventType(FEventType.ChildAdded, withBlock: { snapshot in
let userMatchId = snapshot.key
NSLog("New firebase UserMatch created \(userMatchId)")
}, withCancelBlock: { error in
NSLog("Error listening for new userMatches: \(error)")
})
return handle
}
What's happening is that the event call back is called only once. Subsequent data insertion under userMatch didn't trigger the call. Sort of behaves like observeSingleEventOfType
I have the following data inserted into firebase under user/{some-id}/userMatch:
QGgmQnDLUB
createdAt: 1448934387867
bMfJH1bzNs  
createdAt: 1448934354943
Here are the logs:
2015-11-30 17:32:38.632 listenForNewUserMatches since:2015-12-01 01:32:37 +0000
2015-11-30 17:45:55.163 New firebase UserMatch created bMfJH1bzNs
The call back was fired for bMfJH1bzNs but not for QGgmQnDLUB which was added at a later time. It's very consistent: after opening the app, it only fires for the first event. Not sure what I'm doing wrong here.
Update: Actually the behavior is not very consistent. Sometimes the call back is not fired at all, not even once. But since I persist the since time I should use when calling listenForNewUserMatches function. If I kill the app and restart the app, the callback will get fired (listenForNewUserMatches is called upon app start), for the childAdded event before I killed the app. This happens very consistently (callback always called upon kill-restart the app for events that happened prior to killing the app).
Update 2: Don't know why, but if I add queryLimitedToLast to the query, it works all the time now. I mean, by changing userMatchRef.queryOrderedByChild("createdAt") to userMatchRef.queryOrderedByChild("createdAt").queryLimitedToLast(10), it's working now. 10 is just an arbitrary number I chose.
I think the issue comes from the nature of time based data.
You created a query that says: "Get me all the matches that happened after now." This should work when the app is running and new data comes in like bMfJH1bzNs. But older data like QGgmQnDLUB won't show up.
Then when you run again, the since.timeIntervalSince1970 has changed to a later date. Now neither of the objects before will show up in your query.
When you changed your query to use queryLimitedToLast you avoided this issue because you're no longer querying based on time. Now your query says: "Get me the last ten children at this location."
As long as there is data at that location you'll always receive data in the callback.
So you either need to ensure that since.timeIntervalSince1970 is always earlier than the data you expect to come back, or use queryLimitedToLast.

sproutcore timer usage MaxInactiveInterval

Two absolute beginner questions.
I have working code in my main.js enterState.
enterState: function(context) {
..
//keepAlive
var now = SC.DateTime.create();
if (now.get('hour') < 18){
SC.info ("main_state:enterState:go %#", now.get('hour'));
this.timer = SC.Timer.schedule({
target: this,
action: '_timerFired',
interval: 5000,
repeats: YES
});
} else {
SC.info ("MainState:enterState:nogo %#", now.get('hour'));
};
..
this.mainPane.append();
},
_timerFired: function(){
SC.info ("_timerFired %#", Date.now());
},
exitState: function() {
SC.info('main_state:exitState');
this.timer.invalidate();
this.mainPane.remove();
},
Question 1: the enterState is used every time a user goes to the main view, is the timer schedule initialized once or every time a user switches views?
Question 2: I think I need a query. e.q. the logged in username, to prevent an automatic logout due to the expired session MaxInactiveInterval. Is there sample code to get the spring username in the _timerFired function?
I saw the answer/solution of Maurits, thanks, but it is too complicated for me.
This timer will be scheduled every time this state is being entered. If this is the root state of the application, that will be once. If this state is used to display the mainPane and you are leaving this state somehow, and returning, then this timer will be initialized every time you enter this state.
You could keep the username as property of the current state (this._username) and as _timerFired is called with this being the current state (SC.Timer will take care of that through the target) you will have access to it. You'd need to set it somehow of course. Another solution is to read it directly from the controller you use for the login procedure.
Nevertheless, this solution is prone to trouble. The main reason for this is that you are creating implicit states. I mean that being authenticated is an application state, and instead of making this explicit through the state chart, you hide it within one of the states. As I wrote in the comment: I came to the solution I posted in the other question because of trying many different solutions and hitting trouble. What I learned from those issues is that the statechart is your friend, and really trying to work with it will help you avoid loads of headaches!

Save 2 different PFObjects eventually or fail both

Using Parse SDK for iOS, I have 2 tables :
Game
- UserA : Pointer <_User>
- UserB : Pointer <_User>
- Round : Number
- IsTurnOfUserA : Bool
RoundScore
- GameId : Pointer <Game>
- User : Pointer <_User>
- Score : Number
- etc
A game is done in 3 rounds between 2 users.
When a user ends a round, it toggles Game.IsTurnOfUserA and saves the score for the round to RoundScore table.
In iOS, I didn't find a way to update Game table AND save a RoundScore eventually (maybe later if there is no network).
Both must be done or none at all, but I don't want to end up with only one of the 2 query to be successful and the other one failed.
With Cloud Code, it should be easy to do so but there is no call eventually function.
Update: Maybe there is something to try with Parse's local database ? But I don't know that tool yet.
Important: RoundScore has a field that depends on Game. If Game Object is new, it doesn't have an ObjectId yet, but I still need to link it to the RoundScore Object.
Unfortunately it isn't possible with saveEventually.
What you would need to do is implement your own network checking code and call a cloud method that will save both. That would be the best option.
A hack you could try as an alternative is to save the combined data to another class and have a background job on the server turn that single temporary row into a row in each table, then remove the temporary row.
The drawbacks of this hack is that the background job can run every 15 minutes only, so there might be up-to 15 minutes delay. It also adds extra complexity and overhead to your app.
As Timothy Walters suggested, here is the hack without any background job:
I created a fake table that has the columns of both tables Game and RoundScore
GameAndRoundScore
- Game_UserA
- Game_UserB
- RoundScore_GameId
- RoundScore_Score
- etc
In Cloud Code, I added this function before save
Parse.Cloud.beforeSave("GameAndScoreRound", function(request, response) {
var Game = Parse.Object.extend("Game");
var game = new Game();
game.set("UserA", request.object.get("game_UserA"));
game.set("UserB", request.object.get("game_UserB"));
game.save.then(function(newGame) {
var RoundScore = Parse.Object.extend("RoundScore");
var roundScore = new RoundScore();
//roundScore.set(...)
return scoreRound.save();
}).then(function(newRoundScore) {
response.success();
});
});
Then for the fake table data, I can either leave it as it is or set a background job that empties it or even empty the table manually on the Parse Backend.

Resources