mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-07 19:22:14 +09:00
489 lines
19 KiB
TypeScript
489 lines
19 KiB
TypeScript
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
import {spawnSync} from 'node:child_process';
|
|
import {createServer} from 'node:net';
|
|
import {defineTable} from '@app/api/database/CassandraTableDsl';
|
|
import {Db} from '@app/api/database/CassandraTypes';
|
|
import {
|
|
DEFAULT_TTL_EXPIRY_RESUME,
|
|
DEFAULT_TTL_TABLES,
|
|
expireLegacyDefaultTtlRows,
|
|
} from '@app/api/database/PostgresKvDefaultTtlExpiry';
|
|
import {
|
|
ensurePostgresKvSchema,
|
|
PostgresKvQueryExecutor,
|
|
pruneExpiredPostgresKvRows,
|
|
} from '@app/api/database/PostgresKvQueryExecutor';
|
|
import {startDockerContainer} from '@app/api/test/DockerTestContainer';
|
|
import {
|
|
getDefaultPostgresClient,
|
|
type IPostgresClient,
|
|
initPostgres,
|
|
shutdownPostgres,
|
|
} from '@pkgs/postgres/src/Client';
|
|
import {afterAll, beforeAll, beforeEach, describe, expect, it} from 'vitest';
|
|
|
|
const KV_TABLE = 'kv_default_ttl';
|
|
const CONTAINER = `fluxer-kvttl-${process.pid.toString(36)}-${Date.now().toString(36)}`;
|
|
const dockerAvailable = spawnSync('docker', ['version'], {stdio: 'ignore'}).status === 0;
|
|
const DEFAULT_TTL_SECONDS = 600;
|
|
|
|
interface ProbeRow {
|
|
id: string;
|
|
value: string | null;
|
|
note: string | null;
|
|
}
|
|
|
|
interface OwnedProbeRow {
|
|
owner: string;
|
|
id: string;
|
|
value: string | null;
|
|
}
|
|
|
|
const DefaultTtlProbe = defineTable<ProbeRow, 'id'>({
|
|
name: 'default_ttl_probe',
|
|
columns: ['id', 'value', 'note'],
|
|
primaryKey: ['id'],
|
|
defaultTtlSeconds: DEFAULT_TTL_SECONDS,
|
|
});
|
|
|
|
const DefaultTtlProbeRows = defineTable<OwnedProbeRow, 'owner' | 'id', 'owner'>({
|
|
name: 'default_ttl_probe_rows',
|
|
columns: ['owner', 'id', 'value'],
|
|
primaryKey: ['owner', 'id'],
|
|
partitionKey: ['owner'],
|
|
defaultTtlSeconds: DEFAULT_TTL_SECONDS,
|
|
});
|
|
|
|
const NoTtlProbe = defineTable<ProbeRow, 'id'>({
|
|
name: 'no_ttl_probe',
|
|
columns: ['id', 'value', 'note'],
|
|
primaryKey: ['id'],
|
|
});
|
|
|
|
async function sleep(ms: number): Promise<void> {
|
|
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
}
|
|
|
|
async function freePort(): Promise<number> {
|
|
return new Promise((resolve, reject) => {
|
|
const server = createServer();
|
|
server.on('error', reject);
|
|
server.listen(0, '127.0.0.1', () => {
|
|
const address = server.address();
|
|
if (typeof address === 'string' || address === null) {
|
|
reject(new Error('no port'));
|
|
return;
|
|
}
|
|
const port = address.port;
|
|
server.close(() => resolve(port));
|
|
});
|
|
});
|
|
}
|
|
|
|
function expectExpiresIn(value: Date | number | null, ttlSeconds: number): void {
|
|
expect(value).toBeInstanceOf(Date);
|
|
const remainingSeconds = ((value as Date).getTime() - Date.now()) / 1000;
|
|
expect(remainingSeconds).toBeGreaterThan(ttlSeconds - 60);
|
|
expect(remainingSeconds).toBeLessThanOrEqual(ttlSeconds);
|
|
}
|
|
|
|
describe.skipIf(!dockerAvailable)('Postgres KV default TTL', () => {
|
|
let raw: IPostgresClient;
|
|
let executor: PostgresKvQueryExecutor;
|
|
|
|
async function stored(table: string, id: string): Promise<{expires_at: Date | number | null; row_data: object}> {
|
|
const result = await raw.query<{expires_at: Date | number | null; row_data: object}>(
|
|
`SELECT expires_at, row_data FROM ${KV_TABLE} WHERE table_name = $1 AND row_data ->> 'id' = $2`,
|
|
[table, id],
|
|
);
|
|
expect(result.rows).toHaveLength(1);
|
|
return result.rows[0]!;
|
|
}
|
|
|
|
async function expiresAt(table: string, id: string): Promise<Date | number | null> {
|
|
return (await stored(table, id)).expires_at;
|
|
}
|
|
|
|
async function neverExpires(table: string, id: string): Promise<boolean> {
|
|
const result = await raw.query<{forever: boolean}>(
|
|
`SELECT expires_at = 'infinity'::timestamptz AS forever FROM ${KV_TABLE} WHERE table_name = $1 AND row_data ->> 'id' = $2`,
|
|
[table, id],
|
|
);
|
|
return result.rows[0]?.forever === true;
|
|
}
|
|
|
|
async function setExpiry(table: string, id: string, expression: string): Promise<void> {
|
|
await raw.query(
|
|
`UPDATE ${KV_TABLE} SET expires_at = ${expression} WHERE table_name = $1 AND row_data ->> 'id' = $2`,
|
|
[table, id],
|
|
);
|
|
}
|
|
|
|
async function seed(table: string, key: string, age: string, expires: Date | string | null = null): Promise<string> {
|
|
const result = await raw.query<{updated_at: string}>(
|
|
`INSERT INTO ${KV_TABLE} (table_name, partition_key, row_key, row_data, expires_at, updated_at)
|
|
VALUES ($1, $2, $2, '{}'::jsonb, $3::timestamptz, now() - $4::interval)
|
|
RETURNING updated_at::text`,
|
|
[table, key, expires, age],
|
|
);
|
|
return result.rows[0]!.updated_at;
|
|
}
|
|
|
|
async function remaining(): Promise<Array<{table_name: string; row_key: string}>> {
|
|
const result = await raw.query<{table_name: string; row_key: string}>(
|
|
`SELECT table_name, row_key FROM ${KV_TABLE} WHERE table_name <> '__fluxer_schema_migrations' ORDER BY table_name, row_key`,
|
|
);
|
|
return result.rows;
|
|
}
|
|
|
|
async function ageMarker(): Promise<void> {
|
|
await raw.query(
|
|
`UPDATE ${KV_TABLE} SET row_data = jsonb_build_object('applied_at', now() - interval '2 days') WHERE table_name = '__fluxer_schema_migrations' AND row_key = 'default_ttl_expiry_v1'`,
|
|
);
|
|
}
|
|
|
|
async function resumePoint(): Promise<object | null> {
|
|
const result = await raw.query<{row_data: object}>(
|
|
`SELECT row_data FROM ${KV_TABLE} WHERE table_name = '__fluxer_schema_migrations' AND row_key = $1`,
|
|
[DEFAULT_TTL_EXPIRY_RESUME],
|
|
);
|
|
return result.rows[0]?.row_data ?? null;
|
|
}
|
|
|
|
async function markerCount(): Promise<number> {
|
|
const result = await raw.query<{n: number}>(
|
|
`SELECT count(*)::int AS n FROM ${KV_TABLE} WHERE table_name = '__fluxer_schema_migrations' AND row_key = 'default_ttl_expiry_v1'`,
|
|
);
|
|
return result.rows[0]!.n;
|
|
}
|
|
|
|
beforeAll(async () => {
|
|
const port = await freePort();
|
|
startDockerContainer([
|
|
'run',
|
|
'-d',
|
|
'--name',
|
|
CONTAINER,
|
|
'-e',
|
|
'POSTGRES_USER=fluxer',
|
|
'-e',
|
|
'POSTGRES_PASSWORD=fluxer',
|
|
'-e',
|
|
'POSTGRES_DB=fluxer',
|
|
'-p',
|
|
`127.0.0.1:${port}:5432`,
|
|
'postgres:16-alpine',
|
|
'-c',
|
|
'fsync=off',
|
|
]);
|
|
let ready = false;
|
|
for (let attempt = 0; attempt < 180 && !ready; attempt += 1) {
|
|
await sleep(500);
|
|
const probe = spawnSync('docker', ['exec', CONTAINER, 'pg_isready', '-U', 'fluxer', '-d', 'fluxer'], {
|
|
stdio: 'ignore',
|
|
});
|
|
if (probe.status !== 0) continue;
|
|
try {
|
|
await initPostgres({
|
|
url: `postgres://fluxer:[email protected]:${port}/fluxer`,
|
|
maxConnections: 4,
|
|
kvTable: KV_TABLE,
|
|
});
|
|
await getDefaultPostgresClient().query('SELECT 1');
|
|
ready = true;
|
|
} catch {
|
|
await shutdownPostgres().catch(() => {});
|
|
}
|
|
}
|
|
if (!ready) throw new Error('postgres never came up');
|
|
raw = getDefaultPostgresClient();
|
|
await ensurePostgresKvSchema(raw);
|
|
executor = new PostgresKvQueryExecutor(raw);
|
|
}, 900_000);
|
|
|
|
beforeEach(async () => {
|
|
await raw.query(`DELETE FROM ${KV_TABLE}`);
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await shutdownPostgres().catch(() => {});
|
|
spawnSync('docker', ['rm', '-f', CONTAINER], {stdio: 'ignore'});
|
|
});
|
|
|
|
it('gives every full-row write without a TTL the table default', async () => {
|
|
await executor.executeQuery(DefaultTtlProbe.insert({id: 'insert', value: 'a', note: null}));
|
|
await executor.executeQuery(DefaultTtlProbe.upsertAll({id: 'upsert', value: 'b', note: 'n'}));
|
|
expect(
|
|
await executor.executeQuery(DefaultTtlProbe.insertIfNotExists({id: 'claimed', value: 'c', note: null})),
|
|
).toEqual([{'[applied]': true}]);
|
|
expect(
|
|
await executor.executeQuery(
|
|
DefaultTtlProbeRows.conditionalBatch([{action: 'insert', row: {owner: 'o', id: 'batched', value: 'd'}}]),
|
|
),
|
|
).toEqual([{'[applied]': true}]);
|
|
|
|
for (const id of ['insert', 'upsert', 'claimed']) {
|
|
expectExpiresIn(await expiresAt('default_ttl_probe', id), DEFAULT_TTL_SECONDS);
|
|
}
|
|
expectExpiresIn(await expiresAt('default_ttl_probe_rows', 'batched'), DEFAULT_TTL_SECONDS);
|
|
});
|
|
|
|
it('keeps an explicit TTL ahead of the default', async () => {
|
|
await executor.executeQuery(DefaultTtlProbe.insertWithTtl({id: 'short', value: 'a', note: null}, 60));
|
|
expectExpiresIn(await expiresAt('default_ttl_probe', 'short'), 60);
|
|
|
|
await executor.executeQuery(DefaultTtlProbe.insert({id: 'patched', value: 'a', note: null}));
|
|
await executor.executeQuery(DefaultTtlProbe.patchByPkWithTtl({id: 'patched'}, {value: Db.set('b')}, 60));
|
|
expectExpiresIn(await expiresAt('default_ttl_probe', 'patched'), 60);
|
|
});
|
|
|
|
it('keeps an explicit TTL of zero as no expiry', async () => {
|
|
await executor.executeQuery(DefaultTtlProbe.insertWithTtl({id: 'forever', value: 'a', note: null}, 0));
|
|
expect(await neverExpires('default_ttl_probe', 'forever')).toBe(true);
|
|
expect(
|
|
await executor.executeQuery(
|
|
DefaultTtlProbe.select({where: DefaultTtlProbe.where.eq('id')}).bind({id: 'forever'}),
|
|
),
|
|
).toEqual([{id: 'forever', value: 'a', note: null}]);
|
|
|
|
await executor.executeQuery(DefaultTtlProbe.patchByPk({id: 'forever'}, {note: Db.set('patched')}));
|
|
expect(await neverExpires('default_ttl_probe', 'forever')).toBe(true);
|
|
|
|
await pruneExpiredPostgresKvRows(raw);
|
|
expect(await neverExpires('default_ttl_probe', 'forever')).toBe(true);
|
|
});
|
|
|
|
it('raises a patched row to the default but never lowers it', async () => {
|
|
await executor.executeQuery(DefaultTtlProbe.insertWithTtl({id: 'longer', value: 'a', note: null}, 3600));
|
|
await executor.executeQuery(DefaultTtlProbe.patchByPk({id: 'longer'}, {note: Db.set('patched')}));
|
|
expectExpiresIn(await expiresAt('default_ttl_probe', 'longer'), 3600);
|
|
|
|
await executor.executeQuery(DefaultTtlProbe.insert({id: 'soon', value: 'a', note: null}));
|
|
await setExpiry('default_ttl_probe', 'soon', "now() + interval '5 seconds'");
|
|
await executor.executeQuery(DefaultTtlProbe.patchByPk({id: 'soon'}, {note: Db.set('patched')}));
|
|
expectExpiresIn(await expiresAt('default_ttl_probe', 'soon'), DEFAULT_TTL_SECONDS);
|
|
|
|
await executor.executeQuery(DefaultTtlProbe.patchByPk({id: 'missing'}, {note: Db.set('created')}));
|
|
expectExpiresIn(await expiresAt('default_ttl_probe', 'missing'), DEFAULT_TTL_SECONDS);
|
|
|
|
await executor.executeQuery(DefaultTtlProbe.insert({id: 'unset', value: 'a', note: null}));
|
|
await setExpiry('default_ttl_probe', 'unset', 'NULL');
|
|
await executor.executeQuery(DefaultTtlProbe.patchByPk({id: 'unset'}, {note: Db.set('patched')}));
|
|
expectExpiresIn(await expiresAt('default_ttl_probe', 'unset'), DEFAULT_TTL_SECONDS);
|
|
|
|
await executor.executeQuery(DefaultTtlProbe.insert({id: 'expired', value: 'a', note: null}));
|
|
await setExpiry('default_ttl_probe', 'expired', "now() - interval '1 second'");
|
|
await executor.executeQuery(DefaultTtlProbe.patchByPk({id: 'expired'}, {note: Db.set('patched')}));
|
|
const revived = await stored('default_ttl_probe', 'expired');
|
|
expect(revived.row_data).toEqual({id: 'expired', note: 'patched'});
|
|
expectExpiresIn(revived.expires_at, DEFAULT_TTL_SECONDS);
|
|
});
|
|
|
|
it('raises conditional patches the same way', async () => {
|
|
await executor.executeQuery(DefaultTtlProbe.insert({id: 'soon', value: 'a', note: null}));
|
|
await setExpiry('default_ttl_probe', 'soon', "now() + interval '5 seconds'");
|
|
expect(
|
|
await executor.executeQuery(
|
|
DefaultTtlProbe.conditionalPatchByPk({id: 'soon'}, {note: Db.set('patched')}, {value: 'a'}),
|
|
),
|
|
).toEqual([{'[applied]': true}]);
|
|
expectExpiresIn(await expiresAt('default_ttl_probe', 'soon'), DEFAULT_TTL_SECONDS);
|
|
|
|
await executor.executeQuery(DefaultTtlProbe.insertWithTtl({id: 'longer', value: 'a', note: null}, 3600));
|
|
expect(
|
|
await executor.executeQuery(
|
|
DefaultTtlProbe.conditionalPatchByPk({id: 'longer'}, {note: Db.set('patched')}, {value: 'a'}),
|
|
),
|
|
).toEqual([{'[applied]': true}]);
|
|
expectExpiresIn(await expiresAt('default_ttl_probe', 'longer'), 3600);
|
|
|
|
await executor.executeQuery(DefaultTtlProbeRows.insert({owner: 'o', id: 'existing', value: 'old'}));
|
|
await setExpiry('default_ttl_probe_rows', 'existing', 'NULL');
|
|
expect(
|
|
await executor.executeQuery(
|
|
DefaultTtlProbeRows.conditionalBatch([
|
|
{action: 'insert', row: {owner: 'o', id: 'added', value: 'new'}},
|
|
{
|
|
action: 'patch',
|
|
pk: {owner: 'o', id: 'existing'},
|
|
patch: {value: Db.set('updated')},
|
|
expected: {value: 'old'},
|
|
},
|
|
]),
|
|
),
|
|
).toEqual([{'[applied]': true}]);
|
|
expectExpiresIn(await expiresAt('default_ttl_probe_rows', 'added'), DEFAULT_TTL_SECONDS);
|
|
expectExpiresIn(await expiresAt('default_ttl_probe_rows', 'existing'), DEFAULT_TTL_SECONDS);
|
|
});
|
|
|
|
it('leaves tables without a default untouched', async () => {
|
|
await executor.executeQuery(NoTtlProbe.insert({id: 'plain', value: 'a', note: null}));
|
|
expect(await expiresAt('no_ttl_probe', 'plain')).toBeNull();
|
|
await executor.executeQuery(NoTtlProbe.patchByPk({id: 'plain'}, {note: Db.set('patched')}));
|
|
expect(await expiresAt('no_ttl_probe', 'plain')).toBeNull();
|
|
await executor.executeQuery(NoTtlProbe.insertWithTtl({id: 'zero', value: 'a', note: null}, 0));
|
|
expect(await expiresAt('no_ttl_probe', 'zero')).toBeNull();
|
|
});
|
|
|
|
it('gives rows an older image wrote the expiry of their last write and deletes the ones past it', async () => {
|
|
const mentionWrittenAt = await seed('recent_mentions', 'rm-day', '1 day');
|
|
await seed('recent_mentions', 'rm-week', '8 days');
|
|
await seed('attachment_upload_traces_by_key', 'at-31', '31 days');
|
|
await seed('attachment_upload_traces_by_key', 'at-29', '29 days');
|
|
await seed('oauth2_access_tokens', 'oa-8', '8 days');
|
|
await seed('donor_magic_link_tokens', 'dm-hour', '1 hour');
|
|
await seed('jobs_by_id', 'job', '100 days');
|
|
await seed('users', 'user', '100 days');
|
|
await seed('recent_mentions', 'rm-forever', '1 day', 'infinity');
|
|
await seed('recent_mentions', 'rm-hour', '30 days', new Date(Date.now() + 3_600_000));
|
|
|
|
expect(await expireLegacyDefaultTtlRows(raw, Date.now() + 60_000)).toEqual({
|
|
deleted: 4,
|
|
expiring: 3,
|
|
complete: true,
|
|
});
|
|
expect(await remaining()).toEqual([
|
|
{table_name: 'attachment_upload_traces_by_key', row_key: 'at-29'},
|
|
{table_name: 'jobs_by_id', row_key: 'job'},
|
|
{table_name: 'recent_mentions', row_key: 'rm-day'},
|
|
{table_name: 'recent_mentions', row_key: 'rm-forever'},
|
|
{table_name: 'recent_mentions', row_key: 'rm-hour'},
|
|
{table_name: 'users', row_key: 'user'},
|
|
]);
|
|
|
|
const exact = await raw.query<{row_key: string; exact: boolean; unchanged: boolean | null}>(
|
|
`SELECT row_key,
|
|
expires_at = updated_at + CASE table_name WHEN 'recent_mentions' THEN interval '7 days' WHEN 'attachment_upload_traces_by_key' THEN interval '30 days' ELSE interval '90 days' END AS exact,
|
|
CASE WHEN row_key = 'rm-day' THEN updated_at = $1::timestamptz END AS unchanged
|
|
FROM ${KV_TABLE}
|
|
WHERE row_key IN ('rm-day', 'at-29', 'ip-day')
|
|
ORDER BY row_key`,
|
|
[mentionWrittenAt],
|
|
);
|
|
expect(exact.rows).toEqual([
|
|
{row_key: 'at-29', exact: true, unchanged: null},
|
|
{row_key: 'ip-day', exact: true, unchanged: null},
|
|
{row_key: 'rm-day', exact: true, unchanged: true},
|
|
]);
|
|
const untouched = await raw.query<{row_key: string; state: string}>(
|
|
`SELECT row_key, CASE WHEN expires_at IS NULL THEN 'unset' WHEN expires_at = 'infinity' THEN 'forever' ELSE 'set' END AS state
|
|
FROM ${KV_TABLE}
|
|
WHERE row_key IN ('job', 'user', 'rm-forever', 'rm-hour')
|
|
ORDER BY row_key`,
|
|
);
|
|
expect(untouched.rows).toEqual([
|
|
{row_key: 'job', state: 'unset'},
|
|
{row_key: 'rm-forever', state: 'forever'},
|
|
{row_key: 'rm-hour', state: 'set'},
|
|
{row_key: 'user', state: 'unset'},
|
|
]);
|
|
|
|
expect(await markerCount()).toBe(0);
|
|
expect(await expireLegacyDefaultTtlRows(raw, Date.now() + 60_000)).toEqual({
|
|
deleted: 0,
|
|
expiring: 0,
|
|
complete: true,
|
|
});
|
|
expect(await markerCount()).toBe(1);
|
|
expect(await expireLegacyDefaultTtlRows(raw, Date.now() + 60_000)).toBeNull();
|
|
});
|
|
|
|
it('checks again a day after a clean pass', async () => {
|
|
expect(await expireLegacyDefaultTtlRows(raw, Date.now() + 60_000)).toEqual({
|
|
deleted: 0,
|
|
expiring: 0,
|
|
complete: true,
|
|
});
|
|
expect(await expireLegacyDefaultTtlRows(raw, Date.now() + 60_000)).toBeNull();
|
|
|
|
await seed('recent_mentions', 'rm-rolled-back', '1 day');
|
|
expect(await expireLegacyDefaultTtlRows(raw, Date.now() + 60_000)).toBeNull();
|
|
const before = await raw.query(`SELECT expires_at FROM ${KV_TABLE} WHERE row_key = 'rm-rolled-back'`);
|
|
expect(before.rows).toEqual([{expires_at: null}]);
|
|
|
|
await ageMarker();
|
|
expect(await expireLegacyDefaultTtlRows(raw, Date.now() + 60_000)).toEqual({
|
|
deleted: 0,
|
|
expiring: 1,
|
|
complete: true,
|
|
});
|
|
expect(await expireLegacyDefaultTtlRows(raw, Date.now() + 60_000)).toEqual({
|
|
deleted: 0,
|
|
expiring: 0,
|
|
complete: true,
|
|
});
|
|
expect(await expireLegacyDefaultTtlRows(raw, Date.now() + 60_000)).toBeNull();
|
|
});
|
|
|
|
it('pages through more rows than one page holds and stops at its deadline', async () => {
|
|
await raw.query(
|
|
`INSERT INTO ${KV_TABLE} (table_name, partition_key, row_key, row_data, updated_at)
|
|
SELECT 'recent_mentions', 'rm-' || lpad(g::text, 5, '0'), 'rm-' || lpad(g::text, 5, '0'), '{}'::jsonb, now() - interval '1 day'
|
|
FROM generate_series(1, 2300) g`,
|
|
);
|
|
|
|
expect(await expireLegacyDefaultTtlRows(raw, Date.now() - 1)).toEqual({
|
|
deleted: 0,
|
|
expiring: 0,
|
|
complete: false,
|
|
});
|
|
expect(await markerCount()).toBe(0);
|
|
expect(await expireLegacyDefaultTtlRows(raw, Date.now() + 60_000)).toEqual({
|
|
deleted: 0,
|
|
expiring: 2300,
|
|
complete: true,
|
|
});
|
|
const unset = await raw.query<{n: number}>(
|
|
`SELECT count(*)::int AS n FROM ${KV_TABLE} WHERE table_name = 'recent_mentions' AND expires_at IS NULL`,
|
|
);
|
|
expect(unset.rows[0]).toEqual({n: 0});
|
|
expect(await expireLegacyDefaultTtlRows(raw, Date.now() + 60_000)).toEqual({
|
|
deleted: 0,
|
|
expiring: 0,
|
|
complete: true,
|
|
});
|
|
expect(await expireLegacyDefaultTtlRows(raw, Date.now() + 60_000)).toBeNull();
|
|
});
|
|
|
|
it('saves where a run stopped and starts the next run there', async () => {
|
|
const first = DEFAULT_TTL_TABLES[0]!.name;
|
|
const last = DEFAULT_TTL_TABLES.at(-1)!.name;
|
|
await seed(first, 'a', '1 hour');
|
|
await seed(first, 'z', '1 hour');
|
|
await seed(last, 'k', '1 hour');
|
|
|
|
expect(await expireLegacyDefaultTtlRows(raw, Date.now() - 1)).toEqual({deleted: 0, expiring: 0, complete: false});
|
|
expect(await resumePoint()).toEqual({table: first, row_key: '', unset: 0});
|
|
|
|
await raw.query(
|
|
`UPDATE ${KV_TABLE} SET row_data = jsonb_build_object('table', $1::text, 'row_key', 'm', 'unset', 0) WHERE table_name = '__fluxer_schema_migrations' AND row_key = $2`,
|
|
[first, DEFAULT_TTL_EXPIRY_RESUME],
|
|
);
|
|
expect(await expireLegacyDefaultTtlRows(raw, Date.now() + 60_000)).toEqual({
|
|
deleted: 0,
|
|
expiring: 2,
|
|
complete: true,
|
|
});
|
|
const untouched = await raw.query<{expires_at: Date | null}>(
|
|
`SELECT expires_at FROM ${KV_TABLE} WHERE table_name = $1 AND row_key = 'a'`,
|
|
[first],
|
|
);
|
|
expect(untouched.rows).toEqual([{expires_at: null}]);
|
|
expect(await resumePoint()).toBeNull();
|
|
expect(await markerCount()).toBe(0);
|
|
|
|
expect(await expireLegacyDefaultTtlRows(raw, Date.now() + 60_000)).toEqual({
|
|
deleted: 0,
|
|
expiring: 1,
|
|
complete: true,
|
|
});
|
|
expect(await expireLegacyDefaultTtlRows(raw, Date.now() + 60_000)).toEqual({
|
|
deleted: 0,
|
|
expiring: 0,
|
|
complete: true,
|
|
});
|
|
expect(await markerCount()).toBe(1);
|
|
});
|
|
});
|