kmccormick | 71baac1 | 2013-06-11 13:17:55 -0700 | [diff] [blame] | 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> |