test_temporaryStorageEviction.js (11503B)
1 /** 2 * Any copyright is dedicated to the Public Domain. 3 * http://creativecommons.org/publicdomain/zero/1.0/ 4 */ 5 6 const { PrefUtils } = ChromeUtils.importESModule( 7 "resource://testing-common/dom/quota/test/modules/PrefUtils.sys.mjs" 8 ); 9 const { PrincipalUtils } = ChromeUtils.importESModule( 10 "resource://testing-common/dom/quota/test/modules/PrincipalUtils.sys.mjs" 11 ); 12 const { QuotaUtils } = ChromeUtils.importESModule( 13 "resource://testing-common/dom/quota/test/modules/QuotaUtils.sys.mjs" 14 ); 15 const { SimpleDBUtils } = ChromeUtils.importESModule( 16 "resource://testing-common/dom/simpledb/test/modules/SimpleDBUtils.sys.mjs" 17 ); 18 19 // This value is used to set dom.quotaManager.temporaryStorage.fixedLimit 20 // for this test, and must match the needs of the writes we plan to do. 21 // The storage size must be a multiple of (number of origins - 1) to ensure 22 // `dataSize` is a whole number. This is enforced below with a check to 23 // guarantee predictable writes. 24 const storageSizeKB = 32; 25 26 /** 27 * This test simulates origin usage across five related stages, exercising: 28 * - Access time tracking on first and last access to each origin 29 * - Eviction logic based on activity and last access time 30 * - Usage reflection via QuotaManager's reporting APIs 31 * 32 * The test is data-driven: it defines a list of origins, each with a flag 33 * array representing whether data on disk is expected to exist after each 34 * stage. These flags are used to verify origin usage deterministically at each 35 * point. 36 * 37 * The total storage size is evenly divided among all origins except the last 38 * one. This ensures predictable write outcomes: 39 * - All but the last origin succeed in writing initially 40 * - The last origin exceeds quota and triggers eviction conditions 41 * 42 * Each stage simulates realistic temporary storage (AKA best-effort) behavior: 43 * Stage 1 - Initializes all origin directories in reverse to test access time 44 * updates 45 * Stage 2 - Opens connections and fills storage, leaving no room for the last 46 * origin 47 * Stage 3 - Closes most connections to allow eviction of inactive origins 48 * Stage 4 - Shrinks temporary storage by 50%, triggering additional evictions 49 * Stage 5 - Writes again to the last origin to validate ongoing eviction 50 * behavior 51 * 52 * This test ensures correctness and robustness of temporary storage handling, 53 * especially around eviction and access time policies. 54 */ 55 async function testTemporaryStorageEviction() { 56 const storageSize = storageSizeKB * 1024; 57 58 // flags: [stage1, stage2, stage3, stage4, stage5] 59 // 1 = data on disk should exist, 0 = data on disk should not exist 60 61 /* prettier-ignore */ 62 const originInfos = [ 63 { url: "https://www.alpha.com", flags: [0, 1, 1, 1, 1] }, 64 { url: "https://www.beta.com", flags: [0, 1, 0, 0, 0] }, 65 { url: "https://www.gamma.com", flags: [0, 1, 1, 0, 0] }, 66 { url: "https://www.delta.com", flags: [0, 1, 1, 0, 0] }, 67 { url: "https://www.epsilon.com", flags: [0, 1, 1, 0, 0] }, 68 { url: "https://www2.alpha.com", flags: [0, 1, 1, 0, 0] }, 69 { url: "https://www2.beta.com", flags: [0, 1, 1, 0, 0] }, 70 { url: "https://www2.gamma.com", flags: [0, 1, 1, 0, 0] }, 71 { url: "https://www2.delta.com", flags: [0, 1, 1, 0, 0] }, 72 { url: "https://www2.epsilon.com",flags: [0, 1, 1, 0, 0] }, 73 { url: "https://www3.alpha.com", flags: [0, 1, 1, 0, 0] }, 74 { url: "https://www3.beta.com", flags: [0, 1, 1, 0, 0] }, 75 { url: "https://www3.gamma.com", flags: [0, 1, 1, 0, 0] }, 76 { url: "https://www3.delta.com", flags: [0, 1, 1, 0, 0] }, 77 { url: "https://www3.epsilon.com",flags: [0, 1, 1, 0, 0] }, 78 { url: "https://www.alpha.org", flags: [0, 1, 1, 0, 0] }, 79 { url: "https://www.beta.org", flags: [0, 1, 1, 0, 0] }, 80 { url: "https://www.gamma.org", flags: [0, 1, 1, 0, 0] }, 81 { url: "https://www.delta.org", flags: [0, 1, 1, 1, 0] }, 82 { url: "https://www.epsilon.org", flags: [0, 1, 1, 1, 1] }, 83 { url: "https://www.zeta.org", flags: [0, 1, 1, 1, 1] }, 84 { url: "https://www.eta.org", flags: [0, 1, 1, 1, 1] }, 85 { url: "https://www.theta.org", flags: [0, 1, 1, 1, 1] }, 86 { url: "https://www.iota.org", flags: [0, 1, 1, 1, 1] }, 87 { url: "https://www.kappa.org", flags: [0, 1, 1, 1, 1] }, 88 { url: "https://www.lambda.org", flags: [0, 1, 1, 1, 1] }, 89 { url: "https://www.mu.org", flags: [0, 1, 1, 1, 1] }, 90 { url: "https://www.nu.org", flags: [0, 1, 1, 1, 1] }, 91 { url: "https://www.xi.org", flags: [0, 1, 1, 1, 1] }, 92 { url: "https://www.omicron.org", flags: [0, 1, 1, 1, 1] }, 93 { url: "https://www.pi.org", flags: [0, 1, 1, 1, 1] }, 94 { url: "https://www.rho.org", flags: [0, 1, 1, 1, 1] }, 95 { url: "https://www.omega.org", flags: [0, 0, 1, 1, 1] }, 96 ]; 97 Assert.equal( 98 storageSize % (originInfos.length - 1), 99 0, 100 "Correct storage size" 101 ); 102 103 const name = "test_temporaryStorageEviction"; 104 105 const dataSize = storageSize / (originInfos.length - 1); 106 const dataBuffer = new ArrayBuffer(dataSize); 107 108 async function checkUsage(stage) { 109 for (const originInfo of originInfos) { 110 const url = originInfo.url; 111 112 info(`Checking usage for ${url}`); 113 114 const principal = PrincipalUtils.createPrincipal(url); 115 116 const request = Services.qms.getUsageForPrincipal(principal, {}); 117 const usageResult = await QuotaUtils.requestFinished(request); 118 119 if (originInfo.flags[stage - 1]) { 120 Assert.greater(usageResult.usage, 0, "Correct usage"); 121 } else { 122 Assert.equal(usageResult.usage, 0, "Correct usage"); 123 } 124 } 125 } 126 127 async function createAndOpenConnection(url) { 128 const principal = PrincipalUtils.createPrincipal(url); 129 130 const connection = SimpleDBUtils.createConnection(principal); 131 132 const openRequest = connection.open(name); 133 await SimpleDBUtils.requestFinished(openRequest); 134 135 return connection; 136 } 137 138 info( 139 "Stage 1: Reverse creation of origins to test first/last access time updates" 140 ); 141 142 // Initializes storage and temporary storage and creates all origin 143 // directories with metadata, in reverse order. This ensures that the 144 // "first access" and "last access" logic for updating origin access time is 145 // properly exercised in other stages. 146 147 info("Initializing storage"); 148 149 { 150 const request = Services.qms.init(); 151 await QuotaUtils.requestFinished(request); 152 } 153 154 info("Initializing temporary storage"); 155 156 { 157 const request = Services.qms.initTemporaryStorage(); 158 await QuotaUtils.requestFinished(request); 159 } 160 161 info("Initializing temporary origins"); 162 163 for (const originInfo of originInfos.toReversed()) { 164 const principal = PrincipalUtils.createPrincipal(originInfo.url); 165 166 const request = Services.qms.initializeTemporaryOrigin( 167 "default", 168 principal, 169 /* aCreateIfNonExistent */ true 170 ); 171 await QuotaUtils.requestFinished(request); 172 173 // Wait 40ms to ensure the next origin gets a different access time. Some 174 // systems have low timer resolution, so this adds a safe buffer. 175 await new Promise(function (resolve) { 176 do_timeout(40, resolve); 177 }); 178 } 179 180 info("Checking usage"); 181 182 await checkUsage(/* stage */ 1); 183 184 info( 185 "Stage 2: All origins active; eviction not possible, last write should fail" 186 ); 187 188 // Opens connections for all origins and writes data to each except the last 189 // one. Since all origins remain active (open connections), none can be 190 // evicted, even if storage runs out. This tests that eviction logic respects 191 // activity status. 192 193 const connections = await (async function () { 194 let connections = []; 195 // Stage 1 196 for (const originInfo of originInfos) { 197 const connection = await createAndOpenConnection(originInfo.url); 198 199 connections.push(connection); 200 } 201 202 return connections; 203 })(); 204 205 // Write to all but the last origin. 206 for (const connection of connections.slice(0, -1)) { 207 const writeRequest = connection.write(dataBuffer); 208 await SimpleDBUtils.requestFinished(writeRequest); 209 } 210 211 // Try to write to the last origin. 212 { 213 const writeRequest = connections.at(-1).write(dataBuffer); 214 try { 215 await SimpleDBUtils.requestFinished(writeRequest); 216 Assert.ok(false, "Should have thrown"); 217 } catch (e) { 218 Assert.ok(true, "Should have thrown"); 219 Assert.strictEqual( 220 e.resultCode, 221 NS_ERROR_FILE_NO_DEVICE_SPACE, 222 "Threw right result code" 223 ); 224 } 225 } 226 227 await checkUsage(/* stage */ 2); 228 229 info("Stage 3: Inactive origins can be evicted; last origin writes again"); 230 231 // Closes all connections except the first and last origin. This leaves most 232 // origins inactive, making them eligible for eviction. The last origin 233 // writes data again, which should now succeed because there is at least one 234 // inactive origin that can be evicted to make space. 235 236 // Close all connections except the first and the last 237 for (const connection of connections.slice(1, -1)) { 238 const closeRequest = connection.close(); 239 await SimpleDBUtils.requestFinished(closeRequest); 240 241 // Wait 40ms to ensure the next origin gets a different access time. Some 242 // systems have low timer resolution, so this adds a safe buffer. 243 await new Promise(function (resolve) { 244 do_timeout(40, resolve); 245 }); 246 } 247 248 // Write to the last origin. 249 { 250 const writeRequest = connections.at(-1).write(dataBuffer); 251 await SimpleDBUtils.requestFinished(writeRequest); 252 } 253 254 await checkUsage(/* stage */ 3); 255 256 info("Stage 4: Shrink quota by 50%; evict origins by last access time"); 257 258 // Shrinks the temporary storage quota by 50%. This triggers eviction of 259 // approximately half of the origins based on their last access time. It 260 // tests that quota reduction correctly respects access time ordering when 261 // deciding which origins to evict. 262 263 info("Shutting down storage"); 264 265 { 266 const request = Services.qms.reset(); 267 await QuotaUtils.requestFinished(request); 268 } 269 270 info("Setting preferences"); 271 272 { 273 const prefs = [ 274 ["dom.quotaManager.temporaryStorage.fixedLimit", storageSizeKB / 2], 275 ]; 276 277 PrefUtils.setPrefs(prefs); 278 } 279 280 info("Initializing storage"); 281 282 { 283 const request = Services.qms.init(); 284 await QuotaUtils.requestFinished(request); 285 } 286 287 info("Initializing temporary storage"); 288 289 { 290 const request = Services.qms.initTemporaryStorage(); 291 await QuotaUtils.requestFinished(request); 292 } 293 294 await checkUsage(/* stage */ 4); 295 296 info("Stage 5: Last origin writes more; one more origin should be evicted"); 297 298 // The last origin writes additional data, which should exceed the current 299 // quota again. This triggers eviction of one more inactive origin, 300 // validating that eviction continues to respect quota limits and frees up 301 // space as needed. 302 303 { 304 const connection = await createAndOpenConnection(originInfos.at(-1).url); 305 306 const seekRequest = connection.seek(dataSize); 307 await SimpleDBUtils.requestFinished(seekRequest); 308 309 const writeRequest = connection.write(dataBuffer); 310 await SimpleDBUtils.requestFinished(writeRequest); 311 } 312 313 await checkUsage(/* stage */ 5); 314 } 315 316 async function testSteps() { 317 add_task( 318 { 319 pref_set: [ 320 ["dom.quotaManager.loadQuotaFromCache", false], 321 ["dom.quotaManager.temporaryStorage.fixedLimit", storageSizeKB], 322 ["dom.quotaManager.temporaryStorage.updateOriginAccessTime", true], 323 ], 324 }, 325 testTemporaryStorageEviction 326 ); 327 }