Rietveld Code Review Tool
Help | Bug tracker | Discussion group | Source code

Side by Side Diff: lib/downloader.js

Issue 11153017: Improve Synchronizer functionality (Closed)
Patch Set: Created July 17, 2013, 12:32 p.m.
Left:
Right:
Use n/p to move between diff chunks; N/P to move between comments.
Jump to:
View unified diff | Download patch
« no previous file with comments | « chrome/content/ui/sendReport.js ('k') | lib/subscriptionClasses.js » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(Empty)
1 /*
2 * This file is part of Adblock Plus <http://adblockplus.org/>,
3 * Copyright (C) 2006-2013 Eyeo GmbH
4 *
5 * Adblock Plus is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License version 3 as
7 * published by the Free Software Foundation.
8 *
9 * Adblock Plus is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with Adblock Plus. If not, see <http://www.gnu.org/licenses/>.
16 */
17
18 /**
19 * @fileOverview Downloads a set of URLs in regular time intervals.
20 */
21
22 let {Utils} = require("utils");
23
24 let MILLIS_IN_SECOND = exports.MILLIS_IN_SECOND = 1000;
25 let MILLIS_IN_MINUTE = exports.MILLIS_IN_MINUTE = 60 * MILLIS_IN_SECOND;
26 let MILLIS_IN_HOUR = exports.MILLIS_IN_HOUR = 60 * MILLIS_IN_MINUTE;
27 let MILLIS_IN_DAY = exports.MILLIS_IN_DAY = 24 * MILLIS_IN_HOUR;
28
29 /**
30 * Creates a new downloader instance.
31 * @param {Function} dataSource Function that will yield downloadable objects o n each check
32 * @param {Integer} initialDelay Number of milliseconds to wait before the firs t check
33 * @param {Integer} checkInterval Interval between the checks
34 * @constructor
35 */
36 let Downloader = exports.Downloader = function Downloader(dataSource, initialDel ay, checkInterval)
37 {
38 this.dataSource = dataSource;
39 this._timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
40 this._timer.initWithCallback(function()
41 {
42 this._timer.delay = checkInterval;
43 this._doCheck();
44 }.bind(this), initialDelay, Ci.nsITimer.TYPE_REPEATING_SLACK);
45 this._downloading = Object.create(null);
46 }
47 Downloader.prototype =
48 {
49 /**
50 * Timer triggering the downloads.
51 * @type nsITimer
52 */
53 _timer: null,
54
55 /**
56 * Map containing the URLs of objects currently being downloaded as its keys.
57 */
58 _downloading: null,
59
60 /**
61 * Function that will yield downloadable objects on each check.
62 * @type Function
63 */
64 dataSource: null,
65
66 /**
67 * Maximal time interval that the checks can be left out until the soft
68 * expiration interval increases.
69 * @type Integer
70 */
71 maxAbsenseInterval: 1 * MILLIS_IN_DAY,
Thomas Greiner 2013/07/25 15:21:37 typo: maxAbsenceInterval
72
73 /**
74 * Minimal time interval before retrying a download after an error.
75 * @type Integer
76 */
77 minRetryInterval: 1 * MILLIS_IN_DAY,
78
79 /**
80 * Maximal allowed expiration interval, larger expiration intervals will be
81 * corrected.
82 * @type Integer
83 */
84 maxExpirationInterval: 14 * MILLIS_IN_DAY,
85
86 /**
87 * Maximal number of redirects before the download is considered as failed.
88 * @type Integer
89 */
90 maxRedirects: 5,
91
92 /**
93 * Called whenever expiration intervals for an object need to be adapted.
94 * @type Function
95 */
96 onExpirationChange: null,
97
98 /**
99 * Callback to be triggered whenever a download starts.
100 * @type Function
101 */
102 onDownloadStarted: null,
103
104 /**
105 * Callback to be triggered whenever a download finishes successfully. The
106 * callback can return an error code to indicate that the data is wrong.
107 * @type Function
108 */
109 onDownloadSuccess: null,
110
111 /**
112 * Callback to be triggered whenever a download fails.
113 * @type Function
114 */
115 onDownloadError: null,
116
117 /**
118 * Checks whether anything needs downloading.
119 */
120 _doCheck: function()
121 {
122 let now = Date.now();
123 for each (let downloadable in this.dataSource())
124 {
125 if (downloadable.lastCheck && now - downloadable.lastCheck > this.maxAbsen seInterval)
126 {
127 // No checks for a long time interval - user must have been offline, e.g .
128 // during a weekend. Increase soft expiration to prevent load peaks on t he
129 // server.
130 downloadable.softExpiration += now - downloadable.lastCheck;
131 }
132 downloadable.lastCheck = now;
133
134 // Sanity check: do expiration times make sense? Make sure people changing
135 // system clock don't get stuck with outdated subscriptions.
136 if (downloadable.hardExpiration - now > this.maxExpirationInterval)
137 downloadable.hardExpiration = now + this.maxExpirationInterval;
138 if (downloadable.softExpiration - now > this.maxExpirationInterval)
139 downloadable.softExpiration = now + this.maxExpirationInterval;
140
141 // Notify the caller about changes to expiration parameters
142 if (this.onExpirationChange)
143 this.onExpirationChange(downloadable);
144
145 // Does that object need downloading?
146 if (downloadable.softExpiration > now && downloadable.hardExpiration > now )
147 continue;
148
149 // Do not retry downloads too often
150 if (downloadable.lastError && now - downloadable.lastError < this.minRetry Interval)
151 continue;
152
153 this._download(downloadable, 0);
154 }
155 },
156
157 /**
158 * Stops the periodic checks.
159 */
160 cancel: function()
161 {
162 this._timer.cancel();
163 },
164
165 /**
166 * Checks whether an address is currently being downloaded.
167 */
168 isDownloading: function(/**String*/ url) /**Boolean*/
169 {
170 return url in this._downloading;
171 },
172
173 /**
174 * Starts a download.
175 * @param {Downloadable} url the object to be downloaded
Felix Dahlke 2013/07/25 13:16:02 The doc strings are a bit inconsistent. The one ab
Wladimir Palant 2013/11/05 06:40:42 I generally use @param only when there is a need t
176 */
177 download: function(downloadable)
178 {
179 // Make sure to detach download from the current execution context
180 Utils.runAsync(this._download.bind(this, downloadable, 0));
181 },
182
183 /**
184 * Generates the real download URL for an object by appending various
185 * parameters.
186 */
187 getDownloadUrl: function(/**Downloadable*/ downloadable) /** String*/
188 {
189 let {addonName, addonVersion, application} = require("info");
190 let url = downloadable.redirectURL || downloadable.url;
191 if (url.indexOf("?") >= 0)
192 url += "&";
193 else
194 url += "?";
195 url += "addonName=" + encodeURIComponent(addonName) +
196 "&addonVersion=" + encodeURIComponent(addonVersion) +
197 "&application=" + encodeURIComponent(application) +
198 "&lastVersion=" + encodeURIComponent(downloadable.lastVersion);
199 return url;
200 },
201
202 _download: function(downloadable, redirects)
203 {
204 if (downloadable.url in this._downloading)
Thomas Greiner 2013/07/25 15:21:37 You could use this.isDownloading(downloadable.url)
Wladimir Palant 2013/11/05 06:40:42 It was hard to convince myself that microoptimizin
205 return;
206
207 let downloadURL = this.getDownloadUrl(downloadable);
Felix Dahlke 2013/07/25 13:16:02 Inconsistent camel casing :P
208 let request = null;
209
210 let errorCallback = function errorCallback(error)
211 {
212 let channelStatus = -1;
213 try
214 {
215 channelStatus = request.channel.status;
216 } catch (e) {}
217
218 let responseStatus = "";
219 try
220 {
221 responseStatus = request.channel.QueryInterface(Ci.nsIHttpChannel).respo nseStatus;
222 } catch (e) {}
223
224 Cu.reportError("Adblock Plus: Downloading URL " + downloadable.url + " fai led (" + error + ")\n" +
225 "Download address: " + downloadURL + "\n" +
226 "Channel status: " + channelStatus + "\n" +
227 "Server response: " + responseStatus);
228
229 if (this.onDownloadError)
230 {
231 // Allow one extra redirect if the error handler gives us a redirect URL
232 let redirectCallback = null;
233 if (redirects <= this.maxRedirects)
234 {
235 redirectCallback = function redirectCallback(url)
236 {
237 downloadable.redirectURL = url;
238 this._download(downloadable, redirects + 1);
239 }.bind(this);
240 }
241
242 this.onDownloadError(downloadable, downloadURL, error, channelStatus, re sponseStatus, redirectCallback);
243 }
244 }.bind(this);
245
246 try
247 {
248 request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(Ci. nsIXMLHttpRequest);
249 request.mozBackgroundRequest = true;
250 request.open("GET", downloadURL);
251 }
252 catch (e)
253 {
254 errorCallback("synchronize_invalid_url");
255 return;
256 }
257
258 try {
259 request.overrideMimeType("text/plain");
260 request.channel.loadFlags = request.channel.loadFlags |
261 request.channel.INHIBIT_CACHING |
262 request.channel.VALIDATE_ALWAYS;
263
264 // Override redirect limit from preferences, user might have set it to 1
265 if (request.channel instanceof Ci.nsIHttpChannel)
266 request.channel.redirectionLimit = this.maxRedirects;
267 }
268 catch (e)
269 {
270 Cu.reportError(e)
271 }
272
273 request.addEventListener("error", function(event)
274 {
275 if (onShutdown.done)
276 return;
277
278 delete this._downloading[downloadable.url];
279 errorCallback("synchronize_connection_error");
280 }.bind(this), false);
281
282 request.addEventListener("load", function(event)
283 {
284 if (onShutdown.done)
285 return;
286
287 delete this._downloading[downloadable.url];
288
289 // Status will be 0 for non-HTTP requests
290 if (request.status && request.status != 200)
291 {
292 errorCallback("synchronize_connection_error");
293 return;
294 }
295
296 this.onDownloadSuccess(downloadable, request.responseText, errorCallback, function redirectCallback(url)
297 {
298 if (redirects >= this.maxRedirects)
299 errorCallback("synchronize_connection_error");
300 else
301 {
302 downloadable.redirectURL = url;
303 this._download(downloadable, redirects + 1);
304 }
305 }.bind(this));
306 }.bind(this), false);
307
308 this._downloading[downloadable.url] = true;
309 if (this.onDownloadStarted)
310 this.onDownloadStarted(downloadable);
Thomas Greiner 2013/07/25 15:21:37 Seems weird to me that onDownloadStarted is fired
Wladimir Palant 2013/11/05 06:40:42 Well, nothing really wrong with it (assuming that
311 request.send(null);
312 },
313
314 /**
315 * Produces a soft and a hard expiration interval for a given supplied
316 * expiration interval.
317 * @return {Array} soft and hard expiration interval
318 */
319 processExpirationInterval: function(/**Integer*/ interval)
320 {
321 interval = Math.min(Math.max(interval, 0), this.maxExpirationInterval);
322 let soft = Math.round(interval * (Math.random() * 0.4 + 0.8));
323 let hard = interval * 2;
324 let now = Date.now();
325 return [now + soft, now + hard];
326 }
327 };
328
329 /**
330 * An object that can be downloaded by the downloadable
331 * @param {String} url URL that has to be requested for the object
332 * @constructor
333 */
334 let Downloadable = exports.Downloadable = function Downloadable(url)
335 {
336 this.url = url;
337 }
338 Downloadable.prototype =
339 {
340 /**
341 * URL that has to be requested for the object.
342 * @type String
343 */
344 url: null,
345
346 /**
347 * URL that the download was redirected to if any.
348 * @type String
349 */
350 redirectURL: null,
351
352 /**
353 * Time of last download error or 0 if the last download was successful.
354 * @type Integer
355 */
356 lastError: 0,
357
358 /**
359 * Time of last check whether the object needs downloading.
360 * @type Integer
361 */
362 lastCheck: 0,
363
364 /**
365 * Object version corresponding to the last successful download.
366 * @type Integer
367 */
368 lastVersion: 0,
369
370 /**
371 * Soft expiration interval, will increase if no checks are performed for a
372 * while.
373 * @type Integer
374 */
375 softExpiration: 0,
376
377 /**
378 * Hard expiration interval, this is fixed.
379 * @type Integer
380 */
381 hardExpiration: 0,
382 };
OLDNEW
« no previous file with comments | « chrome/content/ui/sendReport.js ('k') | lib/subscriptionClasses.js » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld