2012-11-08 00:25:09 +00:00
|
|
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
|
|
|
|
/**
|
|
|
|
* This file contains a client API for the Bagheera data storage service.
|
|
|
|
*
|
|
|
|
* Information about Bagheera is available at
|
|
|
|
* https://github.com/mozilla-metrics/bagheera
|
|
|
|
*/
|
|
|
|
|
|
|
|
"use strict";
|
|
|
|
|
2013-01-27 19:26:48 +00:00
|
|
|
#ifndef MERGED_COMPARTMENT
|
|
|
|
|
2012-11-08 00:25:09 +00:00
|
|
|
this.EXPORTED_SYMBOLS = [
|
|
|
|
"BagheeraClient",
|
|
|
|
"BagheeraClientRequestResult",
|
|
|
|
];
|
|
|
|
|
|
|
|
const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
|
|
|
|
|
2013-01-27 19:26:48 +00:00
|
|
|
#endif
|
|
|
|
|
2013-06-14 01:36:21 +00:00
|
|
|
Cu.import("resource://gre/modules/Promise.jsm");
|
2013-03-13 17:14:41 +00:00
|
|
|
Cu.import("resource://gre/modules/Services.jsm");
|
2012-12-05 22:36:27 +00:00
|
|
|
Cu.import("resource://services-common/log4moz.js");
|
2013-02-06 04:25:57 +00:00
|
|
|
Cu.import("resource://services-common/rest.js");
|
2012-12-05 22:36:27 +00:00
|
|
|
Cu.import("resource://services-common/utils.js");
|
2012-11-08 00:25:09 +00:00
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Represents the result of a Bagheera request.
|
|
|
|
*/
|
|
|
|
this.BagheeraClientRequestResult = function BagheeraClientRequestResult() {
|
|
|
|
this.transportSuccess = false;
|
|
|
|
this.serverSuccess = false;
|
|
|
|
this.request = null;
|
2013-02-06 04:25:57 +00:00
|
|
|
};
|
2012-11-08 00:25:09 +00:00
|
|
|
|
|
|
|
Object.freeze(BagheeraClientRequestResult.prototype);
|
|
|
|
|
2013-02-06 04:25:57 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Wrapper around RESTRequest so logging is sane.
|
|
|
|
*/
|
|
|
|
function BagheeraRequest(uri) {
|
|
|
|
RESTRequest.call(this, uri);
|
|
|
|
|
|
|
|
this._log = Log4Moz.repository.getLogger("Services.BagheeraClient");
|
|
|
|
this._log.level = Log4Moz.Level.Debug;
|
|
|
|
}
|
|
|
|
|
|
|
|
BagheeraRequest.prototype = Object.freeze({
|
|
|
|
__proto__: RESTRequest.prototype,
|
|
|
|
});
|
|
|
|
|
|
|
|
|
2012-11-08 00:25:09 +00:00
|
|
|
/**
|
|
|
|
* Create a new Bagheera client instance.
|
|
|
|
*
|
|
|
|
* Each client is associated with a specific Bagheera HTTP URI endpoint.
|
|
|
|
*
|
|
|
|
* @param baseURI
|
|
|
|
* (string) The base URI of the Bagheera HTTP endpoint.
|
|
|
|
*/
|
|
|
|
this.BagheeraClient = function BagheeraClient(baseURI) {
|
|
|
|
if (!baseURI) {
|
|
|
|
throw new Error("baseURI argument must be defined.");
|
|
|
|
}
|
|
|
|
|
|
|
|
this._log = Log4Moz.repository.getLogger("Services.BagheeraClient");
|
2013-02-06 04:25:57 +00:00
|
|
|
this._log.level = Log4Moz.Level.Debug;
|
2012-11-08 00:25:09 +00:00
|
|
|
|
|
|
|
this.baseURI = baseURI;
|
|
|
|
|
|
|
|
if (!baseURI.endsWith("/")) {
|
|
|
|
this.baseURI += "/";
|
|
|
|
}
|
2013-02-06 04:25:57 +00:00
|
|
|
};
|
2012-11-08 00:25:09 +00:00
|
|
|
|
2013-02-06 04:25:57 +00:00
|
|
|
BagheeraClient.prototype = Object.freeze({
|
2012-11-08 00:25:09 +00:00
|
|
|
/**
|
|
|
|
* Channel load flags for all requests.
|
|
|
|
*
|
|
|
|
* Caching is not applicable, so we bypass and disable it. We also
|
|
|
|
* ignore any cookies that may be present for the domain because
|
|
|
|
* Bagheera does not utilize cookies and the release of cookies may
|
|
|
|
* inadvertantly constitute unncessary information disclosure.
|
|
|
|
*/
|
|
|
|
_loadFlags: Ci.nsIRequest.LOAD_BYPASS_CACHE |
|
|
|
|
Ci.nsIRequest.INHIBIT_CACHING |
|
|
|
|
Ci.nsIRequest.LOAD_ANONYMOUS,
|
|
|
|
|
|
|
|
DEFAULT_TIMEOUT_MSEC: 5 * 60 * 1000, // 5 minutes.
|
|
|
|
|
|
|
|
_RE_URI_IDENTIFIER: /^[a-zA-Z0-9_-]+$/,
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Upload a JSON payload to the server.
|
|
|
|
*
|
|
|
|
* The return value is a Promise which will be resolved with a
|
|
|
|
* BagheeraClientRequestResult when the request has finished.
|
|
|
|
*
|
|
|
|
* @param namespace
|
|
|
|
* (string) The namespace to post this data to.
|
|
|
|
* @param id
|
|
|
|
* (string) The ID of the document being uploaded. This is typically
|
|
|
|
* a UUID in hex form.
|
|
|
|
* @param payload
|
|
|
|
* (string|object) Data to upload. Can be specified as a string (which
|
|
|
|
* is assumed to be JSON) or an object. If an object, it will be fed into
|
|
|
|
* JSON.stringify() for serialization.
|
2013-03-13 17:14:41 +00:00
|
|
|
* @param options
|
|
|
|
* (object) Extra options to control behavior. Recognized properties:
|
|
|
|
*
|
2013-06-21 17:30:30 +00:00
|
|
|
* deleteIDs -- (array) Old document IDs to delete as part of
|
2013-03-13 17:14:41 +00:00
|
|
|
* upload. If not specified, no old documents will be deleted as
|
2013-06-21 17:30:30 +00:00
|
|
|
* part of upload. The array values are typically UUIDs in hex
|
2013-03-13 17:14:41 +00:00
|
|
|
* form.
|
|
|
|
*
|
|
|
|
* telemetryCompressed -- (string) Telemetry histogram to record
|
|
|
|
* compressed size of payload under. If not defined, no telemetry
|
|
|
|
* data for the compressed size will be recorded.
|
2012-11-08 00:25:09 +00:00
|
|
|
*
|
|
|
|
* @return Promise<BagheeraClientRequestResult>
|
|
|
|
*/
|
2013-03-13 17:14:41 +00:00
|
|
|
uploadJSON: function uploadJSON(namespace, id, payload, options={}) {
|
2012-11-08 00:25:09 +00:00
|
|
|
if (!namespace) {
|
|
|
|
throw new Error("namespace argument must be defined.");
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!id) {
|
|
|
|
throw new Error("id argument must be defined.");
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!payload) {
|
|
|
|
throw new Error("payload argument must be defined.");
|
|
|
|
}
|
|
|
|
|
2013-03-13 17:14:41 +00:00
|
|
|
if (options && typeof(options) != "object") {
|
|
|
|
throw new Error("Unexpected type for options argument. Expected object. " +
|
|
|
|
"Got: " + typeof(options));
|
|
|
|
}
|
|
|
|
|
2012-11-08 00:25:09 +00:00
|
|
|
let uri = this._submitURI(namespace, id);
|
|
|
|
|
|
|
|
let data = payload;
|
|
|
|
|
|
|
|
if (typeof(payload) == "object") {
|
|
|
|
data = JSON.stringify(payload);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (typeof(data) != "string") {
|
|
|
|
throw new Error("Unknown type for payload: " + typeof(data));
|
|
|
|
}
|
|
|
|
|
|
|
|
this._log.info("Uploading data to " + uri);
|
|
|
|
|
2013-02-06 04:25:57 +00:00
|
|
|
let request = new BagheeraRequest(uri);
|
2012-11-08 00:25:09 +00:00
|
|
|
request.loadFlags = this._loadFlags;
|
|
|
|
request.timeout = this.DEFAULT_TIMEOUT_MSEC;
|
|
|
|
|
2013-06-21 17:30:30 +00:00
|
|
|
// Since API changed, throw on old API usage.
|
|
|
|
if ("deleteID" in options) {
|
|
|
|
throw new Error("API has changed, use (array) deleteIDs instead");
|
|
|
|
}
|
2013-05-10 18:04:48 +00:00
|
|
|
|
2013-06-21 17:30:30 +00:00
|
|
|
let deleteIDs;
|
|
|
|
if (options.deleteIDs && options.deleteIDs.length > 0) {
|
|
|
|
deleteIDs = options.deleteIDs;
|
|
|
|
this._log.debug("Will delete " + deleteIDs.join(", "));
|
|
|
|
request.setHeader("X-Obsolete-Document", deleteIDs.join(","));
|
2012-11-08 00:25:09 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
let deferred = Promise.defer();
|
|
|
|
|
|
|
|
data = CommonUtils.convertString(data, "uncompressed", "deflate");
|
2013-03-13 17:14:41 +00:00
|
|
|
if (options.telemetryCompressed) {
|
|
|
|
try {
|
|
|
|
let h = Services.telemetry.getHistogramById(options.telemetryCompressed);
|
|
|
|
h.add(data.length);
|
|
|
|
} catch (ex) {
|
|
|
|
this._log.warn("Unable to record telemetry for compressed payload size: " +
|
|
|
|
CommonUtils.exceptionStr(ex));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2012-11-08 00:25:09 +00:00
|
|
|
// TODO proper header per bug 807134.
|
|
|
|
request.setHeader("Content-Type", "application/json+zlib; charset=utf-8");
|
|
|
|
|
|
|
|
this._log.info("Request body length: " + data.length);
|
|
|
|
|
|
|
|
let result = new BagheeraClientRequestResult();
|
|
|
|
result.namespace = namespace;
|
|
|
|
result.id = id;
|
2013-06-21 17:30:30 +00:00
|
|
|
result.deleteIDs = deleteIDs ? deleteIDs.slice(0) : null;
|
2012-11-08 00:25:09 +00:00
|
|
|
|
|
|
|
request.onComplete = this._onComplete.bind(this, request, deferred, result);
|
|
|
|
request.post(data);
|
|
|
|
|
|
|
|
return deferred.promise;
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Delete the specified document.
|
|
|
|
*
|
|
|
|
* @param namespace
|
|
|
|
* (string) Namespace from which to delete the document.
|
|
|
|
* @param id
|
|
|
|
* (string) ID of document to delete.
|
|
|
|
*
|
|
|
|
* @return Promise<BagheeraClientRequestResult>
|
|
|
|
*/
|
|
|
|
deleteDocument: function deleteDocument(namespace, id) {
|
|
|
|
let uri = this._submitURI(namespace, id);
|
|
|
|
|
2013-02-06 04:25:57 +00:00
|
|
|
let request = new BagheeraRequest(uri);
|
2012-11-08 00:25:09 +00:00
|
|
|
request.loadFlags = this._loadFlags;
|
|
|
|
request.timeout = this.DEFAULT_TIMEOUT_MSEC;
|
|
|
|
|
|
|
|
let result = new BagheeraClientRequestResult();
|
|
|
|
result.namespace = namespace;
|
|
|
|
result.id = id;
|
|
|
|
let deferred = Promise.defer();
|
|
|
|
|
|
|
|
request.onComplete = this._onComplete.bind(this, request, deferred, result);
|
|
|
|
request.delete();
|
|
|
|
|
|
|
|
return deferred.promise;
|
|
|
|
},
|
|
|
|
|
|
|
|
_submitURI: function _submitURI(namespace, id) {
|
|
|
|
if (!this._RE_URI_IDENTIFIER.test(namespace)) {
|
|
|
|
throw new Error("Illegal namespace name. Must be alphanumeric + [_-]: " +
|
|
|
|
namespace);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!this._RE_URI_IDENTIFIER.test(id)) {
|
|
|
|
throw new Error("Illegal id value. Must be alphanumeric + [_-]: " + id);
|
|
|
|
}
|
|
|
|
|
|
|
|
return this.baseURI + "1.0/submit/" + namespace + "/" + id;
|
|
|
|
},
|
|
|
|
|
|
|
|
_onComplete: function _onComplete(request, deferred, result, error) {
|
|
|
|
result.request = request;
|
|
|
|
|
|
|
|
if (error) {
|
|
|
|
this._log.info("Transport failure on request: " +
|
|
|
|
CommonUtils.exceptionStr(error));
|
|
|
|
result.transportSuccess = false;
|
|
|
|
deferred.resolve(result);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
result.transportSuccess = true;
|
|
|
|
|
|
|
|
let response = request.response;
|
|
|
|
|
|
|
|
switch (response.status) {
|
|
|
|
case 200:
|
|
|
|
case 201:
|
|
|
|
result.serverSuccess = true;
|
|
|
|
break;
|
|
|
|
|
|
|
|
default:
|
|
|
|
result.serverSuccess = false;
|
|
|
|
|
|
|
|
this._log.info("Received unexpected status code: " + response.status);
|
|
|
|
this._log.debug("Response body: " + response.body);
|
|
|
|
}
|
|
|
|
|
|
|
|
deferred.resolve(result);
|
|
|
|
},
|
2013-02-06 04:25:57 +00:00
|
|
|
});
|
2012-11-08 00:25:09 +00:00
|
|
|
|