Home | History | Annotate | Download | only in cloudsave
      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">&#64;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 &quot;best&quot;. 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&mdash;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 &quot;drawer&quot; 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&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
    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