blob: 73e75dba13e1625246e5943f34c5c212f7490b52 [file] [log] [blame]
kmccormick71baac12013-06-11 13:17:55 -07001page.title=Resolving Cloud Save Conflicts
Joe Fernandez33baa5a2013-11-14 11:41:19 -08002page.tags=cloud
kmccormick71baac12013-06-11 13:17:55 -07003
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">&#64;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 &quot;best&quot;. 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&mdash;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 &quot;drawer&quot; 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&mdash;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&mdash;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
Joe Fernandez33baa5a2013-11-14 11:41:19 -0800597probably safe to transfer the coins into another entry and delete the old entry.</p>