Thu, 22 Jan 2015 13:21:57 +0100
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 |