@purposeinplay/payload-version-retention 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +553 -0
- package/dist/defaults.d.ts +93 -0
- package/dist/defaults.js +74 -0
- package/dist/exports/index.d.ts +2 -0
- package/dist/exports/index.js +1 -0
- package/dist/hooks/dedup-versions.d.ts +28 -0
- package/dist/hooks/dedup-versions.js +123 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/lib/dedup-cursor.d.ts +34 -0
- package/dist/lib/dedup-cursor.js +94 -0
- package/dist/lib/entities.d.ts +43 -0
- package/dist/lib/entities.js +98 -0
- package/dist/lib/retention-plan.d.ts +72 -0
- package/dist/lib/retention-plan.js +158 -0
- package/dist/lib/run-lock.d.ts +25 -0
- package/dist/lib/run-lock.js +105 -0
- package/dist/lib/snapshot-compare.d.ts +37 -0
- package/dist/lib/snapshot-compare.js +86 -0
- package/dist/lib/startup-checks.d.ts +55 -0
- package/dist/lib/startup-checks.js +225 -0
- package/dist/lib/state.d.ts +27 -0
- package/dist/lib/state.js +87 -0
- package/dist/lib/sweep.d.ts +72 -0
- package/dist/lib/sweep.js +799 -0
- package/dist/lib/version-row.d.ts +19 -0
- package/dist/lib/version-row.js +77 -0
- package/dist/plugin.d.ts +3 -0
- package/dist/plugin.js +91 -0
- package/dist/tasks/janitor.d.ts +8 -0
- package/dist/tasks/janitor.js +116 -0
- package/dist/types.d.ts +239 -0
- package/dist/types.js +4 -0
- package/package.json +56 -0
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { JANITOR_TASK_SLUG, JOBS_COLLECTION_SLUG, LOG_PREFIX, STATE_GLOBAL_SLUG } from '../defaults.js';
|
|
2
|
+
/**
|
|
3
|
+
* Slug of the global Payload injects into the config as soon as any task or
|
|
4
|
+
* workflow declares a `schedule` (`jobs.scheduling` is flipped on during
|
|
5
|
+
* config sanitization). Its table is `payload_jobs_stats`.
|
|
6
|
+
*/ export const JOB_STATS_GLOBAL_SLUG = 'payload-jobs-stats';
|
|
7
|
+
/** Queue `handleSchedules` falls back to when an autoRun entry names none. */ const DEFAULT_AUTORUN_QUEUE = 'default';
|
|
8
|
+
const MISSING_TABLE_MESSAGE = [
|
|
9
|
+
`${LOG_PREFIX} Could not read the \`payload-jobs-stats\` global.`,
|
|
10
|
+
`This plugin schedules its \`${JANITOR_TASK_SLUG}\` task, which turns on Payload job`,
|
|
11
|
+
'scheduling config-wide and requires the `payload_jobs_stats` table',
|
|
12
|
+
'(wild consumers additionally need the `payload_jobs.meta` column).',
|
|
13
|
+
'Run `payload migrate:create` and `payload migrate` — the plugin adoption and',
|
|
14
|
+
'that migration must ship in the same release.',
|
|
15
|
+
'Until the table exists, `handleSchedules` throws on every autoRun tick before',
|
|
16
|
+
'`jobs.run` is reached, which stalls the entire `default` queue — not just this',
|
|
17
|
+
'plugin. Pass `janitor: false` to opt out of scheduling in the meantime.'
|
|
18
|
+
].join(' ');
|
|
19
|
+
/**
|
|
20
|
+
* Startup probe for the jobs-stats table. Logs an error and resolves `false`
|
|
21
|
+
* when the global cannot be read; never throws and never creates tables.
|
|
22
|
+
*/ export async function checkJobStatsTable(payload) {
|
|
23
|
+
try {
|
|
24
|
+
await payload.db.findGlobal({
|
|
25
|
+
slug: JOB_STATS_GLOBAL_SLUG
|
|
26
|
+
});
|
|
27
|
+
return true;
|
|
28
|
+
} catch (error) {
|
|
29
|
+
payload.logger.error({
|
|
30
|
+
err: error
|
|
31
|
+
}, MISSING_TABLE_MESSAGE);
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const MISSING_META_MESSAGE = [
|
|
36
|
+
`${LOG_PREFIX} Could not read the \`meta\` field of the \`payload-jobs\` collection.`,
|
|
37
|
+
'Enabling job scheduling also sets `jobs.stats`, which adds that field — its',
|
|
38
|
+
'`payload_jobs.meta` column ships in the same migration as `payload_jobs_stats`.',
|
|
39
|
+
'Scheduled jobs are queued with `meta.scheduled = true`, so without the column',
|
|
40
|
+
'every scheduling attempt fails. Run `payload migrate:create` and `payload migrate`.'
|
|
41
|
+
].join(' ');
|
|
42
|
+
/**
|
|
43
|
+
* Probe for the `payload_jobs.meta` column. Selecting the field is enough — the
|
|
44
|
+
* adapter names the column in the query, so a missing one throws. No raw SQL.
|
|
45
|
+
*/ export async function checkJobsMetaColumn(payload) {
|
|
46
|
+
try {
|
|
47
|
+
await payload.db.find({
|
|
48
|
+
collection: JOBS_COLLECTION_SLUG,
|
|
49
|
+
limit: 1,
|
|
50
|
+
pagination: false,
|
|
51
|
+
select: {
|
|
52
|
+
meta: true
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
return true;
|
|
56
|
+
} catch (error) {
|
|
57
|
+
payload.logger.error({
|
|
58
|
+
err: error
|
|
59
|
+
}, MISSING_META_MESSAGE);
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
const MISSING_STATE_MESSAGE = [
|
|
64
|
+
`${LOG_PREFIX} Could not read the \`${STATE_GLOBAL_SLUG}\` global.`,
|
|
65
|
+
'This plugin keeps its dedup cursor there, because Payload deletes a completed',
|
|
66
|
+
'job row by default and a cursor kept in the job output would not survive to the',
|
|
67
|
+
'next run. Without the `version_retention_state` table the age sweep still works,',
|
|
68
|
+
'but identical-snapshot dedup restarts its cycle at the first entity every run and',
|
|
69
|
+
'never reaches the rest of the corpus.',
|
|
70
|
+
'Run `payload migrate:create` and `payload migrate`.'
|
|
71
|
+
].join(' ');
|
|
72
|
+
/**
|
|
73
|
+
* Startup probe for the plugin's own state table. Logs an error and resolves
|
|
74
|
+
* `false` when the global cannot be read; never throws.
|
|
75
|
+
*/ export async function checkStateGlobal(payload) {
|
|
76
|
+
try {
|
|
77
|
+
await payload.db.findGlobal({
|
|
78
|
+
slug: STATE_GLOBAL_SLUG
|
|
79
|
+
});
|
|
80
|
+
return true;
|
|
81
|
+
} catch (error) {
|
|
82
|
+
payload.logger.error({
|
|
83
|
+
err: error
|
|
84
|
+
}, MISSING_STATE_MESSAGE);
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/** The two enums Payload derives from the registered task slugs on Postgres. */ export const TASK_SLUG_ENUMS = [
|
|
89
|
+
'enum_payload_jobs_task_slug',
|
|
90
|
+
'enum_payload_jobs_log_task_slug'
|
|
91
|
+
];
|
|
92
|
+
function isPoolLike(value) {
|
|
93
|
+
return typeof value === 'object' && value !== null && typeof value.query === 'function';
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* The adapter's raw connection pool, when it has one and is a Postgres.
|
|
97
|
+
*
|
|
98
|
+
* `pool` is a Postgres-adapter detail the shared `BaseDatabaseAdapter` type
|
|
99
|
+
* does not declare, so it is probed rather than asserted. Returns `undefined`
|
|
100
|
+
* for every other adapter, which is also the "nothing to check" answer:
|
|
101
|
+
* Mongo and SQLite store the task slug as plain text.
|
|
102
|
+
*/ function postgresPool(payload) {
|
|
103
|
+
const db = payload.db;
|
|
104
|
+
if (typeof db !== 'object' || db === null) {
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
const name = 'name' in db ? db.name : undefined;
|
|
108
|
+
if (typeof name !== 'string' || !name.includes('postgres')) {
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
const pool = 'pool' in db ? db.pool : undefined;
|
|
112
|
+
return isPoolLike(pool) ? pool : undefined;
|
|
113
|
+
}
|
|
114
|
+
/** The adapter's `schemaName`, or Postgres' default. */ function schemaOf(payload) {
|
|
115
|
+
const db = payload.db;
|
|
116
|
+
if (typeof db === 'object' && db !== null && 'schemaName' in db) {
|
|
117
|
+
const { schemaName } = db;
|
|
118
|
+
if (typeof schemaName === 'string' && schemaName.length > 0) {
|
|
119
|
+
return schemaName;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return 'public';
|
|
123
|
+
}
|
|
124
|
+
// Scoped to the adapter's schema: a multi-tenant database can hold the same
|
|
125
|
+
// enum name in several schemas, and grouping across them would let a sibling
|
|
126
|
+
// tenant's migration vouch for this one.
|
|
127
|
+
const ENUM_QUERY = `SELECT t.typname AS enum_name, bool_or(e.enumlabel = $1) AS has_label
|
|
128
|
+
FROM pg_type t
|
|
129
|
+
JOIN pg_namespace n ON n.oid = t.typnamespace
|
|
130
|
+
JOIN pg_enum e ON e.enumtypid = t.oid
|
|
131
|
+
WHERE t.typname = ANY($2) AND n.nspname = $3
|
|
132
|
+
GROUP BY t.typname`;
|
|
133
|
+
function missingEnumMessage(missing) {
|
|
134
|
+
return [
|
|
135
|
+
`${LOG_PREFIX} The \`${JANITOR_TASK_SLUG}\` task slug is missing from`,
|
|
136
|
+
`${missing.map((name)=>`\`${name}\``).join(' and ')}.`,
|
|
137
|
+
'On Postgres, Payload generates `payload_jobs.task_slug` and',
|
|
138
|
+
'`payload_jobs_log.task_slug` as enums built from the registered tasks, so queuing',
|
|
139
|
+
'the janitor fails with `invalid input value for enum ... ` until the slug is added.',
|
|
140
|
+
'Run `payload migrate:create` and `payload migrate` — it emits, for each enum:',
|
|
141
|
+
missing.map((name)=>`ALTER TYPE "${name}" ADD VALUE '${JANITOR_TASK_SLUG}';`).join(' '),
|
|
142
|
+
'The plugin adoption and that migration must ship in the same release.',
|
|
143
|
+
'MongoDB and SQLite store the slug as plain text and need nothing.'
|
|
144
|
+
].join(' ');
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Verifies the janitor's slug is a member of both jobs task-slug enums.
|
|
148
|
+
*
|
|
149
|
+
* Postgres only, and only when the adapter exposes a pool — every other
|
|
150
|
+
* adapter stores the slug as text, and an adapter this cannot inspect is left
|
|
151
|
+
* alone rather than warned about. Logs an error and resolves `false` when the
|
|
152
|
+
* slug is missing; never throws.
|
|
153
|
+
*/ export async function checkTaskSlugEnum(payload) {
|
|
154
|
+
const pool = postgresPool(payload);
|
|
155
|
+
if (!pool) {
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
try {
|
|
159
|
+
const { rows } = await pool.query(ENUM_QUERY, [
|
|
160
|
+
JANITOR_TASK_SLUG,
|
|
161
|
+
[
|
|
162
|
+
...TASK_SLUG_ENUMS
|
|
163
|
+
],
|
|
164
|
+
schemaOf(payload)
|
|
165
|
+
]);
|
|
166
|
+
// An enum type that does not exist at all means the jobs tables are not
|
|
167
|
+
// there either — a different problem, already reported by the jobs-stats
|
|
168
|
+
// check. Only complain about an enum that exists without the slug.
|
|
169
|
+
const missing = rows.filter((row)=>row.has_label !== true).map((row)=>String(row.enum_name));
|
|
170
|
+
if (missing.length === 0) {
|
|
171
|
+
return true;
|
|
172
|
+
}
|
|
173
|
+
payload.logger.error(missingEnumMessage(missing));
|
|
174
|
+
return false;
|
|
175
|
+
} catch (error) {
|
|
176
|
+
payload.logger.warn({
|
|
177
|
+
err: error
|
|
178
|
+
}, `${LOG_PREFIX} Could not inspect the jobs task-slug enums; skipping that check. ` + 'If queuing the janitor fails with `invalid input value for enum`, the slug is ' + 'missing and a migration must add it.');
|
|
179
|
+
return true;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Warns when nothing in `jobs.autoRun` drains a queue the plugin schedules onto.
|
|
184
|
+
* `handleSchedules` silently skips every queue the running autoRun entry does
|
|
185
|
+
* not name (unless it sets `allQueues`), so a schedule on an undrained queue
|
|
186
|
+
* never fires and produces no error of its own.
|
|
187
|
+
*
|
|
188
|
+
* A warning, not an error: the consumer may instead run a dedicated worker with
|
|
189
|
+
* `payload jobs:run --handle-schedules`, which this cannot see. Returns false
|
|
190
|
+
* when a warning was emitted.
|
|
191
|
+
*/ export function checkScheduleIsDrained(payload, scheduledQueues) {
|
|
192
|
+
const autoRun = payload.config.jobs?.autoRun;
|
|
193
|
+
if (typeof autoRun === 'function') {
|
|
194
|
+
// Resolved by Payload at cron-init time; not knowable here without calling
|
|
195
|
+
// the consumer's factory a second time.
|
|
196
|
+
return true;
|
|
197
|
+
}
|
|
198
|
+
const entries = autoRun ?? [];
|
|
199
|
+
const undrained = scheduledQueues.filter((queue)=>!entries.some((entry)=>!entry.disableScheduling && (entry.allQueues === true || (entry.queue ?? DEFAULT_AUTORUN_QUEUE) === queue)));
|
|
200
|
+
if (undrained.length === 0) {
|
|
201
|
+
return true;
|
|
202
|
+
}
|
|
203
|
+
payload.logger.warn(`${LOG_PREFIX} Nothing in \`jobs.autoRun\` drains ${undrained.map((queue)=>`\`${queue}\``).join(', ')}, which the version-retention janitor is scheduled onto. ` + '`handleSchedules` skips queues the running autoRun entry does not name (unless it sets ' + '`allQueues`), so the janitor will never be queued. Add an autoRun entry for that queue, ' + 'or run a dedicated worker with `payload jobs:run --handle-schedules` — this check cannot ' + 'see a separate worker process.');
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* The boot-time checks that only matter once a schedule exists. The state
|
|
208
|
+
* global is checked separately, because a manually queued janitor needs it
|
|
209
|
+
* just as much. Logs; never throws.
|
|
210
|
+
*/ export async function runSchedulingStartupChecks(payload, scheduledQueues) {
|
|
211
|
+
const statsTableOk = await checkJobStatsTable(payload);
|
|
212
|
+
// The meta column ships in the same migration; probing it after a failed
|
|
213
|
+
// stats read would only repeat the same news.
|
|
214
|
+
if (statsTableOk) {
|
|
215
|
+
await checkJobsMetaColumn(payload);
|
|
216
|
+
}
|
|
217
|
+
await checkTaskSlugEnum(payload);
|
|
218
|
+
checkScheduleIsDrained(payload, scheduledQueues);
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* One-liner for `janitor: { schedule: [] }` — the task is registered but
|
|
222
|
+
* nothing will ever queue it, which is easy to reach by accident.
|
|
223
|
+
*/ export function warnScheduleDisabled(payload) {
|
|
224
|
+
payload.logger.warn(`${LOG_PREFIX} \`janitor.schedule\` is empty: the \`${JANITOR_TASK_SLUG}\` task is ` + 'registered but never scheduled, so old versions are not deleted. Queue it yourself, or ' + 'drop `schedule` to use the default (daily 03:00 on the `default` queue).');
|
|
225
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { GlobalConfig, Payload } from 'payload';
|
|
2
|
+
import type { VersionRetentionDedupCursor } from '../types.js';
|
|
3
|
+
/**
|
|
4
|
+
* The plugin's own scrap of persistent state: one hidden global holding one
|
|
5
|
+
* JSON field.
|
|
6
|
+
*
|
|
7
|
+
* It exists because there is nowhere else to put the dedup cursor. Payload's
|
|
8
|
+
* `deleteJobOnComplete` defaults to `true` and the completed job row is hard
|
|
9
|
+
* deleted, so a cursor written into a job's output is unreadable by the time
|
|
10
|
+
* the next run starts — which made the whole rotation inert under the default
|
|
11
|
+
* configuration, however carefully the cursor was computed.
|
|
12
|
+
*
|
|
13
|
+
* Access is denied to everyone; the plugin reads and writes it with
|
|
14
|
+
* `overrideAccess`, and nothing else has any business in it.
|
|
15
|
+
*/
|
|
16
|
+
export declare function createStateGlobal(): GlobalConfig;
|
|
17
|
+
/**
|
|
18
|
+
* The dedup cursor the last run left behind, or `undefined` to start the cycle
|
|
19
|
+
* from the beginning. Never throws: a consumer who has not run the migration
|
|
20
|
+
* yet gets a warning and a fresh cycle, not a failed job.
|
|
21
|
+
*/
|
|
22
|
+
export declare function readStoredCursor(payload: Payload): Promise<undefined | VersionRetentionDedupCursor>;
|
|
23
|
+
/**
|
|
24
|
+
* Records where dedup stopped, so the next run resumes there. `null` means the
|
|
25
|
+
* cycle completed and the next run starts over.
|
|
26
|
+
*/
|
|
27
|
+
export declare function writeStoredCursor(payload: Payload, dedupCursor: null | VersionRetentionDedupCursor): Promise<void>;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { LOG_PREFIX, STATE_GLOBAL_SLUG } from '../defaults.js';
|
|
2
|
+
import { toDedupCursor } from './dedup-cursor.js';
|
|
3
|
+
/**
|
|
4
|
+
* The plugin's own scrap of persistent state: one hidden global holding one
|
|
5
|
+
* JSON field.
|
|
6
|
+
*
|
|
7
|
+
* It exists because there is nowhere else to put the dedup cursor. Payload's
|
|
8
|
+
* `deleteJobOnComplete` defaults to `true` and the completed job row is hard
|
|
9
|
+
* deleted, so a cursor written into a job's output is unreadable by the time
|
|
10
|
+
* the next run starts — which made the whole rotation inert under the default
|
|
11
|
+
* configuration, however carefully the cursor was computed.
|
|
12
|
+
*
|
|
13
|
+
* Access is denied to everyone; the plugin reads and writes it with
|
|
14
|
+
* `overrideAccess`, and nothing else has any business in it.
|
|
15
|
+
*/ export function createStateGlobal() {
|
|
16
|
+
return {
|
|
17
|
+
slug: STATE_GLOBAL_SLUG,
|
|
18
|
+
access: {
|
|
19
|
+
read: ()=>false,
|
|
20
|
+
update: ()=>false
|
|
21
|
+
},
|
|
22
|
+
admin: {
|
|
23
|
+
hidden: true
|
|
24
|
+
},
|
|
25
|
+
fields: [
|
|
26
|
+
{
|
|
27
|
+
name: 'state',
|
|
28
|
+
type: 'json',
|
|
29
|
+
admin: {
|
|
30
|
+
readOnly: true
|
|
31
|
+
},
|
|
32
|
+
label: 'Version retention state'
|
|
33
|
+
}
|
|
34
|
+
],
|
|
35
|
+
graphQL: false,
|
|
36
|
+
label: 'Version Retention State',
|
|
37
|
+
// Nothing edits this in the admin, so a row in `payload_locked_documents`
|
|
38
|
+
// on every cursor write would be pure overhead.
|
|
39
|
+
lockDocuments: false,
|
|
40
|
+
// Versioning our own bookkeeping would be a joke in poor taste.
|
|
41
|
+
versions: false
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function isRecord(value) {
|
|
45
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* The dedup cursor the last run left behind, or `undefined` to start the cycle
|
|
49
|
+
* from the beginning. Never throws: a consumer who has not run the migration
|
|
50
|
+
* yet gets a warning and a fresh cycle, not a failed job.
|
|
51
|
+
*/ export async function readStoredCursor(payload) {
|
|
52
|
+
try {
|
|
53
|
+
const global = await payload.findGlobal({
|
|
54
|
+
depth: 0,
|
|
55
|
+
overrideAccess: true,
|
|
56
|
+
slug: STATE_GLOBAL_SLUG
|
|
57
|
+
});
|
|
58
|
+
const state = isRecord(global) ? global.state : undefined;
|
|
59
|
+
return isRecord(state) ? toDedupCursor(state.dedupCursor) : undefined;
|
|
60
|
+
} catch (error) {
|
|
61
|
+
payload.logger.warn({
|
|
62
|
+
err: error
|
|
63
|
+
}, `${LOG_PREFIX} Could not read the \`${STATE_GLOBAL_SLUG}\` global; dedup starts its cycle ` + 'from the first entity. Run the migration that creates `version_retention_state`.');
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Records where dedup stopped, so the next run resumes there. `null` means the
|
|
69
|
+
* cycle completed and the next run starts over.
|
|
70
|
+
*/ export async function writeStoredCursor(payload, dedupCursor) {
|
|
71
|
+
try {
|
|
72
|
+
await payload.updateGlobal({
|
|
73
|
+
data: {
|
|
74
|
+
state: {
|
|
75
|
+
dedupCursor
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
depth: 0,
|
|
79
|
+
overrideAccess: true,
|
|
80
|
+
slug: STATE_GLOBAL_SLUG
|
|
81
|
+
});
|
|
82
|
+
} catch (error) {
|
|
83
|
+
payload.logger.warn({
|
|
84
|
+
err: error
|
|
85
|
+
}, `${LOG_PREFIX} Could not persist the dedup cursor to \`${STATE_GLOBAL_SLUG}\`. This run's ` + 'work still counts; the next one restarts the dedup cycle from the first entity.');
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { Payload, SelectType } from 'payload';
|
|
2
|
+
import type { ResolvedEntity, VersionRetentionDedupCursor, VersionRetentionJanitorOutput } from '../types.js';
|
|
3
|
+
/** Mutable per-run budget, shared across every entity in one invocation. */
|
|
4
|
+
export interface SweepBudget {
|
|
5
|
+
/** Version bodies the sweep may still read for dedup. */
|
|
6
|
+
dedupBodiesRemaining: number;
|
|
7
|
+
deletionsRemaining: number;
|
|
8
|
+
/** Only documents that actually yield a deletion draw on this. */
|
|
9
|
+
documentsRemaining: number;
|
|
10
|
+
}
|
|
11
|
+
/** What one entity's sweep contributed to the run. */
|
|
12
|
+
export interface EntitySweepResult {
|
|
13
|
+
/** Last document dedup actually read, when it stopped short of the end. */
|
|
14
|
+
dedupResumeAfter?: number | string;
|
|
15
|
+
/** True when a budget stopped dedup with documents in this entity unread. */
|
|
16
|
+
dedupStarved: boolean;
|
|
17
|
+
dedupedCount: number;
|
|
18
|
+
deletedCount: number;
|
|
19
|
+
/** Documents whose sweep threw. Logged, skipped, retried next run. */
|
|
20
|
+
failedDocuments: number;
|
|
21
|
+
hasMore: boolean;
|
|
22
|
+
scannedDocuments: number;
|
|
23
|
+
skippedRaced: number;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Columns the retention decision reads. Anything else on a version row is a
|
|
27
|
+
* full document body — the reason these tables are 54% of the database — and
|
|
28
|
+
* pulling it into the janitor would defeat the point.
|
|
29
|
+
*
|
|
30
|
+
* A drafts-less entity has no `_status`, so `version` is left out entirely
|
|
31
|
+
* rather than asking the adapter for a field that does not exist.
|
|
32
|
+
*/
|
|
33
|
+
export declare function narrowVersionSelect(hasDrafts: boolean): SelectType;
|
|
34
|
+
/**
|
|
35
|
+
* Sweeps one versioned collection.
|
|
36
|
+
*
|
|
37
|
+
* Rather than walking every document, the pass walks the stale-version rows
|
|
38
|
+
* themselves and visits only the parents that still hold deletable history.
|
|
39
|
+
* Paging is by an ascending `parent` cursor, not an offset, so deleting rows
|
|
40
|
+
* mid-pass cannot make it skip a document.
|
|
41
|
+
*/
|
|
42
|
+
export declare function sweepCollection(payload: Payload, entity: ResolvedEntity, budget: SweepBudget, now?: Date,
|
|
43
|
+
/** Resume dedup after this parent id; see `EntitySweepResult.dedupCursor`. */
|
|
44
|
+
dedupCursorAfter?: number | string): Promise<EntitySweepResult>;
|
|
45
|
+
/** Sweeps one versioned global. A global is a single "document". */
|
|
46
|
+
export declare function sweepGlobal(payload: Payload, entity: ResolvedEntity, budget: SweepBudget, now?: Date): Promise<EntitySweepResult>;
|
|
47
|
+
export interface RunSweepArgs {
|
|
48
|
+
collections: ResolvedEntity[];
|
|
49
|
+
/** Where the previous run's dedup pass stopped, from its output. */
|
|
50
|
+
dedupCursor?: null | VersionRetentionDedupCursor;
|
|
51
|
+
globals: ResolvedEntity[];
|
|
52
|
+
maxDedupBodiesPerRun: number;
|
|
53
|
+
maxDeletionsPerRun: number;
|
|
54
|
+
maxDocumentsPerRun: number;
|
|
55
|
+
now?: Date;
|
|
56
|
+
payload: Payload;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* One full janitor pass. Globals go first: there are a handful of them and
|
|
60
|
+
* they are cheap, and letting a large collection's backlog eat the budget
|
|
61
|
+
* would otherwise starve them run after run.
|
|
62
|
+
*
|
|
63
|
+
* Dedup rides a single cursor over this whole order rather than one per
|
|
64
|
+
* entity. That is what guarantees progress: a run resumes at the entity the
|
|
65
|
+
* last one stopped in, skips everything already covered in this cycle, and
|
|
66
|
+
* carries on into the entities behind it. With a per-entity cursor the
|
|
67
|
+
* entities *before* the stalled one were re-read from scratch every night and
|
|
68
|
+
* consumed the entire body budget, so the pass never advanced — measured on
|
|
69
|
+
* the production copy, where dedup collapsed pairs only in the four
|
|
70
|
+
* collections ahead of `categories` and never reached `pages` or `games`.
|
|
71
|
+
*/
|
|
72
|
+
export declare function runRetentionSweep({ collections, dedupCursor, globals, maxDedupBodiesPerRun, maxDeletionsPerRun, maxDocumentsPerRun, now, payload, }: RunSweepArgs): Promise<VersionRetentionJanitorOutput>;
|