1page.title=Resolving Cloud Save Conflicts 2page.tags=cloud 3 4page.article=true 5@jd:body 6 7<style type="text/css"> 8.new-value { 9 color: #00F; 10} 11.conflict { 12 color: #F00; 13} 14</style> 15 16<div id="tb-wrapper"> 17 <div id="tb"> 18 <h2>In this document</h2> 19 <ol class="nolist"> 20 <li><a href="#conflict">Get Notified of Conflicts</a></li> 21 <li><a href="#simple">Handle the Simple Cases</a></li> 22 <li><a href="#complicated">Design a Strategy for More Complex Cases</a> 23 <ol class="nolist"> 24 <li><a href="#attempt-1">First Attempt: Store Only the Total</a></li> 25 <li><a href="#attempt-2">Second Attempt: Store the Total and the Delta</a></li> 26 <li><a href="#solution">Solution: Store the Sub-totals per Device</a></li> 27 </ol> 28 </li> 29 <li><a href="#cleanup">Clean Up Your Data</a></li> 30 </ol> 31 <h2>You should also read</h2> 32 <ul> 33 <li><a href="http://developers.google.com/games/services/common/concepts/cloudsave">Cloud Save</a></li> 34 <li><a href="https://developers.google.com/games/services/android/cloudsave">Cloud Save in Android</a></li> 35 </ul> 36 </div> 37</div> 38 39<p>This article describes how to design a robust conflict resolution strategy for 40apps that save data to the cloud using the 41<a href="http://developers.google.com/games/services/common/concepts/cloudsave"> 42Cloud Save service</a>. The Cloud Save service 43allows you to store application data for each user of an application on Google's 44servers. Your application can retrieve and update this user data from Android 45devices, iOS devices, or web applications by using the Cloud Save APIs.</p> 46 47<p>Saving and loading progress in Cloud Save is straightforward: it's just a matter 48of serializing the player's data to and from byte arrays and storing those arrays 49in the cloud. However, when your user has multiple devices and two or more of them attempt 50to save data to the cloud, the saves might conflict, and you must decide how to 51resolve it. The structure of your cloud save data largely dictates how robust 52your conflict resolution can be, so you must design your data carefully in order 53to allow your conflict resolution logic to handle each case correctly.</p> 54 55<p>The article starts by describing a few flawed approaches 56and explains where they fall short. Then it presents a solution for avoiding 57conflicts. The discussion focuses on games, but you can 58apply the same principles to any app that saves data to the cloud.</p> 59 60<h2 id="conflict">Get Notified of Conflicts</h2> 61 62<p>The 63<a href="{@docRoot}reference/com/google/android/gms/appstate/OnStateLoadedListener.html">{@code OnStateLoadedListener}</a> 64methods are responsible for loading an application's state data from Google's servers. 65The callback <a href="{@docRoot}reference/com/google/android/gms/appstate/OnStateLoadedListener.html#onStateConflict"> 66{@code OnStateLoadedListener.onStateConflict}</a> provides a mechanism 67for your application to resolve conflicts between the local state on a user's 68device and the state stored in the cloud:</p> 69 70<pre style="clear:right">@Override 71public void onStateConflict(int stateKey, String resolvedVersion, 72 byte[] localData, byte[] serverData) { 73 // resolve conflict, then call mAppStateClient.resolveConflict() 74 ... 75}</pre> 76 77<p>At this point your application must choose which one of the data sets should 78be kept, or it can submit a new data set that represents the merged data. It is 79up to you to implement this conflict resolution logic.</p> 80 81<p>It's important to realize that the Cloud Save service synchronizes 82data in the background. Therefore, you should ensure that your app is prepared 83to receive that callback outside of the context where you originally generated 84the data. Specifically, if the Google Play services application detects a conflict 85in the background, the callback will be called the next time you attempt to load the 86data, which might not happen until the next time the user starts the app.</p> 87 88<p>Therefore, design of your cloud save data and conflict resolution code must be 89<em>context-independent</em>: given two conflicting save states, you must be able 90to resolve the conflict using only the data available within the data sets, without 91consulting any external context. </p> 92 93<h2 id="simple">Handle the Simple Cases</h2> 94 95<p>Here are some simple cases of conflict resolution. For many apps, it is 96sufficient to adopt a variant of one of these strategies:</p> 97 98<ul> 99 <li> <strong>New is better than old</strong>. In some cases, new data should 100always replace old data. For example, if the data represents the player's choice 101for a character's shirt color, then a more recent choice should override an 102older choice. In this case, you would probably choose to store the timestamp in the cloud 103save data. When resolving the conflict, pick the data set with the most recent 104timestamp (remember to use a reliable clock, and be careful about time zone 105differences).</li> 106 107 <li> <strong>One set of data is clearly better than the other</strong>. In other 108cases, it will always be clear which data can be defined as "best". For 109example, if the data represents the player's best time in a racing game, then it's 110clear that, in case of conflicts, you should keep the best (smallest) time.</li> 111 112 <li> <strong>Merge by union</strong>. It may be possible to resolve the conflict 113by computing a union of the two conflicting sets. For example, if your data 114represents the set of levels that player has unlocked, then the resolved data is 115simply the union of the two conflicting sets. This way, players won't lose any 116levels they have unlocked. The 117<a href="https://github.com/playgameservices/android-samples/tree/master/CollectAllTheStars"> 118CollectAllTheStars</a> sample game uses a variant of this strategy.</li> 119</ul> 120 121<h2 id="complicated">Design a Strategy for More Complex Cases</h2> 122 123<p>A more complicated case happens when your game allows the player to collect 124fungible items or units, such as gold coins or experience points. Let's 125consider a hypothetical game, called Coin Run, an infinite runner where the goal 126is to collect coins and become very, very rich. Each coin collected gets added to 127the player's piggy bank.</p> 128 129<p>The following sections describe three strategies for resolving sync conflicts 130between multiple devices: two that sound good but ultimately fail to successfully 131resolve all scenarios, and one final solution that can manage conflicts between 132any number of devices.</p> 133 134<h3 id="attempt-1">First Attempt: Store Only the Total</h3> 135 136<p>At first thought, it might seem that the cloud save data should simply be the 137number of coins in the bank. But if that data is all that's available, conflict 138resolution will be severely limited. The best you could do would be to pick the largest of 139the two numbers in case of a conflict.</p> 140 141<p>Consider the scenario illustrated in Table 1. Suppose the player initially 142has 20 coins, and then collects 10 coins on device A and 15 coins on device B. 143Then device B saves the state to the cloud. When device A attempts to save, a 144conflict is detected. The "store only the total" conflict resolution algorithm would resolve 145the conflict by writing 35 (the largest of the two numbers).</p> 146 147<p class="table-caption"><strong>Table 1.</strong> Storing only the total number 148of coins (failed strategy).</p> 149 150<table border="1"> 151 <tr> 152 <th>Event</th> 153 <th>Data on Device A</th> 154 <th>Data on Device B</th> 155 <th>Data on Cloud</th> 156 <th>Actual Total</th> 157 </tr> 158 <tr> 159 <td>Starting conditions</td> 160 <td>20</td> 161 <td>20</td> 162 <td>20</td> 163 <td>20</td> 164 </tr> 165 <tr> 166 <td>Player collects 10 coins on device A</td> 167 <td class="new-value">30</td> 168 <td>20</td> 169 <td>20</td> 170 <td>30</td> 171 </tr> 172 <tr> 173 <td>Player collects 15 coins on device B</td> 174 <td>30</td> 175 <td class="new-value">35</td> 176 <td>20</td> 177 <td>45</td> 178 </tr> 179 <tr> 180 <td>Device B saves state to cloud</td> 181 <td>30</td> 182 <td>35</td> 183 <td class="new-value">35</td> 184 <td>45</td> 185 </tr> 186 <tr> 187 <td>Device A tries to save state to cloud.<br /> 188 <span class="conflict">Conflict detected.</span></td> 189 <td class="conflict">30</td> 190 <td>35</td> 191 <td class="conflict">35</td> 192 <td>45</td> 193 </tr> 194 <tr> 195 <td>Device A resolves conflict by picking largest of the two numbers.</td> 196 <td class="new-value">35</td> 197 <td>35</td> 198 <td class="new-value">35</td> 199 <td>45</td> 200 </tr> 201</table> 202 203<p>This strategy would fail—the player's bank has gone from 20 204to 35, when the user actually collected a total of 25 coins (10 on device A and 15 on 205device B). So 10 coins were lost. Storing only the total number of coins in the 206cloud save is not enough to implement a robust conflict resolution algorithm.</p> 207 208<h3 id="attempt-2">Second Attempt: Store the Total and the Delta</h3> 209 210<p>A different approach is to include an additional field in 211the save data: the number of coins added (the delta) since the last commit. In 212this approach the save data can be represented by a tuple <em>(T,d)</em> where <em>T</em> is 213the total number of coins and <em>d</em> is the number of coins that you just 214added.</p> 215 216<p>With this structure, your conflict resolution algorithm has room to be more 217robust, as illustrated below. But this approach still doesn't give your app 218a reliable picture of the player's overall state.</p> 219 220<p>Here is the conflict resolution algorithm for including the delta:</p> 221 222<ul> 223 <li><strong>Local data:</strong> (T, d)</li> 224 <li><strong>Cloud data:</strong> (T', d')</li> 225 <li><strong>Resolved data:</strong> (T' + d, d)</li> 226</ul> 227 228<p>For example, when you get a conflict between the local state <em>(T,d)</em> 229and the cloud state <em>(T',d')</em>, you can resolve it as <em>(T'+d, d)</em>. 230What this means is that you are taking the delta from your local data and 231incorporating it into the cloud data, hoping that this will correctly account for 232any gold coins that were collected on the other device.</p> 233 234<p>This approach might sound promising, but it breaks down in a dynamic mobile 235environment:</p> 236<ul> 237<li>Users might save state when the device is offline. These changes will be 238queued up for submission when the device comes back online.</li> 239 240<li>The way that sync works is that 241the most recent change overwrites any previous changes. In other words, the 242second write is the only one that gets sent to the cloud (this happens 243when the device eventually comes online), and the delta in the first 244write is ignored.</li> 245</ul> 246 247<p>To illustrate, consider the scenario illustrated by Table 2. After the 248series of operations shown in the table, the cloud state 249will be (130, +5). This means the resolved state would be (140, +10). This is 250incorrect because in total, the user has collected 110 coins on device A and 251120 coins on device B. The total should be 250 coins.</p> 252 253<p class="table-caption"><strong>Table 2.</strong> Failure case for total+delta 254strategy.</p> 255 256<table border="1"> 257 <tr> 258 <th>Event</th> 259 <th>Data on Device A</th> 260 <th>Data on Device B</th> 261 <th>Data on Cloud</th> 262 <th>Actual Total</th> 263 </tr> 264 <tr> 265 <td>Starting conditions</td> 266 <td>(20, x)</td> 267 <td>(20, x)</td> 268 <td>(20, x)</td> 269 <td>20</td> 270 </tr> 271 <tr> 272 <td>Player collects 100 coins on device A</td> 273 <td class="test2">(120, +100)</td> 274 <td>(20, x)</td> 275 <td>(20, x)</td> 276 <td>120</td> 277 </tr> 278 <tr> 279 <td>Player collects 10 more coins on device A</td> 280 <td class="new-value" style="white-space:nowrap">(130, +10)</td> 281 <td>(20, x)</td> 282 <td>(20, x)</td> 283 <td>130</td> 284 </tr> 285 <tr> 286 <td>Player collects 115 coins on device B</td> 287 <td>(130, +10)</td> 288 <td class="new-value" style="white-space:nowrap">(125, +115)</td> 289 <td>(20, x)</td> 290 <td>245</td> 291 </tr> 292 <tr> 293 <td>Player collects 5 more coins on device B</td> 294 <td>(130, +10)</td> 295 <td class="new-value"> 296(130, +5)</td> 297 <td> 298(20, x)</td> 299 <td>250</td> 300 </tr> 301 <tr> 302 <td>Device B uploads its data to the cloud 303 </td> 304 <td>(130, +10)</td> 305 <td>(130, +5)</td> 306 <td class="new-value"> 307(130, +5)</td> 308 <td>250</td> 309 </tr> 310 <tr> 311 <td>Device A tries to upload its data to the cloud. 312 <br /> 313 <span class="conflict">Conflict detected.</span></td> 314 <td class="conflict">(130, +10)</td> 315 <td>(130, +5)</td> 316 <td class="conflict">(130, +5)</td> 317 <td>250</td> 318 </tr> 319 <tr> 320 <td>Device A resolves the conflict by applying the local delta to the cloud total. 321 </td> 322 <td class="new-value" style="white-space:nowrap">(140, +10)</td> 323 <td>(130, +5)</td> 324 <td class="new-value" style="white-space:nowrap">(140, +10)</td> 325 <td>250</td> 326 </tr> 327</table> 328<p><em>(*): x represents data that is irrelevant to our scenario.</em></p> 329 330<p>You might try to fix the problem by not resetting the delta after each save, 331so that the second save on each device accounts for all the coins collected thus far. 332With that change the second save made by device A would be<em> (130, +110)</em> instead of 333<em>(130, +10)</em>. However, you would then run into the problem illustrated in Table 3.</p> 334 335<p class="table-caption"><strong>Table 3.</strong> Failure case for the modified 336algorithm.</p> 337<table border="1"> 338 <tr> 339 <th>Event</th> 340 <th>Data on Device A</th> 341 <th>Data on Device B</th> 342 <th>Data on Cloud</th> 343 <th>Actual Total</th> 344 </tr> 345 <tr> 346 <td>Starting conditions</td> 347 <td>(20, x)</td> 348 <td>(20, x)</td> 349 <td>(20, x)</td> 350 <td>20</td> 351 </tr> 352 <tr> 353 <td>Player collects 100 coins on device A 354 </td> 355 <td class="new-value">(120, +100)</td> 356 <td>(20, x)</td> 357 <td>(20, x)</td> 358 <td>120</td> 359 </tr> 360 <tr> 361 <td>Device A saves state to cloud</td> 362 <td>(120, +100)</td> 363 <td>(20, x)</td> 364 <td class="new-value">(120, +100)</td> 365 <td>120</td> 366 </tr> 367 <tr> 368 <td>Player collects 10 more coins on device A 369 </td> 370 <td class="new-value">(130, +110)</td> 371 <td> 372(20, x)</td> 373 <td>(120, +100)</td> 374 <td>130</td> 375 </tr> 376 <tr> 377 <td>Player collects 1 coin on device B 378 379 </td> 380 <td>(130, +110)</td> 381 <td class="new-value">(21, +1)</td> 382 <td>(120, +100)</td> 383 <td>131</td> 384 </tr> 385 <tr> 386 <td>Device B attempts to save state to cloud. 387 <br /> 388 Conflict detected. 389 </td> 390 <td>(130, +110)</td> 391 <td class="conflict">(21, +1)</td> 392 <td class="conflict"> 393(120, +100)</td> 394 <td>131</td> 395 </tr> 396 <tr> 397 <td>Device B solves conflict by applying local delta to cloud total. 398 399 </td> 400 <td>(130, +110)</td> 401 <td>(121, +1)</td> 402 <td>(121, +1)</td> 403 <td>131</td> 404 </tr> 405 <tr> 406 <td>Device A tries to upload its data to the cloud. 407 <br /> 408 <span class="conflict">Conflict detected. </span></td> 409 <td class="conflict">(130, +110)</td> 410 <td>(121, +1)</td> 411 <td class="conflict">(121, +1)</td> 412 <td>131</td> 413 </tr> 414 <tr> 415 <td>Device A resolves the conflict by applying the local delta to the cloud total. 416 417 </td> 418 <td class="new-value" style="white-space:nowrap">(231, +110)</td> 419 <td>(121, +1)</td> 420 <td class="new-value" style="white-space:nowrap">(231, +110)</td> 421 <td>131</td> 422 </tr> 423</table> 424<p><em>(*): x represents data that is irrelevant to our scenario.</em></p> 425 426<p>Now you have the opposite problem: you are giving the player too many coins. 427The player has gained 211 coins, when in fact she has collected only 111 coins.</p> 428 429<h3 id="solution">Solution: Store the Sub-totals per Device</h3> 430 431<p>Analyzing the previous attempts, it seems that what those strategies 432fundamentally miss is the ability to know which coins have already been counted 433and which coins have not been counted yet, especially in the presence of multiple 434consecutive commits coming from different devices.</p> 435 436<p>The solution to the problem is to change the structure of your cloud save to 437be a dictionary that maps strings to integers. Each key-value pair in this 438dictionary represents a "drawer" that contains coins, and the total 439number of coins in the save is the sum of the values of all entries. 440The fundamental principle of this design is that each device has its own 441drawer, and only the device itself can put coins into that drawer.</p> 442 443<p>The structure of the dictionary is <em>(A:a, B:b, C:c, ...)</em>, where 444<em>a</em> is the total number of coins in the drawer A, <em>b</em> is 445the total number of coins in drawer B, and so on.</p> 446 447<p>The new conflict resolution algorithm for the "drawer" solution is as follows:</p> 448 449 <ul> 450 <li><strong>Local data:</strong> (A:a, B:b, C:c, ...)</li> 451 <li><strong>Cloud data:</strong> (A:a', B:b', C:c', ...)</li> 452 <li><strong>Resolved data:</strong> (A:<em>max</em>(a,a'), B:<em>max</em>(b,b'), 453 C:<em>max</em>(c,c'), ...)</li> 454 </ul> 455 456<p>For example, if the local data is <em>(A:20, B:4, C:7)</em> and the cloud data 457is <em>(B:10, C:2, D:14)</em>, then the resolved data will be 458<em>(A:20, B:10, C:7, D:14)</em>. Note that how you apply conflict resolution 459logic to this dictionary data may vary depending on your app. For example, for 460some apps you might want to take the lower value.</p> 461 462<p>To test this new algorithm, apply it to any of the test scenarios 463mentioned above. You will see that it arrives at the correct result.</p> 464 465Table 4 illustrates this, based on the scenario from Table 3. Note the following:</p> 466 467<ul> 468 <li>In the initial state, the player has 20 coins. This is accurately reflected 469 on each device and the cloud. This value is represented as a dictionary (X:20), 470 where the value of X isn't significant—we don't care where this initial data came from.</li> 471 <li>When the player collects 100 coins on device A, this change 472 is packaged as a dictionary and saved to the cloud. The dictionary's value is 100 because 473 that is the number of coins that the player collected on device A. There is no 474 calculation being performed on the data at this point—device A is simply 475 reporting the number of coins the player collected on it.</li> 476 <li>Each new 477 submission of coins is packaged as a dictionary associated with the device 478 that saved it to the cloud. When the player collects 10 more coins on device A, 479 for example, the device A dictionary value is updated to be 110.</li> 480 481 <li>The net result is that the app knows the total number of coins 482 the player has collected on each device. Thus it can easily calculate the total.</li> 483</ul> 484 485<p class="table-caption"><strong>Table 4.</strong> Successful application of the 486key-value pair strategy.</p> 487 488<table border="1"> 489 <tr> 490 <th>Event</th> 491 <th>Data on Device A</th> 492 <th>Data on Device B</th> 493 <th>Data on Cloud</th> 494 <th>Actual Total</th> 495 </tr> 496 <tr> 497 <td>Starting conditions</td> 498 <td>(X:20, x)</td> 499 <td>(X:20, x)</td> 500 <td>(X:20, x)</td> 501 <td>20</td> 502 </tr> 503 <tr> 504 <td>Player collects 100 coins on device A 505 506 </td> 507 <td class="new-value">(X:20, A:100)</td> 508 <td>(X:20)</td> 509 <td>(X:20)</td> 510 <td>120</td> 511 </tr> 512 <tr> 513 <td>Device A saves state to cloud 514 515 </td> 516 <td>(X:20, A:100)</td> 517 <td>(X:20)</td> 518 <td class="new-value">(X:20, A:100)</td> 519 <td>120</td> 520 </tr> 521 <tr> 522 <td>Player collects 10 more coins on device A 523 </td> 524 <td class="new-value">(X:20, A:110)</td> 525 <td>(X:20)</td> 526 <td>(X:20, A:100)</td> 527 <td>130</td> 528 </tr> 529 <tr> 530 <td>Player collects 1 coin on device B</td> 531 <td>(X:20, A:110)</td> 532 <td class="new-value"> 533(X:20, B:1)</td> 534 <td> 535(X:20, A:100)</td> 536 <td>131</td> 537 </tr> 538 <tr> 539 <td>Device B attempts to save state to cloud. 540 <br /> 541 <span class="conflict">Conflict detected. </span></td> 542 <td>(X:20, A:110)</td> 543 <td class="conflict">(X:20, B:1)</td> 544 <td class="conflict"> 545(X:20, A:100)</td> 546 <td>131</td> 547 </tr> 548 <tr> 549 <td>Device B solves conflict 550 551 </td> 552 <td>(X:20, A:110)</td> 553 <td class="new-value">(X:20, A:100, B:1)</td> 554 <td class="new-value">(X:20, A:100, B:1)</td> 555 <td>131</td> 556 </tr> 557 <tr> 558 <td>Device A tries to upload its data to the cloud. <br /> 559 <span class="conflict">Conflict detected.</span></td> 560 <td class="conflict">(X:20, A:110)</td> 561 <td>(X:20, A:100, B:1)</td> 562 <td class="conflict"> 563(X:20, A:100, B:1)</td> 564 <td>131</td> 565 </tr> 566 <tr> 567 <td>Device A resolves the conflict 568 569 </td> 570 <td class="new-value" style="white-space:nowrap">(X:20, A:110, B:1)</td> 571 <td style="white-space:nowrap">(X:20, A:100, B:1)</td> 572 <td class="new-value" style="white-space:nowrap">(X:20, A:110, B:1) 573 <br /> 574 <em>total 131</em></td> 575 <td>131</td> 576 </tr> 577</table> 578 579 580<h2 id="cleanup">Clean Up Your Data</h2> 581<p>There is a limit to the size of cloud save data, so in following the strategy 582outlined in this article, take care not to create arbitrarily large dictionaries. At first 583glance it may seem that the dictionary will have only one entry per device, and even 584the very enthusiastic user is unlikely to have thousands of them. However, 585obtaining a device ID is difficult and considered a bad practice, so instead you should 586use an installation ID, which is easier to obtain and more reliable. This means 587that the dictionary might have one entry for each time the user installed the 588application on each device. Assuming each key-value pair takes 32 bytes, and 589since an individual cloud save buffer can be 590up to 128K in size, you are safe if you have up to 4,096 entries.</p> 591 592<p>In real-life situations, your data will probably be more complex than a number 593of coins. In this case, the number of entries in this dictionary may be much more 594limited. Depending on your implementation, it might make sense to store the 595timestamp for when each entry in the dictionary was modified. When you detect that a 596given entry has not been modified in the last several weeks or months, it is 597probably safe to transfer the coins into another entry and delete the old entry.</p> 598