@@ -7,10 +7,33 @@ import type { JSONObject} from "../utils.js";
77import { isObject , isJSONValue , isString } from "../utils.js" ;
88import type { StorageBuilder } from "../../platform/index.js" ;
99import log , { LoggingLevel } from "../log.js" ;
10+ import { DELETION_REQUEST_PING_NAME } from "../constants.js" ;
11+ import { strToU8 } from "fflate" ;
1012
1113const LOG_TAG = "core.Pings.Database" ;
1214
15+ /**
16+ * Whether or not a given ping is a deletion-request ping.
17+ *
18+ * @param ping The ping to verify.
19+ * @returns Whether or not the ping is a deletion-request ping.
20+ */
21+ export function isDeletionRequest ( ping : PingInternalRepresentation ) : boolean {
22+ return ping . path . split ( "/" ) [ 3 ] === DELETION_REQUEST_PING_NAME ;
23+ }
24+
25+ /**
26+ * Gets the size of a ping in bytes.
27+ *
28+ * @param ping The ping to get the size of.
29+ * @returns Size of the given ping in bytes.
30+ */
31+ function getPingSize ( ping : PingInternalRepresentation ) : number {
32+ return strToU8 ( JSON . stringify ( ping ) ) . length ;
33+ }
34+
1335export interface PingInternalRepresentation extends JSONObject {
36+ collectionDate : string ,
1437 path : string ,
1538 payload : JSONObject ,
1639 headers ?: Record < string , string >
@@ -95,6 +118,7 @@ class PingsDatabase {
95118 headers ?: Record < string , string >
96119 ) : Promise < void > {
97120 const ping : PingInternalRepresentation = {
121+ collectionDate : ( new Date ( ) ) . toISOString ( ) ,
98122 path,
99123 payload
100124 } ;
@@ -119,11 +143,16 @@ class PingsDatabase {
119143 }
120144
121145 /**
122- * Gets all pings from the pings database. Deletes any data in unexpected format that is found.
146+ * Gets all pings from the pings database.
147+ * Deletes any data in unexpected format that is found.
148+ *
149+ * # Note
150+ *
151+ * The return value of this function can be turned into an object using Object.fromEntries.
123152 *
124- * @returns List of all currently stored pings.
153+ * @returns List of all currently stored pings in ascending order by date .
125154 */
126- async getAllPings ( ) : Promise < { [ id : string ] : PingInternalRepresentation } > {
155+ async getAllPings ( ) : Promise < [ string , PingInternalRepresentation ] [ ] > {
127156 const allStoredPings = await this . store . _getWholeStore ( ) ;
128157 const finalPings : { [ ident : string ] : PingInternalRepresentation } = { } ;
129158 for ( const identifier in allStoredPings ) {
@@ -136,22 +165,117 @@ class PingsDatabase {
136165 }
137166 }
138167
139- return finalPings ;
168+ return Object . entries ( finalPings )
169+ . sort ( ( [ _idA , { collectionDate : dateA } ] , [ _idB , { collectionDate : dateB } ] ) : number => {
170+ const timeA = ( new Date ( dateA ) ) . getTime ( ) ;
171+ const timeB = ( new Date ( dateB ) ) . getTime ( ) ;
172+ return timeA - timeB ;
173+ } ) ;
174+ }
175+
176+ /**
177+ * Delete surplus of pings in the database by count or database size
178+ * and return list of remaining pings. Pings are deleted from oldest to newest.
179+ *
180+ * The size of the database will be calculated
181+ * (by accumulating each ping's size in bytes)
182+ * and in case the quota is exceeded, outstanding pings get deleted.
183+ *
184+ * Note: `deletion-request` pings are never deleted.
185+ *
186+ * @param maxCount The max number of pings in the database. Default: 250.
187+ * @param maxSize The max size of the database (in bytes). Default: 10MB.
188+ * @returns List of all currently stored pings, in ascending order by date.
189+ * `deletion-request` pings are always in the front of the list.
190+ */
191+ private async getAllPingsWithoutSurplus (
192+ maxCount = 250 ,
193+ maxSize = 10 * 1024 * 1024 , // 10MB
194+ ) : Promise < [ string , PingInternalRepresentation ] [ ] > {
195+ const allPings = await this . getAllPings ( ) ;
196+ // Separate deletion-request from other pings.
197+ const pings = allPings
198+ . filter ( ( [ _ , ping ] ) => ! isDeletionRequest ( ping ) )
199+ // We need to calculate the size of the pending pings database
200+ // and delete the **oldest** pings in case quota is reached.
201+ // So, we sort them in descending order (newest -> oldest).
202+ . reverse ( ) ;
203+ const deletionRequestPings = allPings . filter ( ( [ _ , ping ] ) => isDeletionRequest ( ping ) ) ;
204+
205+ const total = pings . length ;
206+ // TODO (bug 1722682): Record `glean.pending_pings` metric.
207+ if ( total > maxCount ) {
208+ log (
209+ LOG_TAG ,
210+ [
211+ `More than ${ maxCount } pending pings in the pings database,` ,
212+ `will delete ${ total - maxCount } old pings.`
213+ ] ,
214+ LoggingLevel . Warn
215+ ) ;
216+ }
217+
218+ let deleting = false ;
219+ let pendingPingsCount = 0 ;
220+ let pendingPingsDatabaseSize = 0 ;
221+ const remainingPings : [ string , PingInternalRepresentation ] [ ] = [ ] ;
222+ for ( const [ identifier , ping ] of pings ) {
223+ pendingPingsCount ++ ;
224+ pendingPingsDatabaseSize += getPingSize ( ping ) ;
225+
226+ if ( ! deleting && pendingPingsDatabaseSize > maxSize ) {
227+ log (
228+ LOG_TAG ,
229+ [
230+ `Pending pings database has reached the size quota of ${ maxSize } bytes,` ,
231+ "outstanding pings will be deleted."
232+ ] ,
233+ LoggingLevel . Warn
234+ ) ;
235+ deleting = true ;
236+ }
237+
238+ // Once we reach the number of allowed pings we start deleting,
239+ // no matter what size. We already log this before the loop.
240+ if ( pendingPingsCount > maxCount ) {
241+ deleting = true ;
242+ }
243+
244+ if ( deleting ) {
245+ // Delete ping from database.
246+ await this . deletePing ( identifier ) ;
247+
248+ // TODO (bug 1722685): Record `deleted_pings_after_quota_hit` metric.
249+ } else {
250+ // Add pings in reverse order so the final array is in ascending order again.
251+ remainingPings . unshift ( [ identifier , ping ] ) ;
252+ }
253+ }
254+
255+ // TODO(bug 1722686): Record `pending_pings_directory_size` metric.
256+
257+ // Return pings to original order.
258+ return [ ...deletionRequestPings , ...remainingPings ] ;
140259 }
141260
142261 /**
143262 * Scans the database for pending pings and enqueues them.
263+ *
264+ * # Important
265+ *
266+ * This function will also clear off pings in case
267+ * the database is exceeding the ping or size quota.
144268 */
145269 async scanPendingPings ( ) : Promise < void > {
146270 // If there's no observer, then there's no point in iterating.
147271 if ( ! this . observer ) {
148272 return ;
149273 }
150274
151- const pings = await this . getAllPings ( ) ;
152- for ( const identifier in pings ) {
275+ const pings = await this . getAllPingsWithoutSurplus ( ) ;
276+ for ( const [ identifier , ping ] of pings ) {
153277 // Notify the observer that a new ping has been added to the pings database.
154- this . observer . update ( identifier , pings [ identifier ] ) ;
278+ this . observer . update ( identifier , ping ) ;
155279 }
156280 }
157281
0 commit comments