services/common/bagheeraclient.js

Thu, 22 Jan 2015 13:21:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 22 Jan 2015 13:21:57 +0100
branch
TOR_BUG_9701
changeset 15
b8a032363ba2
permissions
-rw-r--r--

Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6

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 /**
michael@0 6 * This file contains a client API for the Bagheera data storage service.
michael@0 7 *
michael@0 8 * Information about Bagheera is available at
michael@0 9 * https://github.com/mozilla-metrics/bagheera
michael@0 10 */
michael@0 11
michael@0 12 "use strict";
michael@0 13
michael@0 14 #ifndef MERGED_COMPARTMENT
michael@0 15
michael@0 16 this.EXPORTED_SYMBOLS = [
michael@0 17 "BagheeraClient",
michael@0 18 "BagheeraClientRequestResult",
michael@0 19 ];
michael@0 20
michael@0 21 const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
michael@0 22
michael@0 23 #endif
michael@0 24
michael@0 25 Cu.import("resource://gre/modules/Promise.jsm");
michael@0 26 Cu.import("resource://gre/modules/Services.jsm");
michael@0 27 Cu.import("resource://gre/modules/Log.jsm");
michael@0 28 Cu.import("resource://services-common/rest.js");
michael@0 29 Cu.import("resource://services-common/utils.js");
michael@0 30
michael@0 31
michael@0 32 /**
michael@0 33 * Represents the result of a Bagheera request.
michael@0 34 */
michael@0 35 this.BagheeraClientRequestResult = function BagheeraClientRequestResult() {
michael@0 36 this.transportSuccess = false;
michael@0 37 this.serverSuccess = false;
michael@0 38 this.request = null;
michael@0 39 };
michael@0 40
michael@0 41 Object.freeze(BagheeraClientRequestResult.prototype);
michael@0 42
michael@0 43
michael@0 44 /**
michael@0 45 * Wrapper around RESTRequest so logging is sane.
michael@0 46 */
michael@0 47 function BagheeraRequest(uri) {
michael@0 48 RESTRequest.call(this, uri);
michael@0 49
michael@0 50 this._log = Log.repository.getLogger("Services.BagheeraClient");
michael@0 51 this._log.level = Log.Level.Debug;
michael@0 52 }
michael@0 53
michael@0 54 BagheeraRequest.prototype = Object.freeze({
michael@0 55 __proto__: RESTRequest.prototype,
michael@0 56 });
michael@0 57
michael@0 58
michael@0 59 /**
michael@0 60 * Create a new Bagheera client instance.
michael@0 61 *
michael@0 62 * Each client is associated with a specific Bagheera HTTP URI endpoint.
michael@0 63 *
michael@0 64 * @param baseURI
michael@0 65 * (string) The base URI of the Bagheera HTTP endpoint.
michael@0 66 */
michael@0 67 this.BagheeraClient = function BagheeraClient(baseURI) {
michael@0 68 if (!baseURI) {
michael@0 69 throw new Error("baseURI argument must be defined.");
michael@0 70 }
michael@0 71
michael@0 72 this._log = Log.repository.getLogger("Services.BagheeraClient");
michael@0 73 this._log.level = Log.Level.Debug;
michael@0 74
michael@0 75 this.baseURI = baseURI;
michael@0 76
michael@0 77 if (!baseURI.endsWith("/")) {
michael@0 78 this.baseURI += "/";
michael@0 79 }
michael@0 80 };
michael@0 81
michael@0 82 BagheeraClient.prototype = Object.freeze({
michael@0 83 /**
michael@0 84 * Channel load flags for all requests.
michael@0 85 *
michael@0 86 * Caching is not applicable, so we bypass and disable it. We also
michael@0 87 * ignore any cookies that may be present for the domain because
michael@0 88 * Bagheera does not utilize cookies and the release of cookies may
michael@0 89 * inadvertantly constitute unncessary information disclosure.
michael@0 90 */
michael@0 91 _loadFlags: Ci.nsIRequest.LOAD_BYPASS_CACHE |
michael@0 92 Ci.nsIRequest.INHIBIT_CACHING |
michael@0 93 Ci.nsIRequest.LOAD_ANONYMOUS,
michael@0 94
michael@0 95 DEFAULT_TIMEOUT_MSEC: 5 * 60 * 1000, // 5 minutes.
michael@0 96
michael@0 97 _RE_URI_IDENTIFIER: /^[a-zA-Z0-9_-]+$/,
michael@0 98
michael@0 99 /**
michael@0 100 * Upload a JSON payload to the server.
michael@0 101 *
michael@0 102 * The return value is a Promise which will be resolved with a
michael@0 103 * BagheeraClientRequestResult when the request has finished.
michael@0 104 *
michael@0 105 * @param namespace
michael@0 106 * (string) The namespace to post this data to.
michael@0 107 * @param id
michael@0 108 * (string) The ID of the document being uploaded. This is typically
michael@0 109 * a UUID in hex form.
michael@0 110 * @param payload
michael@0 111 * (string|object) Data to upload. Can be specified as a string (which
michael@0 112 * is assumed to be JSON) or an object. If an object, it will be fed into
michael@0 113 * JSON.stringify() for serialization.
michael@0 114 * @param options
michael@0 115 * (object) Extra options to control behavior. Recognized properties:
michael@0 116 *
michael@0 117 * deleteIDs -- (array) Old document IDs to delete as part of
michael@0 118 * upload. If not specified, no old documents will be deleted as
michael@0 119 * part of upload. The array values are typically UUIDs in hex
michael@0 120 * form.
michael@0 121 *
michael@0 122 * telemetryCompressed -- (string) Telemetry histogram to record
michael@0 123 * compressed size of payload under. If not defined, no telemetry
michael@0 124 * data for the compressed size will be recorded.
michael@0 125 *
michael@0 126 * @return Promise<BagheeraClientRequestResult>
michael@0 127 */
michael@0 128 uploadJSON: function uploadJSON(namespace, id, payload, options={}) {
michael@0 129 if (!namespace) {
michael@0 130 throw new Error("namespace argument must be defined.");
michael@0 131 }
michael@0 132
michael@0 133 if (!id) {
michael@0 134 throw new Error("id argument must be defined.");
michael@0 135 }
michael@0 136
michael@0 137 if (!payload) {
michael@0 138 throw new Error("payload argument must be defined.");
michael@0 139 }
michael@0 140
michael@0 141 if (options && typeof(options) != "object") {
michael@0 142 throw new Error("Unexpected type for options argument. Expected object. " +
michael@0 143 "Got: " + typeof(options));
michael@0 144 }
michael@0 145
michael@0 146 let uri = this._submitURI(namespace, id);
michael@0 147
michael@0 148 let data = payload;
michael@0 149
michael@0 150 if (typeof(payload) == "object") {
michael@0 151 data = JSON.stringify(payload);
michael@0 152 }
michael@0 153
michael@0 154 if (typeof(data) != "string") {
michael@0 155 throw new Error("Unknown type for payload: " + typeof(data));
michael@0 156 }
michael@0 157
michael@0 158 this._log.info("Uploading data to " + uri);
michael@0 159
michael@0 160 let request = new BagheeraRequest(uri);
michael@0 161 request.loadFlags = this._loadFlags;
michael@0 162 request.timeout = this.DEFAULT_TIMEOUT_MSEC;
michael@0 163
michael@0 164 // Since API changed, throw on old API usage.
michael@0 165 if ("deleteID" in options) {
michael@0 166 throw new Error("API has changed, use (array) deleteIDs instead");
michael@0 167 }
michael@0 168
michael@0 169 let deleteIDs;
michael@0 170 if (options.deleteIDs && options.deleteIDs.length > 0) {
michael@0 171 deleteIDs = options.deleteIDs;
michael@0 172 this._log.debug("Will delete " + deleteIDs.join(", "));
michael@0 173 request.setHeader("X-Obsolete-Document", deleteIDs.join(","));
michael@0 174 }
michael@0 175
michael@0 176 let deferred = Promise.defer();
michael@0 177
michael@0 178 data = CommonUtils.convertString(data, "uncompressed", "deflate");
michael@0 179 if (options.telemetryCompressed) {
michael@0 180 try {
michael@0 181 let h = Services.telemetry.getHistogramById(options.telemetryCompressed);
michael@0 182 h.add(data.length);
michael@0 183 } catch (ex) {
michael@0 184 this._log.warn("Unable to record telemetry for compressed payload size: " +
michael@0 185 CommonUtils.exceptionStr(ex));
michael@0 186 }
michael@0 187 }
michael@0 188
michael@0 189 // TODO proper header per bug 807134.
michael@0 190 request.setHeader("Content-Type", "application/json+zlib; charset=utf-8");
michael@0 191
michael@0 192 this._log.info("Request body length: " + data.length);
michael@0 193
michael@0 194 let result = new BagheeraClientRequestResult();
michael@0 195 result.namespace = namespace;
michael@0 196 result.id = id;
michael@0 197 result.deleteIDs = deleteIDs ? deleteIDs.slice(0) : null;
michael@0 198
michael@0 199 request.onComplete = this._onComplete.bind(this, request, deferred, result);
michael@0 200 request.post(data);
michael@0 201
michael@0 202 return deferred.promise;
michael@0 203 },
michael@0 204
michael@0 205 /**
michael@0 206 * Delete the specified document.
michael@0 207 *
michael@0 208 * @param namespace
michael@0 209 * (string) Namespace from which to delete the document.
michael@0 210 * @param id
michael@0 211 * (string) ID of document to delete.
michael@0 212 *
michael@0 213 * @return Promise<BagheeraClientRequestResult>
michael@0 214 */
michael@0 215 deleteDocument: function deleteDocument(namespace, id) {
michael@0 216 let uri = this._submitURI(namespace, id);
michael@0 217
michael@0 218 let request = new BagheeraRequest(uri);
michael@0 219 request.loadFlags = this._loadFlags;
michael@0 220 request.timeout = this.DEFAULT_TIMEOUT_MSEC;
michael@0 221
michael@0 222 let result = new BagheeraClientRequestResult();
michael@0 223 result.namespace = namespace;
michael@0 224 result.id = id;
michael@0 225 let deferred = Promise.defer();
michael@0 226
michael@0 227 request.onComplete = this._onComplete.bind(this, request, deferred, result);
michael@0 228 request.delete();
michael@0 229
michael@0 230 return deferred.promise;
michael@0 231 },
michael@0 232
michael@0 233 _submitURI: function _submitURI(namespace, id) {
michael@0 234 if (!this._RE_URI_IDENTIFIER.test(namespace)) {
michael@0 235 throw new Error("Illegal namespace name. Must be alphanumeric + [_-]: " +
michael@0 236 namespace);
michael@0 237 }
michael@0 238
michael@0 239 if (!this._RE_URI_IDENTIFIER.test(id)) {
michael@0 240 throw new Error("Illegal id value. Must be alphanumeric + [_-]: " + id);
michael@0 241 }
michael@0 242
michael@0 243 return this.baseURI + "1.0/submit/" + namespace + "/" + id;
michael@0 244 },
michael@0 245
michael@0 246 _onComplete: function _onComplete(request, deferred, result, error) {
michael@0 247 result.request = request;
michael@0 248
michael@0 249 if (error) {
michael@0 250 this._log.info("Transport failure on request: " +
michael@0 251 CommonUtils.exceptionStr(error));
michael@0 252 result.transportSuccess = false;
michael@0 253 deferred.resolve(result);
michael@0 254 return;
michael@0 255 }
michael@0 256
michael@0 257 result.transportSuccess = true;
michael@0 258
michael@0 259 let response = request.response;
michael@0 260
michael@0 261 switch (response.status) {
michael@0 262 case 200:
michael@0 263 case 201:
michael@0 264 result.serverSuccess = true;
michael@0 265 break;
michael@0 266
michael@0 267 default:
michael@0 268 result.serverSuccess = false;
michael@0 269
michael@0 270 this._log.info("Received unexpected status code: " + response.status);
michael@0 271 this._log.debug("Response body: " + response.body);
michael@0 272 }
michael@0 273
michael@0 274 deferred.resolve(result);
michael@0 275 },
michael@0 276 });
michael@0 277

mercurial