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
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 });