Wed, 31 Dec 2014 07:22:50 +0100
Correct previous dual key logic pending first delivery installment.
michael@0 | 1 | /* This Source Code Form is subject to the terms of the Mozilla Public |
michael@0 | 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this |
michael@0 | 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
michael@0 | 4 | |
michael@0 | 5 | package org.mozilla.gecko.background.healthreport.upload; |
michael@0 | 6 | |
michael@0 | 7 | import java.util.ArrayList; |
michael@0 | 8 | import java.util.Collection; |
michael@0 | 9 | import java.util.Collections; |
michael@0 | 10 | import java.util.Comparator; |
michael@0 | 11 | import java.util.HashSet; |
michael@0 | 12 | import java.util.List; |
michael@0 | 13 | import java.util.Map.Entry; |
michael@0 | 14 | import java.util.Set; |
michael@0 | 15 | |
michael@0 | 16 | import org.mozilla.gecko.background.common.log.Logger; |
michael@0 | 17 | import org.mozilla.gecko.background.healthreport.HealthReportConstants; |
michael@0 | 18 | import org.mozilla.gecko.sync.ExtendedJSONObject; |
michael@0 | 19 | |
michael@0 | 20 | import android.content.SharedPreferences; |
michael@0 | 21 | |
michael@0 | 22 | public class ObsoleteDocumentTracker { |
michael@0 | 23 | public static final String LOG_TAG = ObsoleteDocumentTracker.class.getSimpleName(); |
michael@0 | 24 | |
michael@0 | 25 | protected final SharedPreferences sharedPrefs; |
michael@0 | 26 | |
michael@0 | 27 | public ObsoleteDocumentTracker(SharedPreferences sharedPrefs) { |
michael@0 | 28 | this.sharedPrefs = sharedPrefs; |
michael@0 | 29 | } |
michael@0 | 30 | |
michael@0 | 31 | protected ExtendedJSONObject getObsoleteIds() { |
michael@0 | 32 | String s = sharedPrefs.getString(HealthReportConstants.PREF_OBSOLETE_DOCUMENT_IDS_TO_DELETION_ATTEMPTS_REMAINING, null); |
michael@0 | 33 | if (s == null) { |
michael@0 | 34 | // It's possible we're migrating an old profile forward. |
michael@0 | 35 | String lastId = sharedPrefs.getString(HealthReportConstants.PREF_LAST_UPLOAD_DOCUMENT_ID, null); |
michael@0 | 36 | if (lastId == null) { |
michael@0 | 37 | return new ExtendedJSONObject(); |
michael@0 | 38 | } |
michael@0 | 39 | ExtendedJSONObject ids = new ExtendedJSONObject(); |
michael@0 | 40 | ids.put(lastId, HealthReportConstants.DELETION_ATTEMPTS_PER_OBSOLETE_DOCUMENT_ID); |
michael@0 | 41 | setObsoleteIds(ids); |
michael@0 | 42 | return ids; |
michael@0 | 43 | } |
michael@0 | 44 | try { |
michael@0 | 45 | return ExtendedJSONObject.parseJSONObject(s); |
michael@0 | 46 | } catch (Exception e) { |
michael@0 | 47 | Logger.warn(LOG_TAG, "Got exception getting obsolete ids.", e); |
michael@0 | 48 | return new ExtendedJSONObject(); |
michael@0 | 49 | } |
michael@0 | 50 | } |
michael@0 | 51 | |
michael@0 | 52 | /** |
michael@0 | 53 | * Write obsolete ids to disk. |
michael@0 | 54 | * |
michael@0 | 55 | * @param ids to write. |
michael@0 | 56 | */ |
michael@0 | 57 | protected void setObsoleteIds(ExtendedJSONObject ids) { |
michael@0 | 58 | sharedPrefs |
michael@0 | 59 | .edit() |
michael@0 | 60 | .putString(HealthReportConstants.PREF_OBSOLETE_DOCUMENT_IDS_TO_DELETION_ATTEMPTS_REMAINING, ids.toString()) |
michael@0 | 61 | .commit(); |
michael@0 | 62 | } |
michael@0 | 63 | |
michael@0 | 64 | /** |
michael@0 | 65 | * Remove id from set of obsolete document ids tracked for deletion. |
michael@0 | 66 | * |
michael@0 | 67 | * Public for testing. |
michael@0 | 68 | * |
michael@0 | 69 | * @param id to stop tracking. |
michael@0 | 70 | */ |
michael@0 | 71 | public void removeObsoleteId(String id) { |
michael@0 | 72 | ExtendedJSONObject ids = getObsoleteIds(); |
michael@0 | 73 | ids.remove(id); |
michael@0 | 74 | setObsoleteIds(ids); |
michael@0 | 75 | } |
michael@0 | 76 | |
michael@0 | 77 | protected void decrementObsoleteId(ExtendedJSONObject ids, String id) { |
michael@0 | 78 | if (!ids.containsKey(id)) { |
michael@0 | 79 | return; |
michael@0 | 80 | } |
michael@0 | 81 | try { |
michael@0 | 82 | Long attempts = ids.getLong(id); |
michael@0 | 83 | if (attempts == null || --attempts < 1) { |
michael@0 | 84 | ids.remove(id); |
michael@0 | 85 | } else { |
michael@0 | 86 | ids.put(id, attempts); |
michael@0 | 87 | } |
michael@0 | 88 | } catch (ClassCastException e) { |
michael@0 | 89 | ids.remove(id); |
michael@0 | 90 | Logger.info(LOG_TAG, "Got exception decrementing obsolete ids counter.", e); |
michael@0 | 91 | } |
michael@0 | 92 | } |
michael@0 | 93 | |
michael@0 | 94 | /** |
michael@0 | 95 | * Decrement attempts remaining for id in set of obsolete document ids tracked |
michael@0 | 96 | * for deletion. |
michael@0 | 97 | * |
michael@0 | 98 | * Public for testing. |
michael@0 | 99 | * |
michael@0 | 100 | * @param id to decrement attempts. |
michael@0 | 101 | */ |
michael@0 | 102 | public void decrementObsoleteIdAttempts(String id) { |
michael@0 | 103 | ExtendedJSONObject ids = getObsoleteIds(); |
michael@0 | 104 | decrementObsoleteId(ids, id); |
michael@0 | 105 | setObsoleteIds(ids); |
michael@0 | 106 | } |
michael@0 | 107 | |
michael@0 | 108 | public void purgeObsoleteIds(Collection<String> oldIds) { |
michael@0 | 109 | ExtendedJSONObject ids = getObsoleteIds(); |
michael@0 | 110 | for (String oldId : oldIds) { |
michael@0 | 111 | ids.remove(oldId); |
michael@0 | 112 | } |
michael@0 | 113 | setObsoleteIds(ids); |
michael@0 | 114 | } |
michael@0 | 115 | |
michael@0 | 116 | public void decrementObsoleteIdAttempts(Collection<String> oldIds) { |
michael@0 | 117 | ExtendedJSONObject ids = getObsoleteIds(); |
michael@0 | 118 | for (String oldId : oldIds) { |
michael@0 | 119 | decrementObsoleteId(ids, oldId); |
michael@0 | 120 | } |
michael@0 | 121 | setObsoleteIds(ids); |
michael@0 | 122 | } |
michael@0 | 123 | |
michael@0 | 124 | /** |
michael@0 | 125 | * Sort Longs in decreasing order, moving null and non-Longs to the front. |
michael@0 | 126 | * |
michael@0 | 127 | * Public for testing only. |
michael@0 | 128 | */ |
michael@0 | 129 | public static class PairComparator implements Comparator<Entry<String, Object>> { |
michael@0 | 130 | @Override |
michael@0 | 131 | public int compare(Entry<String, Object> lhs, Entry<String, Object> rhs) { |
michael@0 | 132 | Object l = lhs.getValue(); |
michael@0 | 133 | Object r = rhs.getValue(); |
michael@0 | 134 | if (l == null || !(l instanceof Long)) { |
michael@0 | 135 | if (r == null || !(r instanceof Long)) { |
michael@0 | 136 | return 0; |
michael@0 | 137 | } |
michael@0 | 138 | return -1; |
michael@0 | 139 | } |
michael@0 | 140 | if (r == null || !(r instanceof Long)) { |
michael@0 | 141 | return 1; |
michael@0 | 142 | } |
michael@0 | 143 | return ((Long) r).compareTo((Long) l); |
michael@0 | 144 | } |
michael@0 | 145 | } |
michael@0 | 146 | |
michael@0 | 147 | /** |
michael@0 | 148 | * Return a batch of obsolete document IDs that should be deleted next. |
michael@0 | 149 | * |
michael@0 | 150 | * Document IDs are long and sending too many in a single request might |
michael@0 | 151 | * increase the likelihood of POST failures, so we delete a (deterministic) |
michael@0 | 152 | * subset here. |
michael@0 | 153 | * |
michael@0 | 154 | * @return a non-null collection. |
michael@0 | 155 | */ |
michael@0 | 156 | public Collection<String> getBatchOfObsoleteIds() { |
michael@0 | 157 | ExtendedJSONObject ids = getObsoleteIds(); |
michael@0 | 158 | // Sort by increasing order of key values. |
michael@0 | 159 | List<Entry<String, Object>> pairs = new ArrayList<Entry<String,Object>>(ids.entrySet()); |
michael@0 | 160 | Collections.sort(pairs, new PairComparator()); |
michael@0 | 161 | List<String> batch = new ArrayList<String>(HealthReportConstants.MAXIMUM_DELETIONS_PER_POST); |
michael@0 | 162 | int i = 0; |
michael@0 | 163 | while (batch.size() < HealthReportConstants.MAXIMUM_DELETIONS_PER_POST && i < pairs.size()) { |
michael@0 | 164 | batch.add(pairs.get(i++).getKey()); |
michael@0 | 165 | } |
michael@0 | 166 | return batch; |
michael@0 | 167 | } |
michael@0 | 168 | |
michael@0 | 169 | /** |
michael@0 | 170 | * Track the given document ID for eventual obsolescence and deletion. |
michael@0 | 171 | * Obsolete IDs are not known to have been uploaded to the server, so we just |
michael@0 | 172 | * give a best effort attempt at deleting them |
michael@0 | 173 | * |
michael@0 | 174 | * @param id to eventually delete. |
michael@0 | 175 | */ |
michael@0 | 176 | public void addObsoleteId(String id) { |
michael@0 | 177 | ExtendedJSONObject ids = getObsoleteIds(); |
michael@0 | 178 | if (ids.size() >= HealthReportConstants.MAXIMUM_STORED_OBSOLETE_DOCUMENT_IDS) { |
michael@0 | 179 | // Remove the one that's been tried the most and is least likely to be |
michael@0 | 180 | // known to be on the server. Since the comparator orders in decreasing |
michael@0 | 181 | // order, we take the max. |
michael@0 | 182 | ids.remove(Collections.max(ids.entrySet(), new PairComparator()).getKey()); |
michael@0 | 183 | } |
michael@0 | 184 | ids.put(id, HealthReportConstants.DELETION_ATTEMPTS_PER_OBSOLETE_DOCUMENT_ID); |
michael@0 | 185 | setObsoleteIds(ids); |
michael@0 | 186 | } |
michael@0 | 187 | |
michael@0 | 188 | /** |
michael@0 | 189 | * Track the given document ID for eventual obsolescence and deletion, and |
michael@0 | 190 | * give it priority since we know this ID has made it to the server, and we |
michael@0 | 191 | * definitely don't want to orphan it. |
michael@0 | 192 | * |
michael@0 | 193 | * @param id to eventually delete. |
michael@0 | 194 | */ |
michael@0 | 195 | public void markIdAsUploaded(String id) { |
michael@0 | 196 | ExtendedJSONObject ids = getObsoleteIds(); |
michael@0 | 197 | ids.put(id, HealthReportConstants.DELETION_ATTEMPTS_PER_KNOWN_TO_BE_ON_SERVER_DOCUMENT_ID); |
michael@0 | 198 | setObsoleteIds(ids); |
michael@0 | 199 | } |
michael@0 | 200 | |
michael@0 | 201 | public boolean hasObsoleteIds() { |
michael@0 | 202 | return getObsoleteIds().size() > 0; |
michael@0 | 203 | } |
michael@0 | 204 | |
michael@0 | 205 | public int numberOfObsoleteIds() { |
michael@0 | 206 | return getObsoleteIds().size(); |
michael@0 | 207 | } |
michael@0 | 208 | |
michael@0 | 209 | public String getNextObsoleteId() { |
michael@0 | 210 | ExtendedJSONObject ids = getObsoleteIds(); |
michael@0 | 211 | if (ids.size() < 1) { |
michael@0 | 212 | return null; |
michael@0 | 213 | } |
michael@0 | 214 | try { |
michael@0 | 215 | // Delete the one that's most likely to be known to be on the server, and |
michael@0 | 216 | // that's not been tried as much. Since the comparator orders in |
michael@0 | 217 | // decreasing order, we take the min. |
michael@0 | 218 | return Collections.min(ids.entrySet(), new PairComparator()).getKey(); |
michael@0 | 219 | } catch (Exception e) { |
michael@0 | 220 | Logger.warn(LOG_TAG, "Got exception picking obsolete id to delete.", e); |
michael@0 | 221 | return null; |
michael@0 | 222 | } |
michael@0 | 223 | } |
michael@0 | 224 | |
michael@0 | 225 | /** |
michael@0 | 226 | * We want cleaning up documents on the server to be best effort. Purge badly |
michael@0 | 227 | * formed IDs and cap the number of times we try to delete so that the queue |
michael@0 | 228 | * doesn't take too long. |
michael@0 | 229 | */ |
michael@0 | 230 | public void limitObsoleteIds() { |
michael@0 | 231 | ExtendedJSONObject ids = getObsoleteIds(); |
michael@0 | 232 | |
michael@0 | 233 | Set<String> keys = new HashSet<String>(ids.keySet()); // Avoid invalidating an iterator. |
michael@0 | 234 | for (String key : keys) { |
michael@0 | 235 | Object o = ids.get(key); |
michael@0 | 236 | if (!(o instanceof Long)) { |
michael@0 | 237 | continue; |
michael@0 | 238 | } |
michael@0 | 239 | if (((Long) o).longValue() > HealthReportConstants.DELETION_ATTEMPTS_PER_OBSOLETE_DOCUMENT_ID) { |
michael@0 | 240 | ids.put(key, HealthReportConstants.DELETION_ATTEMPTS_PER_OBSOLETE_DOCUMENT_ID); |
michael@0 | 241 | } |
michael@0 | 242 | } |
michael@0 | 243 | setObsoleteIds(ids); |
michael@0 | 244 | } |
michael@0 | 245 | } |