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

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

mercurial