1 page.title=Resolving Cloud Save Conflicts 2 page.tags=cloud 3 4 page.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 40 apps that save data to the cloud using the 41 <a href="http://developers.google.com/games/services/common/concepts/cloudsave"> 42 Cloud Save service</a>. The Cloud Save service 43 allows you to store application data for each user of an application on Google's 44 servers. Your application can retrieve and update this user data from Android 45 devices, 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 48 of serializing the player's data to and from byte arrays and storing those arrays 49 in the cloud. However, when your user has multiple devices and two or more of them attempt 50 to save data to the cloud, the saves might conflict, and you must decide how to 51 resolve it. The structure of your cloud save data largely dictates how robust 52 your conflict resolution can be, so you must design your data carefully in order 53 to allow your conflict resolution logic to handle each case correctly.</p> 54 55 <p>The article starts by describing a few flawed approaches 56 and explains where they fall short. Then it presents a solution for avoiding 57 conflicts. The discussion focuses on games, but you can 58 apply 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> 64 methods are responsible for loading an application's state data from Google's servers. 65 The callback <a href="{@docRoot}reference/com/google/android/gms/appstate/OnStateLoadedListener.html#onStateConflict"> 66 {@code OnStateLoadedListener.onStateConflict}</a> provides a mechanism 67 for your application to resolve conflicts between the local state on a user's 68 device and the state stored in the cloud:</p> 69 70 <pre style="clear:right">@Override 71 public 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 78 be kept, or it can submit a new data set that represents the merged data. It is 79 up to you to implement this conflict resolution logic.</p> 80 81 <p>It's important to realize that the Cloud Save service synchronizes 82 data in the background. Therefore, you should ensure that your app is prepared 83 to receive that callback outside of the context where you originally generated 84 the data. Specifically, if the Google Play services application detects a conflict 85 in the background, the callback will be called the next time you attempt to load the 86 data, 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 90 to resolve the conflict using only the data available within the data sets, without 91 consulting 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 96 sufficient 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 100 always replace old data. For example, if the data represents the player's choice 101 for a character's shirt color, then a more recent choice should override an 102 older choice. In this case, you would probably choose to store the timestamp in the cloud 103 save data. When resolving the conflict, pick the data set with the most recent 104 timestamp (remember to use a reliable clock, and be careful about time zone 105 differences).</li> 106 107 <li> <strong>One set of data is clearly better than the other</strong>. In other 108 cases, it will always be clear which data can be defined as "best". For 109 example, if the data represents the player's best time in a racing game, then it's 110 clear 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 113 by computing a union of the two conflicting sets. For example, if your data 114 represents the set of levels that player has unlocked, then the resolved data is 115 simply the union of the two conflicting sets. This way, players won't lose any 116 levels they have unlocked. The 117 <a href="https://github.com/playgameservices/android-samples/tree/master/CollectAllTheStars"> 118 CollectAllTheStars</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 124 fungible items or units, such as gold coins or experience points. Let's 125 consider a hypothetical game, called Coin Run, an infinite runner where the goal 126 is to collect coins and become very, very rich. Each coin collected gets added to 127 the player's piggy bank.</p> 128 129 <p>The following sections describe three strategies for resolving sync conflicts 130 between multiple devices: two that sound good but ultimately fail to successfully 131 resolve all scenarios, and one final solution that can manage conflicts between 132 any 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 137 number of coins in the bank. But if that data is all that's available, conflict 138 resolution will be severely limited. The best you could do would be to pick the largest of 139 the two numbers in case of a conflict.</p> 140 141 <p>Consider the scenario illustrated in Table 1. Suppose the player initially 142 has 20 coins, and then collects 10 coins on device A and 15 coins on device B. 143 Then device B saves the state to the cloud. When device A attempts to save, a 144 conflict is detected. The "store only the total" conflict resolution algorithm would resolve 145 the 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 148 of 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 204 to 35, when the user actually collected a total of 25 coins (10 on device A and 15 on 205 device B). So 10 coins were lost. Storing only the total number of coins in the 206 cloud 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 211 the save data: the number of coins added (the delta) since the last commit. In 212 this approach the save data can be represented by a tuple <em>(T,d)</em> where <em>T</em> is 213 the total number of coins and <em>d</em> is the number of coins that you just 214 added.</p> 215 216 <p>With this structure, your conflict resolution algorithm has room to be more 217 robust, as illustrated below. But this approach still doesn't give your app 218 a 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> 229 and the cloud state <em>(T',d')</em>, you can resolve it as <em>(T'+d, d)</em>. 230 What this means is that you are taking the delta from your local data and 231 incorporating it into the cloud data, hoping that this will correctly account for 232 any 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 235 environment:</p> 236 <ul> 237 <li>Users might save state when the device is offline. These changes will be 238 queued up for submission when the device comes back online.</li> 239 240 <li>The way that sync works is that 241 the most recent change overwrites any previous changes. In other words, the 242 second write is the only one that gets sent to the cloud (this happens 243 when the device eventually comes online), and the delta in the first 244 write is ignored.</li> 245 </ul> 246 247 <p>To illustrate, consider the scenario illustrated by Table 2. After the 248 series of operations shown in the table, the cloud state 249 will be (130, +5). This means the resolved state would be (140, +10). This is 250 incorrect because in total, the user has collected 110 coins on device A and 251 120 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 254 strategy.</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, 331 so that the second save on each device accounts for all the coins collected thus far. 332 With 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 336 algorithm.</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. 427 The 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 432 fundamentally miss is the ability to know which coins have already been counted 433 and which coins have not been counted yet, especially in the presence of multiple 434 consecutive commits coming from different devices.</p> 435 436 <p>The solution to the problem is to change the structure of your cloud save to 437 be a dictionary that maps strings to integers. Each key-value pair in this 438 dictionary represents a "drawer" that contains coins, and the total 439 number of coins in the save is the sum of the values of all entries. 440 The fundamental principle of this design is that each device has its own 441 drawer, 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 445 the 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 457 is <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 459 logic to this dictionary data may vary depending on your app. For example, for 460 some 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 463 mentioned above. You will see that it arrives at the correct result.</p> 464 465 Table 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 486 key-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 582 outlined in this article, take care not to create arbitrarily large dictionaries. At first 583 glance it may seem that the dictionary will have only one entry per device, and even 584 the very enthusiastic user is unlikely to have thousands of them. However, 585 obtaining a device ID is difficult and considered a bad practice, so instead you should 586 use an installation ID, which is easier to obtain and more reliable. This means 587 that the dictionary might have one entry for each time the user installed the 588 application on each device. Assuming each key-value pair takes 32 bytes, and 589 since an individual cloud save buffer can be 590 up 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 593 of coins. In this case, the number of entries in this dictionary may be much more 594 limited. Depending on your implementation, it might make sense to store the 595 timestamp for when each entry in the dictionary was modified. When you detect that a 596 given entry has not been modified in the last several weeks or months, it is 597 probably safe to transfer the coins into another entry and delete the old entry.</p> 598