327 lines
11 KiB
JavaScript
327 lines
11 KiB
JavaScript
/**
|
|
* Any copyright is dedicated to the Public Domain.
|
|
* http://creativecommons.org/publicdomain/zero/1.0/
|
|
*/
|
|
|
|
const { PrefUtils } = ChromeUtils.importESModule(
|
|
"resource://testing-common/dom/quota/test/modules/PrefUtils.sys.mjs"
|
|
);
|
|
const { PrincipalUtils } = ChromeUtils.importESModule(
|
|
"resource://testing-common/dom/quota/test/modules/PrincipalUtils.sys.mjs"
|
|
);
|
|
const { QuotaUtils } = ChromeUtils.importESModule(
|
|
"resource://testing-common/dom/quota/test/modules/QuotaUtils.sys.mjs"
|
|
);
|
|
const { SimpleDBUtils } = ChromeUtils.importESModule(
|
|
"resource://testing-common/dom/simpledb/test/modules/SimpleDBUtils.sys.mjs"
|
|
);
|
|
|
|
// This value is used to set dom.quotaManager.temporaryStorage.fixedLimit
|
|
// for this test, and must match the needs of the writes we plan to do.
|
|
// The storage size must be a multiple of (number of origins - 1) to ensure
|
|
// `dataSize` is a whole number. This is enforced below with a check to
|
|
// guarantee predictable writes.
|
|
const storageSizeKB = 32;
|
|
|
|
/**
|
|
* This test simulates origin usage across five related stages, exercising:
|
|
* - Access time tracking on first and last access to each origin
|
|
* - Eviction logic based on activity and last access time
|
|
* - Usage reflection via QuotaManager's reporting APIs
|
|
*
|
|
* The test is data-driven: it defines a list of origins, each with a flag
|
|
* array representing whether data on disk is expected to exist after each
|
|
* stage. These flags are used to verify origin usage deterministically at each
|
|
* point.
|
|
*
|
|
* The total storage size is evenly divided among all origins except the last
|
|
* one. This ensures predictable write outcomes:
|
|
* - All but the last origin succeed in writing initially
|
|
* - The last origin exceeds quota and triggers eviction conditions
|
|
*
|
|
* Each stage simulates realistic temporary storage (AKA best-effort) behavior:
|
|
* Stage 1 - Initializes all origin directories in reverse to test access time
|
|
* updates
|
|
* Stage 2 - Opens connections and fills storage, leaving no room for the last
|
|
* origin
|
|
* Stage 3 - Closes most connections to allow eviction of inactive origins
|
|
* Stage 4 - Shrinks temporary storage by 50%, triggering additional evictions
|
|
* Stage 5 - Writes again to the last origin to validate ongoing eviction
|
|
* behavior
|
|
*
|
|
* This test ensures correctness and robustness of temporary storage handling,
|
|
* especially around eviction and access time policies.
|
|
*/
|
|
async function testTemporaryStorageEviction() {
|
|
const storageSize = storageSizeKB * 1024;
|
|
|
|
// flags: [stage1, stage2, stage3, stage4, stage5]
|
|
// 1 = data on disk should exist, 0 = data on disk should not exist
|
|
|
|
/* prettier-ignore */
|
|
const originInfos = [
|
|
{ url: "https://www.alpha.com", flags: [0, 1, 1, 1, 1] },
|
|
{ url: "https://www.beta.com", flags: [0, 1, 0, 0, 0] },
|
|
{ url: "https://www.gamma.com", flags: [0, 1, 1, 0, 0] },
|
|
{ url: "https://www.delta.com", flags: [0, 1, 1, 0, 0] },
|
|
{ url: "https://www.epsilon.com", flags: [0, 1, 1, 0, 0] },
|
|
{ url: "https://www2.alpha.com", flags: [0, 1, 1, 0, 0] },
|
|
{ url: "https://www2.beta.com", flags: [0, 1, 1, 0, 0] },
|
|
{ url: "https://www2.gamma.com", flags: [0, 1, 1, 0, 0] },
|
|
{ url: "https://www2.delta.com", flags: [0, 1, 1, 0, 0] },
|
|
{ url: "https://www2.epsilon.com",flags: [0, 1, 1, 0, 0] },
|
|
{ url: "https://www3.alpha.com", flags: [0, 1, 1, 0, 0] },
|
|
{ url: "https://www3.beta.com", flags: [0, 1, 1, 0, 0] },
|
|
{ url: "https://www3.gamma.com", flags: [0, 1, 1, 0, 0] },
|
|
{ url: "https://www3.delta.com", flags: [0, 1, 1, 0, 0] },
|
|
{ url: "https://www3.epsilon.com",flags: [0, 1, 1, 0, 0] },
|
|
{ url: "https://www.alpha.org", flags: [0, 1, 1, 0, 0] },
|
|
{ url: "https://www.beta.org", flags: [0, 1, 1, 0, 0] },
|
|
{ url: "https://www.gamma.org", flags: [0, 1, 1, 0, 0] },
|
|
{ url: "https://www.delta.org", flags: [0, 1, 1, 1, 0] },
|
|
{ url: "https://www.epsilon.org", flags: [0, 1, 1, 1, 1] },
|
|
{ url: "https://www.zeta.org", flags: [0, 1, 1, 1, 1] },
|
|
{ url: "https://www.eta.org", flags: [0, 1, 1, 1, 1] },
|
|
{ url: "https://www.theta.org", flags: [0, 1, 1, 1, 1] },
|
|
{ url: "https://www.iota.org", flags: [0, 1, 1, 1, 1] },
|
|
{ url: "https://www.kappa.org", flags: [0, 1, 1, 1, 1] },
|
|
{ url: "https://www.lambda.org", flags: [0, 1, 1, 1, 1] },
|
|
{ url: "https://www.mu.org", flags: [0, 1, 1, 1, 1] },
|
|
{ url: "https://www.nu.org", flags: [0, 1, 1, 1, 1] },
|
|
{ url: "https://www.xi.org", flags: [0, 1, 1, 1, 1] },
|
|
{ url: "https://www.omicron.org", flags: [0, 1, 1, 1, 1] },
|
|
{ url: "https://www.pi.org", flags: [0, 1, 1, 1, 1] },
|
|
{ url: "https://www.rho.org", flags: [0, 1, 1, 1, 1] },
|
|
{ url: "https://www.omega.org", flags: [0, 0, 1, 1, 1] },
|
|
];
|
|
Assert.equal(
|
|
storageSize % (originInfos.length - 1),
|
|
0,
|
|
"Correct storage size"
|
|
);
|
|
|
|
const name = "test_temporaryStorageEviction";
|
|
|
|
const dataSize = storageSize / (originInfos.length - 1);
|
|
const dataBuffer = new ArrayBuffer(dataSize);
|
|
|
|
async function checkUsage(stage) {
|
|
for (const originInfo of originInfos) {
|
|
const url = originInfo.url;
|
|
|
|
info(`Checking usage for ${url}`);
|
|
|
|
const principal = PrincipalUtils.createPrincipal(url);
|
|
|
|
const request = Services.qms.getUsageForPrincipal(principal, {});
|
|
const usageResult = await QuotaUtils.requestFinished(request);
|
|
|
|
if (originInfo.flags[stage - 1]) {
|
|
Assert.greater(usageResult.usage, 0, "Correct usage");
|
|
} else {
|
|
Assert.equal(usageResult.usage, 0, "Correct usage");
|
|
}
|
|
}
|
|
}
|
|
|
|
async function createAndOpenConnection(url) {
|
|
const principal = PrincipalUtils.createPrincipal(url);
|
|
|
|
const connection = SimpleDBUtils.createConnection(principal);
|
|
|
|
const openRequest = connection.open(name);
|
|
await SimpleDBUtils.requestFinished(openRequest);
|
|
|
|
return connection;
|
|
}
|
|
|
|
info(
|
|
"Stage 1: Reverse creation of origins to test first/last access time updates"
|
|
);
|
|
|
|
// Initializes storage and temporary storage and creates all origin
|
|
// directories with metadata, in reverse order. This ensures that the
|
|
// "first access" and "last access" logic for updating origin access time is
|
|
// properly exercised in other stages.
|
|
|
|
info("Initializing storage");
|
|
|
|
{
|
|
const request = Services.qms.init();
|
|
await QuotaUtils.requestFinished(request);
|
|
}
|
|
|
|
info("Initializing temporary storage");
|
|
|
|
{
|
|
const request = Services.qms.initTemporaryStorage();
|
|
await QuotaUtils.requestFinished(request);
|
|
}
|
|
|
|
info("Initializing temporary origins");
|
|
|
|
for (const originInfo of originInfos.toReversed()) {
|
|
const principal = PrincipalUtils.createPrincipal(originInfo.url);
|
|
|
|
const request = Services.qms.initializeTemporaryOrigin(
|
|
"default",
|
|
principal,
|
|
/* aCreateIfNonExistent */ true
|
|
);
|
|
await QuotaUtils.requestFinished(request);
|
|
|
|
// Wait 40ms to ensure the next origin gets a different access time. Some
|
|
// systems have low timer resolution, so this adds a safe buffer.
|
|
await new Promise(function (resolve) {
|
|
do_timeout(40, resolve);
|
|
});
|
|
}
|
|
|
|
info("Checking usage");
|
|
|
|
await checkUsage(/* stage */ 1);
|
|
|
|
info(
|
|
"Stage 2: All origins active; eviction not possible, last write should fail"
|
|
);
|
|
|
|
// Opens connections for all origins and writes data to each except the last
|
|
// one. Since all origins remain active (open connections), none can be
|
|
// evicted, even if storage runs out. This tests that eviction logic respects
|
|
// activity status.
|
|
|
|
const connections = await (async function () {
|
|
let connections = [];
|
|
// Stage 1
|
|
for (const originInfo of originInfos) {
|
|
const connection = await createAndOpenConnection(originInfo.url);
|
|
|
|
connections.push(connection);
|
|
}
|
|
|
|
return connections;
|
|
})();
|
|
|
|
// Write to all but the last origin.
|
|
for (const connection of connections.slice(0, -1)) {
|
|
const writeRequest = connection.write(dataBuffer);
|
|
await SimpleDBUtils.requestFinished(writeRequest);
|
|
}
|
|
|
|
// Try to write to the last origin.
|
|
{
|
|
const writeRequest = connections.at(-1).write(dataBuffer);
|
|
try {
|
|
await SimpleDBUtils.requestFinished(writeRequest);
|
|
Assert.ok(false, "Should have thrown");
|
|
} catch (e) {
|
|
Assert.ok(true, "Should have thrown");
|
|
Assert.strictEqual(
|
|
e.resultCode,
|
|
NS_ERROR_FILE_NO_DEVICE_SPACE,
|
|
"Threw right result code"
|
|
);
|
|
}
|
|
}
|
|
|
|
await checkUsage(/* stage */ 2);
|
|
|
|
info("Stage 3: Inactive origins can be evicted; last origin writes again");
|
|
|
|
// Closes all connections except the first and last origin. This leaves most
|
|
// origins inactive, making them eligible for eviction. The last origin
|
|
// writes data again, which should now succeed because there is at least one
|
|
// inactive origin that can be evicted to make space.
|
|
|
|
// Close all connections except the first and the last
|
|
for (const connection of connections.slice(1, -1)) {
|
|
const closeRequest = connection.close();
|
|
await SimpleDBUtils.requestFinished(closeRequest);
|
|
|
|
// Wait 40ms to ensure the next origin gets a different access time. Some
|
|
// systems have low timer resolution, so this adds a safe buffer.
|
|
await new Promise(function (resolve) {
|
|
do_timeout(40, resolve);
|
|
});
|
|
}
|
|
|
|
// Write to the last origin.
|
|
{
|
|
const writeRequest = connections.at(-1).write(dataBuffer);
|
|
await SimpleDBUtils.requestFinished(writeRequest);
|
|
}
|
|
|
|
await checkUsage(/* stage */ 3);
|
|
|
|
info("Stage 4: Shrink quota by 50%; evict origins by last access time");
|
|
|
|
// Shrinks the temporary storage quota by 50%. This triggers eviction of
|
|
// approximately half of the origins based on their last access time. It
|
|
// tests that quota reduction correctly respects access time ordering when
|
|
// deciding which origins to evict.
|
|
|
|
info("Shutting down storage");
|
|
|
|
{
|
|
const request = Services.qms.reset();
|
|
await QuotaUtils.requestFinished(request);
|
|
}
|
|
|
|
info("Setting preferences");
|
|
|
|
{
|
|
const prefs = [
|
|
["dom.quotaManager.temporaryStorage.fixedLimit", storageSizeKB / 2],
|
|
];
|
|
|
|
PrefUtils.setPrefs(prefs);
|
|
}
|
|
|
|
info("Initializing storage");
|
|
|
|
{
|
|
const request = Services.qms.init();
|
|
await QuotaUtils.requestFinished(request);
|
|
}
|
|
|
|
info("Initializing temporary storage");
|
|
|
|
{
|
|
const request = Services.qms.initTemporaryStorage();
|
|
await QuotaUtils.requestFinished(request);
|
|
}
|
|
|
|
await checkUsage(/* stage */ 4);
|
|
|
|
info("Stage 5: Last origin writes more; one more origin should be evicted");
|
|
|
|
// The last origin writes additional data, which should exceed the current
|
|
// quota again. This triggers eviction of one more inactive origin,
|
|
// validating that eviction continues to respect quota limits and frees up
|
|
// space as needed.
|
|
|
|
{
|
|
const connection = await createAndOpenConnection(originInfos.at(-1).url);
|
|
|
|
const seekRequest = connection.seek(dataSize);
|
|
await SimpleDBUtils.requestFinished(seekRequest);
|
|
|
|
const writeRequest = connection.write(dataBuffer);
|
|
await SimpleDBUtils.requestFinished(writeRequest);
|
|
}
|
|
|
|
await checkUsage(/* stage */ 5);
|
|
}
|
|
|
|
async function testSteps() {
|
|
add_task(
|
|
{
|
|
pref_set: [
|
|
["dom.quotaManager.loadQuotaFromCache", false],
|
|
["dom.quotaManager.temporaryStorage.fixedLimit", storageSizeKB],
|
|
["dom.quotaManager.temporaryStorage.updateOriginAccessTime", true],
|
|
],
|
|
},
|
|
testTemporaryStorageEviction
|
|
);
|
|
}
|