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