Skip to content

Commit 3eefee9

Browse files
authored
Merge pull request #174 from Dexterp37/upload_circular_deps
Bug 1701578 - Remove a circular dependency in the upload module
2 parents 1f90e71 + 6d7806b commit 3eefee9

File tree

5 files changed

+140
-44
lines changed

5 files changed

+140
-44
lines changed

glean/src/core/glean.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -225,17 +225,16 @@ class Glean {
225225
// The configuration constructor will throw in case config has any incorrect prop.
226226
const correctConfig = new Configuration(config);
227227

228-
// Initialize the ping uploader with either the platform defaults or a custom
229-
// provided uploader from the configuration object.
230-
Glean.instance._pingUploader =
231-
new PingUploader(correctConfig.httpClient ? correctConfig.httpClient : Glean.platform.uploader);
232-
233228
Glean.instance._db = {
234229
metrics: new MetricsDatabase(Glean.platform.Storage),
235230
events: new EventsDatabase(Glean.platform.Storage),
236-
pings: new PingsDatabase(Glean.platform.Storage, Glean.pingUploader)
231+
pings: new PingsDatabase(Glean.platform.Storage)
237232
};
238233

234+
Glean.instance._pingUploader = new PingUploader(correctConfig, Glean.platform, Glean.pingsDatabase);
235+
236+
Glean.pingsDatabase.attachObserver(Glean.pingUploader);
237+
239238
if (config?.plugins) {
240239
for (const plugin of config.plugins) {
241240
registerPluginToEvent(plugin);
@@ -264,6 +263,7 @@ class Glean {
264263
// This is fine, we are inside a dispatched task that is guaranteed to run before any
265264
// other task. No external API call will be executed before we leave this task.
266265
Glean.instance._initialized = true;
266+
Glean.pingUploader.setInitialized(true);
267267

268268
// The upload enabled flag may have changed since the last run, for
269269
// example by the changing of a config file.
@@ -295,7 +295,7 @@ class Glean {
295295
}
296296
}
297297

298-
await Glean.pingUploader.scanPendingPings();
298+
await Glean.pingsDatabase.scanPendingPings();
299299

300300
// Even though this returns a promise, there is no need to block on it returning.
301301
//

glean/src/core/pings/database.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,16 @@ class PingsDatabase {
6464
private store: Store;
6565
private observer?: Observer;
6666

67-
constructor(store: StorageBuilder, observer?: Observer) {
67+
constructor(store: StorageBuilder) {
6868
this.store = new store("pings");
69+
}
70+
71+
/**
72+
* Attach an observer that reacts to the pings storage changes.
73+
*
74+
* @param observer The new observer to attach.
75+
*/
76+
attachObserver(observer: Observer): void {
6977
this.observer = observer;
7078
}
7179

@@ -128,6 +136,22 @@ class PingsDatabase {
128136
return finalPings;
129137
}
130138

139+
/**
140+
* Scans the database for pending pings and enqueues them.
141+
*/
142+
async scanPendingPings(): Promise<void> {
143+
// If there's no observer, then there's no point in iterating.
144+
if (!this.observer) {
145+
return;
146+
}
147+
148+
const pings = await this.getAllPings();
149+
for (const identifier in pings) {
150+
// Notify the observer that a new ping has been added to the pings database.
151+
this.observer.update(identifier, pings[identifier]);
152+
}
153+
}
154+
131155
/**
132156
* Clears all the pings from the database.
133157
*/

glean/src/core/upload/index.ts

Lines changed: 35 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,12 @@
22
* License, v. 2.0. If a copy of the MPL was not distributed with this
33
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
44

5+
import Platform from "../../platform/index.js";
6+
import { Configuration } from "../config.js";
57
import { GLEAN_VERSION } from "../constants.js";
68
import { Observer as PingsDatabaseObserver, PingInternalRepresentation } from "../pings/database.js";
7-
import Glean from "../glean.js";
9+
import type PingsDatabase from "../pings/database.js";
10+
import PlatformInfo from "../platform_info.js";
811
import Uploader, { UploadResult, UploadResultStatus } from "./uploader.js";
912

1013
interface QueuedPing extends PingInternalRepresentation {
@@ -45,10 +48,34 @@ class PingUploader implements PingsDatabaseObserver {
4548
// The object that concretely handles the ping transmission.
4649
private readonly uploader: Uploader;
4750

48-
constructor(uploader: Uploader) {
51+
private readonly platformInfo: PlatformInfo;
52+
private readonly serverEndpoint: string;
53+
private readonly pingsDatabase: PingsDatabase;
54+
55+
// Whether or not Glean was initialized, as reported by the
56+
// singleton object.
57+
private initialized = false;
58+
59+
constructor(config: Configuration, platform: Platform, pingsDatabase: PingsDatabase) {
4960
this.queue = [];
5061
this.status = PingUploaderStatus.Idle;
51-
this.uploader = uploader;
62+
// Initialize the ping uploader with either the platform defaults or a custom
63+
// provided uploader from the configuration object.
64+
this.uploader = config.httpClient ? config.httpClient : platform.uploader;
65+
this.platformInfo = platform.info;
66+
this.serverEndpoint = config.serverEndpoint;
67+
this.pingsDatabase = pingsDatabase;
68+
}
69+
70+
/**
71+
* Signals that initialization of Glean was completed.
72+
*
73+
* This is required in order to not depend on the Glean object.
74+
*
75+
* @param state An optional state to set the initialization status to.
76+
*/
77+
setInitialized(state?: boolean): void {
78+
this.initialized = state ?? true;
5279
}
5380

5481
/**
@@ -105,7 +132,7 @@ class PingUploader implements PingsDatabaseObserver {
105132
"Date": (new Date()).toISOString(),
106133
"X-Client-Type": "Glean.js",
107134
"X-Client-Version": GLEAN_VERSION,
108-
"User-Agent": `Glean/${GLEAN_VERSION} (JS on ${await Glean.platform.info.os()})`
135+
"User-Agent": `Glean/${GLEAN_VERSION} (JS on ${await this.platformInfo.os()})`
109136
};
110137

111138
return {
@@ -122,7 +149,7 @@ class PingUploader implements PingsDatabaseObserver {
122149
* @returns The status number of the response or `undefined` if unable to attempt upload.
123150
*/
124151
private async attemptPingUpload(ping: QueuedPing): Promise<UploadResult> {
125-
if (!Glean.initialized) {
152+
if (!this.initialized) {
126153
console.warn("Attempted to upload a ping, but Glean is not initialized yet. Ignoring.");
127154
return { result: UploadResultStatus.RecoverableFailure };
128155
}
@@ -132,9 +159,7 @@ class PingUploader implements PingsDatabaseObserver {
132159
// We are sure that the applicationId is not `undefined` at this point,
133160
// this function is only called when submitting a ping
134161
// and that function return early when Glean is not initialized.
135-
//
136-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
137-
`${Glean.serverEndpoint!}${ping.path}`,
162+
`${this.serverEndpoint}${ping.path}`,
138163
finalPing.payload,
139164
finalPing.headers
140165
);
@@ -183,14 +208,14 @@ class PingUploader implements PingsDatabaseObserver {
183208
const { status, result } = response;
184209
if (status && status >= 200 && status < 300) {
185210
console.info(`Ping ${identifier} succesfully sent ${status}.`);
186-
await Glean.pingsDatabase.deletePing(identifier);
211+
await this.pingsDatabase.deletePing(identifier);
187212
return false;
188213
}
189214

190215
if (result === UploadResultStatus.UnrecoverableFailure || (status && status >= 400 && status < 500)) {
191216
console.warn(
192217
`Unrecoverable upload failure while attempting to send ping ${identifier}. Error was ${status ?? "no status"}.`);
193-
await Glean.pingsDatabase.deletePing(identifier);
218+
await this.pingsDatabase.deletePing(identifier);
194219
return false;
195220
}
196221

@@ -273,16 +298,6 @@ class PingUploader implements PingsDatabaseObserver {
273298
this.queue = [];
274299
}
275300

276-
/**
277-
* Scans the database for pending pings and enqueues them.
278-
*/
279-
async scanPendingPings(): Promise<void> {
280-
const pings = await Glean.pingsDatabase.getAllPings();
281-
for (const identifier in pings) {
282-
this.enqueuePing({ identifier, ...pings[identifier] });
283-
}
284-
}
285-
286301
/**
287302
* Enqueues a new ping and trigger uploading of enqueued pings.
288303
*

glean/tests/core/pings/database.spec.ts

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,13 +48,14 @@ describe("PingsDatabase", function() {
4848
let wasNotified = false;
4949
const identifier = "THE IDENTIFIER";
5050
const observer: Observer = {
51-
update: (id: string): void => {
51+
update(id: string): void {
5252
wasNotified = true;
5353
assert.strictEqual(id, identifier);
5454
}
5555
};
5656

57-
const db = new Database(Glean.platform.Storage, observer);
57+
const db = new Database(Glean.platform.Storage);
58+
db.attachObserver(observer);
5859
const path = "some/random/path/doesnt/matter";
5960

6061
const payload = {
@@ -234,4 +235,51 @@ describe("PingsDatabase", function() {
234235
assert.strictEqual(Object.keys(await db["store"]._getWholeStore()).length, 0);
235236
});
236237
});
238+
239+
describe("pending pings", function() {
240+
it("scanning the pending pings directory fills up the queue", async function() {
241+
let resolver: (value: unknown) => void;
242+
const testPromise = new Promise(r => resolver = r);
243+
let pingIds: string[] = [];
244+
const observer: Observer = {
245+
update(id: string): void {
246+
pingIds.push(id);
247+
248+
if (pingIds.length == 10) {
249+
resolver(pingIds);
250+
}
251+
}
252+
};
253+
const db = new Database(Glean.platform.Storage);
254+
db.attachObserver(observer);
255+
256+
const path = "some/random/path/doesnt/matter";
257+
const payload = {
258+
ping_info: {
259+
seq: 1,
260+
start_time: "2018-02-24+01:00",
261+
end_time: "2018-02-25+11:00",
262+
},
263+
client_info: {
264+
telemetry_sdk_build: "32.0.0"
265+
}
266+
};
267+
268+
for (let id = 0; id < 10; id++) {
269+
const newPayload = payload;
270+
newPayload.ping_info.seq = id;
271+
await db.recordPing(path, `id-${id}`, payload);
272+
}
273+
274+
// Reset the ids we've seen because `Observer` will get called once again in `record`.
275+
pingIds = [];
276+
277+
await db.scanPendingPings();
278+
await testPromise;
279+
assert.strictEqual(pingIds.length, 10);
280+
for (let id = 0; id < 10; id++) {
281+
assert.ok(id in pingIds);
282+
}
283+
});
284+
});
237285
});

glean/tests/core/upload/index.spec.ts

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import assert from "assert";
66
import sinon from "sinon";
77
import { v4 as UUIDv4 } from "uuid";
88

9+
import { Configuration } from "../../../src/core/config";
910
import Glean from "../../../src/core/glean";
1011
import PingType from "../../../src/core/pings";
1112
import collectAndStorePing from "../../../src/core/pings/maker";
@@ -66,15 +67,6 @@ describe("PingUploader", function() {
6667
await Glean.testResetGlean(testAppId);
6768
});
6869

69-
it("scanning the pending pings directory fills up the queue", async function() {
70-
disableGleanUploader();
71-
await fillUpPingsDatabase(10);
72-
73-
const uploader = new PingUploader(Glean.platform.uploader);
74-
await uploader.scanPendingPings();
75-
assert.strictEqual(uploader["queue"].length, 10);
76-
});
77-
7870
it("whenever the pings dabase records a new ping, upload is triggered", async function() {
7971
const spy = sandbox.spy(Glean["pingUploader"], "triggerUpload");
8072
await fillUpPingsDatabase(10);
@@ -85,10 +77,25 @@ describe("PingUploader", function() {
8577
disableGleanUploader();
8678
await fillUpPingsDatabase(10);
8779

88-
const uploader = new PingUploader(Glean.platform.uploader);
89-
await uploader.scanPendingPings();
80+
// Create a new uploader and attach it to the existing storage.
81+
const uploader = new PingUploader(new Configuration(), Glean.platform, Glean.pingsDatabase);
82+
uploader.setInitialized();
83+
Glean.pingsDatabase.attachObserver(uploader);
84+
85+
// Mock the 'triggerUpload' function so that 'scanPendingPings' does not
86+
// mistakenly trigger ping submission. Note that since we're swapping
87+
// the function back later in this test, we can safely shut down the linter.
88+
89+
// eslint-disable-next-line @typescript-eslint/unbound-method
90+
const uploadTriggerFunc = uploader.triggerUpload;
91+
uploader.triggerUpload = async () => {
92+
// Intentionally empty.
93+
};
94+
95+
await Glean.pingsDatabase.scanPendingPings();
9096
assert.strictEqual(uploader["queue"].length, 10);
9197

98+
uploader.triggerUpload = uploadTriggerFunc;
9299
await uploader.triggerUpload();
93100
assert.deepStrictEqual(await Glean.pingsDatabase.getAllPings(), {});
94101
assert.strictEqual(uploader["queue"].length, 0);
@@ -105,8 +112,10 @@ describe("PingUploader", function() {
105112
disableGleanUploader();
106113
await fillUpPingsDatabase(10);
107114

108-
const uploader = new PingUploader(Glean.platform.uploader);
109-
await uploader.scanPendingPings();
115+
const uploader = new PingUploader(new Configuration(), Glean.platform, Glean.pingsDatabase);
116+
uploader.setInitialized();
117+
Glean.pingsDatabase.attachObserver(uploader);
118+
await Glean.pingsDatabase.scanPendingPings();
110119

111120
// Trigger uploading, but don't wait for it to finish,
112121
// so that it is ongoing when we cancel.
@@ -157,7 +166,7 @@ describe("PingUploader", function() {
157166
});
158167

159168
it("duplicates are not enqueued", function() {
160-
const uploader = new PingUploader(Glean.platform.uploader);
169+
const uploader = new PingUploader(new Configuration(), Glean.platform, Glean.pingsDatabase);
161170
for (let i = 0; i < 10; i++) {
162171
uploader["enqueuePing"]({
163172
identifier: "id",

0 commit comments

Comments
 (0)