@prairielearn/migrations 2.1.0 → 3.0.0
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/.mocharc.cjs +3 -0
- package/CHANGELOG.md +14 -0
- package/dist/batched-migrations/batched-migration-job.js +18 -22
- package/dist/batched-migrations/batched-migration-job.js.map +1 -1
- package/dist/batched-migrations/batched-migration-runner.d.ts +2 -2
- package/dist/batched-migrations/batched-migration-runner.js +22 -26
- package/dist/batched-migrations/batched-migration-runner.js.map +1 -1
- package/dist/batched-migrations/batched-migration-runner.test.js +53 -78
- package/dist/batched-migrations/batched-migration-runner.test.js.map +1 -1
- package/dist/batched-migrations/batched-migration.js +30 -41
- package/dist/batched-migrations/batched-migration.js.map +1 -1
- package/dist/batched-migrations/batched-migrations-runner.d.ts +1 -1
- package/dist/batched-migrations/batched-migrations-runner.js +32 -67
- package/dist/batched-migrations/batched-migrations-runner.js.map +1 -1
- package/dist/batched-migrations/batched-migrations-runner.test.js +41 -69
- package/dist/batched-migrations/batched-migrations-runner.test.js.map +1 -1
- package/dist/batched-migrations/fixtures/20230406184103_successful_migration.js +2 -4
- package/dist/batched-migrations/fixtures/20230406184103_successful_migration.js.map +1 -1
- package/dist/batched-migrations/fixtures/20230406184107_failing_migration.d.ts +9 -0
- package/dist/batched-migrations/fixtures/20230406184107_failing_migration.js +14 -0
- package/dist/batched-migrations/fixtures/20230406184107_failing_migration.js.map +1 -0
- package/dist/batched-migrations/fixtures/20230407230446_no_rows_migration.js +2 -4
- package/dist/batched-migrations/fixtures/20230407230446_no_rows_migration.js.map +1 -1
- package/dist/batched-migrations/index.d.ts +3 -3
- package/dist/batched-migrations/index.js +3 -17
- package/dist/batched-migrations/index.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +4 -22
- package/dist/index.js.map +1 -1
- package/dist/load-migrations.js +6 -16
- package/dist/load-migrations.js.map +1 -1
- package/dist/load-migrations.test.js +16 -44
- package/dist/load-migrations.test.js.map +1 -1
- package/dist/migrations/fixtures/20230407210430_insert_user.js +3 -6
- package/dist/migrations/fixtures/20230407210430_insert_user.js.map +1 -1
- package/dist/migrations/index.d.ts +1 -1
- package/dist/migrations/index.js +1 -5
- package/dist/migrations/index.js.map +1 -1
- package/dist/migrations/migrations.d.ts +1 -1
- package/dist/migrations/migrations.js +25 -57
- package/dist/migrations/migrations.js.map +1 -1
- package/dist/migrations/migrations.test.js +12 -17
- package/dist/migrations/migrations.test.js.map +1 -1
- package/package.json +8 -7
- package/src/batched-migrations/batched-migration-job.ts +1 -1
- package/src/batched-migrations/batched-migration-runner.test.ts +4 -4
- package/src/batched-migrations/batched-migration-runner.ts +3 -3
- package/src/batched-migrations/batched-migration.ts +1 -1
- package/src/batched-migrations/batched-migrations-runner.test.ts +8 -8
- package/src/batched-migrations/batched-migrations-runner.ts +4 -4
- package/src/batched-migrations/fixtures/20230406184103_successful_migration.ts +1 -1
- package/src/batched-migrations/fixtures/{20230406184107_failing_migration.js → 20230406184107_failing_migration.ts} +2 -3
- package/src/batched-migrations/fixtures/20230407230446_no_rows_migration.ts +1 -1
- package/src/batched-migrations/index.ts +7 -7
- package/src/index.ts +7 -7
- package/src/load-migrations.test.ts +1 -1
- package/src/migrations/index.ts +1 -1
- package/src/migrations/migrations.test.ts +2 -2
- package/src/migrations/migrations.ts +2 -2
- package/tsconfig.json +4 -1
package/.mocharc.cjs
ADDED
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# @prairielearn/migrations
|
|
2
2
|
|
|
3
|
+
## 3.0.0
|
|
4
|
+
|
|
5
|
+
### Major Changes
|
|
6
|
+
|
|
7
|
+
- 4f30b7e: Publish as native ESM
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- Updated dependencies [4f30b7e]
|
|
12
|
+
- @prairielearn/named-locks@3.0.0
|
|
13
|
+
- @prairielearn/postgres@2.0.0
|
|
14
|
+
- @prairielearn/logger@2.0.0
|
|
15
|
+
- @prairielearn/error@2.0.0
|
|
16
|
+
|
|
3
17
|
## 2.1.0
|
|
4
18
|
|
|
5
19
|
### Minor Changes
|
|
@@ -1,25 +1,21 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
const
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
started_at: zod_1.z.date().nullable(),
|
|
18
|
-
finished_at: zod_1.z.date().nullable(),
|
|
19
|
-
data: zod_1.z.unknown(),
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { loadSqlEquiv, queryRows } from '@prairielearn/postgres';
|
|
3
|
+
const sql = loadSqlEquiv(import.meta.filename);
|
|
4
|
+
export const BatchedMigrationJobStatusSchema = z.enum(['pending', 'failed', 'succeeded']);
|
|
5
|
+
export const BatchedMigrationJobRowSchema = z.object({
|
|
6
|
+
id: z.string(),
|
|
7
|
+
batched_migration_id: z.string(),
|
|
8
|
+
min_value: z.bigint({ coerce: true }),
|
|
9
|
+
max_value: z.bigint({ coerce: true }),
|
|
10
|
+
status: BatchedMigrationJobStatusSchema,
|
|
11
|
+
attempts: z.number(),
|
|
12
|
+
created_at: z.date(),
|
|
13
|
+
updated_at: z.date(),
|
|
14
|
+
started_at: z.date().nullable(),
|
|
15
|
+
finished_at: z.date().nullable(),
|
|
16
|
+
data: z.unknown(),
|
|
20
17
|
});
|
|
21
|
-
async function selectRecentJobsWithStatus(batchedMigrationId, status, limit) {
|
|
22
|
-
return await
|
|
18
|
+
export async function selectRecentJobsWithStatus(batchedMigrationId, status, limit) {
|
|
19
|
+
return await queryRows(sql.select_recent_jobs_with_status, { batched_migration_id: batchedMigrationId, status, limit }, BatchedMigrationJobRowSchema);
|
|
23
20
|
}
|
|
24
|
-
exports.selectRecentJobsWithStatus = selectRecentJobsWithStatus;
|
|
25
21
|
//# sourceMappingURL=batched-migration-job.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"batched-migration-job.js","sourceRoot":"","sources":["../../src/batched-migrations/batched-migration-job.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"batched-migration-job.js","sourceRoot":"","sources":["../../src/batched-migrations/batched-migration-job.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AAEjE,MAAM,GAAG,GAAG,YAAY,CAAC,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;AAE/C,MAAM,CAAC,MAAM,+BAA+B,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,QAAQ,EAAE,WAAW,CAAC,CAAC,CAAC;AAG1F,MAAM,CAAC,MAAM,4BAA4B,GAAG,CAAC,CAAC,MAAM,CAAC;IACnD,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE;IACd,oBAAoB,EAAE,CAAC,CAAC,MAAM,EAAE;IAChC,SAAS,EAAE,CAAC,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;IACrC,SAAS,EAAE,CAAC,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;IACrC,MAAM,EAAE,+BAA+B;IACvC,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE;IACpB,UAAU,EAAE,CAAC,CAAC,IAAI,EAAE;IACpB,UAAU,EAAE,CAAC,CAAC,IAAI,EAAE;IACpB,UAAU,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,QAAQ,EAAE;IAC/B,WAAW,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,QAAQ,EAAE;IAChC,IAAI,EAAE,CAAC,CAAC,OAAO,EAAE;CAClB,CAAC,CAAC;AAGH,MAAM,CAAC,KAAK,UAAU,0BAA0B,CAC9C,kBAA0B,EAC1B,MAAiC,EACjC,KAAa;IAEb,OAAO,MAAM,SAAS,CACpB,GAAG,CAAC,8BAA8B,EAClC,EAAE,oBAAoB,EAAE,kBAAkB,EAAE,MAAM,EAAE,KAAK,EAAE,EAC3D,4BAA4B,CAC7B,CAAC;AACJ,CAAC","sourcesContent":["import { z } from 'zod';\nimport { loadSqlEquiv, queryRows } from '@prairielearn/postgres';\n\nconst sql = loadSqlEquiv(import.meta.filename);\n\nexport const BatchedMigrationJobStatusSchema = z.enum(['pending', 'failed', 'succeeded']);\nexport type BatchedMigrationJobStatus = z.infer<typeof BatchedMigrationJobStatusSchema>;\n\nexport const BatchedMigrationJobRowSchema = z.object({\n id: z.string(),\n batched_migration_id: z.string(),\n min_value: z.bigint({ coerce: true }),\n max_value: z.bigint({ coerce: true }),\n status: BatchedMigrationJobStatusSchema,\n attempts: z.number(),\n created_at: z.date(),\n updated_at: z.date(),\n started_at: z.date().nullable(),\n finished_at: z.date().nullable(),\n data: z.unknown(),\n});\nexport type BatchedMigrationJobRow = z.infer<typeof BatchedMigrationJobRowSchema>;\n\nexport async function selectRecentJobsWithStatus(\n batchedMigrationId: string,\n status: BatchedMigrationJobStatus,\n limit: number,\n): Promise<BatchedMigrationJobRow[]> {\n return await queryRows(\n sql.select_recent_jobs_with_status,\n { batched_migration_id: batchedMigrationId, status, limit },\n BatchedMigrationJobRowSchema,\n );\n}\n"]}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
/// <reference types="node" />
|
|
2
|
-
import { BatchedMigrationRow, BatchedMigrationImplementation } from './batched-migration';
|
|
1
|
+
/// <reference types="node" resolution-mode="require"/>
|
|
2
|
+
import { BatchedMigrationRow, BatchedMigrationImplementation } from './batched-migration.js';
|
|
3
3
|
interface BatchedMigrationRunnerOptions {
|
|
4
4
|
logProgress?: boolean;
|
|
5
5
|
}
|
|
@@ -1,14 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
const batched_migration_job_1 = require("./batched-migration-job");
|
|
10
|
-
const sql = (0, postgres_1.loadSqlEquiv)(__filename);
|
|
11
|
-
class BatchedMigrationRunner {
|
|
1
|
+
import { loadSqlEquiv, queryAsync, queryRow, queryOptionalRow } from '@prairielearn/postgres';
|
|
2
|
+
import { logger } from '@prairielearn/logger';
|
|
3
|
+
import { serializeError } from 'serialize-error';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import { updateBatchedMigrationStatus, BatchedMigrationStatusSchema, } from './batched-migration.js';
|
|
6
|
+
import { BatchedMigrationJobRowSchema, } from './batched-migration-job.js';
|
|
7
|
+
const sql = loadSqlEquiv(import.meta.filename);
|
|
8
|
+
export class BatchedMigrationRunner {
|
|
12
9
|
options;
|
|
13
10
|
migration;
|
|
14
11
|
migrationImplementation;
|
|
@@ -21,19 +18,19 @@ class BatchedMigrationRunner {
|
|
|
21
18
|
}
|
|
22
19
|
log(message, ...meta) {
|
|
23
20
|
if (this.options.logProgress) {
|
|
24
|
-
|
|
21
|
+
logger.info(`[${this.migration.filename}] ${message}`, ...meta);
|
|
25
22
|
}
|
|
26
23
|
}
|
|
27
24
|
async hasIncompleteJobs(migration) {
|
|
28
|
-
return await
|
|
25
|
+
return await queryRow(sql.batched_migration_has_incomplete_jobs, { batched_migration_id: migration.id }, z.boolean());
|
|
29
26
|
}
|
|
30
27
|
async hasFailedJobs(migration) {
|
|
31
|
-
return await
|
|
28
|
+
return await queryRow(sql.batched_migration_has_failed_jobs, { batched_migration_id: migration.id }, z.boolean());
|
|
32
29
|
}
|
|
33
30
|
async refreshMigrationStatus(migration) {
|
|
34
|
-
this.migrationStatus = await
|
|
31
|
+
this.migrationStatus = await queryRow(sql.get_migration_status, {
|
|
35
32
|
id: migration.id,
|
|
36
|
-
},
|
|
33
|
+
}, BatchedMigrationStatusSchema);
|
|
37
34
|
}
|
|
38
35
|
async finishRunningMigration(migration) {
|
|
39
36
|
// Safety check: if there are any pending jobs, don't mark this
|
|
@@ -44,11 +41,11 @@ class BatchedMigrationRunner {
|
|
|
44
41
|
}
|
|
45
42
|
const hasFailedJobs = await this.hasFailedJobs(migration);
|
|
46
43
|
const finalStatus = hasFailedJobs ? 'failed' : 'succeeded';
|
|
47
|
-
await
|
|
44
|
+
await updateBatchedMigrationStatus(migration.id, finalStatus);
|
|
48
45
|
this.log(`Finished with status '${finalStatus}'`);
|
|
49
46
|
}
|
|
50
47
|
async getNextBatchBounds(migration) {
|
|
51
|
-
const lastJob = await
|
|
48
|
+
const lastJob = await queryOptionalRow(sql.select_last_batched_migration_job, { batched_migration_id: migration.id }, BatchedMigrationJobRowSchema);
|
|
52
49
|
const nextMin = lastJob ? lastJob.max_value + 1n : migration.min_value;
|
|
53
50
|
if (nextMin > migration.max_value)
|
|
54
51
|
return null;
|
|
@@ -58,7 +55,7 @@ class BatchedMigrationRunner {
|
|
|
58
55
|
return [nextMin, nextMax];
|
|
59
56
|
}
|
|
60
57
|
async startJob(job) {
|
|
61
|
-
await
|
|
58
|
+
await queryAsync(sql.start_batched_migration_job, { id: job.id });
|
|
62
59
|
const jobRange = `[${job.min_value}, ${job.max_value}]`;
|
|
63
60
|
const migrationRange = `[${this.migration.min_value}, ${this.migration.max_value}]`;
|
|
64
61
|
this.log(`Started job ${job.id} for range ${jobRange} in ${migrationRange}`);
|
|
@@ -74,7 +71,7 @@ class BatchedMigrationRunner {
|
|
|
74
71
|
});
|
|
75
72
|
}
|
|
76
73
|
async finishJob(job, status, data) {
|
|
77
|
-
await
|
|
74
|
+
await queryAsync(sql.finish_batched_migration_job, {
|
|
78
75
|
id: job.id,
|
|
79
76
|
status,
|
|
80
77
|
data: this.serializeJobData(data),
|
|
@@ -84,17 +81,17 @@ class BatchedMigrationRunner {
|
|
|
84
81
|
async getOrCreateNextMigrationJob(migration) {
|
|
85
82
|
const nextBatchBounds = await this.getNextBatchBounds(migration);
|
|
86
83
|
if (nextBatchBounds) {
|
|
87
|
-
return await
|
|
84
|
+
return await queryRow(sql.insert_batched_migration_job, {
|
|
88
85
|
batched_migration_id: migration.id,
|
|
89
86
|
min_value: nextBatchBounds[0],
|
|
90
87
|
max_value: nextBatchBounds[1],
|
|
91
|
-
},
|
|
88
|
+
}, BatchedMigrationJobRowSchema);
|
|
92
89
|
}
|
|
93
90
|
else {
|
|
94
91
|
// Pick up any old pending jobs from this migration. These will only exist if
|
|
95
92
|
// an admin manually elected to retry all failed jobs; we'll never automatically
|
|
96
93
|
// transition failed jobs back to pending.
|
|
97
|
-
return await
|
|
94
|
+
return await queryOptionalRow(sql.select_first_pending_batched_migration_job, { batched_migration_id: migration.id }, BatchedMigrationJobRowSchema);
|
|
98
95
|
}
|
|
99
96
|
}
|
|
100
97
|
async runMigrationJob(migration, migrationImplementation) {
|
|
@@ -111,8 +108,8 @@ class BatchedMigrationRunner {
|
|
|
111
108
|
error = err;
|
|
112
109
|
}
|
|
113
110
|
if (error) {
|
|
114
|
-
|
|
115
|
-
await this.finishJob(nextJob, 'failed', { error:
|
|
111
|
+
logger.error(`Error running job ${nextJob.id} for batched migration ${migration.filename}`, error);
|
|
112
|
+
await this.finishJob(nextJob, 'failed', { error: serializeError(error) });
|
|
116
113
|
}
|
|
117
114
|
else {
|
|
118
115
|
await this.finishJob(nextJob, 'succeeded');
|
|
@@ -137,5 +134,4 @@ class BatchedMigrationRunner {
|
|
|
137
134
|
}
|
|
138
135
|
}
|
|
139
136
|
}
|
|
140
|
-
exports.BatchedMigrationRunner = BatchedMigrationRunner;
|
|
141
137
|
//# sourceMappingURL=batched-migration-runner.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"batched-migration-runner.js","sourceRoot":"","sources":["../../src/batched-migrations/batched-migration-runner.ts"],"names":[],"mappings":"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","sourcesContent":["import { loadSqlEquiv, queryAsync, queryRow, queryOptionalRow } from '@prairielearn/postgres';\nimport { logger } from '@prairielearn/logger';\nimport { serializeError } from 'serialize-error';\nimport { z } from 'zod';\n\nimport {\n BatchedMigrationStatus,\n BatchedMigrationRow,\n updateBatchedMigrationStatus,\n BatchedMigrationStatusSchema,\n BatchedMigrationImplementation,\n} from './batched-migration';\nimport {\n BatchedMigrationJobRowSchema,\n BatchedMigrationJobStatus,\n BatchedMigrationJobRow,\n} from './batched-migration-job';\n\nconst sql = loadSqlEquiv(__filename);\n\ninterface BatchedMigrationRunnerOptions {\n logProgress?: boolean;\n}\n\nexport class BatchedMigrationRunner {\n private options: BatchedMigrationRunnerOptions;\n private migration: BatchedMigrationRow;\n private migrationImplementation: BatchedMigrationImplementation;\n private migrationStatus: BatchedMigrationStatus;\n\n constructor(\n migration: BatchedMigrationRow,\n migrationImplementation: BatchedMigrationImplementation,\n options: BatchedMigrationRunnerOptions = {},\n ) {\n this.options = options;\n this.migration = migration;\n this.migrationImplementation = migrationImplementation;\n this.migrationStatus = migration.status;\n }\n\n private log(message: string, ...meta: any[]) {\n if (this.options.logProgress) {\n logger.info(`[${this.migration.filename}] ${message}`, ...meta);\n }\n }\n\n private async hasIncompleteJobs(migration: BatchedMigrationRow): Promise<boolean> {\n return await queryRow(\n sql.batched_migration_has_incomplete_jobs,\n { batched_migration_id: migration.id },\n z.boolean(),\n );\n }\n\n private async hasFailedJobs(migration: BatchedMigrationRow): Promise<boolean> {\n return await queryRow(\n sql.batched_migration_has_failed_jobs,\n { batched_migration_id: migration.id },\n z.boolean(),\n );\n }\n\n private async refreshMigrationStatus(migration: BatchedMigrationRow) {\n this.migrationStatus = await queryRow(\n sql.get_migration_status,\n {\n id: migration.id,\n },\n BatchedMigrationStatusSchema,\n );\n }\n\n private async finishRunningMigration(migration: BatchedMigrationRow) {\n // Safety check: if there are any pending jobs, don't mark this\n // migration as finished.\n if (await this.hasIncompleteJobs(migration)) {\n this.log(`Incomplete jobs found, not marking as finished`);\n return;\n }\n\n const hasFailedJobs = await this.hasFailedJobs(migration);\n const finalStatus = hasFailedJobs ? 'failed' : 'succeeded';\n await updateBatchedMigrationStatus(migration.id, finalStatus);\n this.log(`Finished with status '${finalStatus}'`);\n }\n\n private async getNextBatchBounds(\n migration: BatchedMigrationRow,\n ): Promise<null | [bigint, bigint]> {\n const lastJob = await queryOptionalRow(\n sql.select_last_batched_migration_job,\n { batched_migration_id: migration.id },\n BatchedMigrationJobRowSchema,\n );\n\n const nextMin = lastJob ? lastJob.max_value + 1n : migration.min_value;\n if (nextMin > migration.max_value) return null;\n\n let nextMax = nextMin + BigInt(migration.batch_size) - 1n;\n if (nextMax > migration.max_value) nextMax = migration.max_value;\n\n return [nextMin, nextMax];\n }\n\n private async startJob(job: BatchedMigrationJobRow) {\n await queryAsync(sql.start_batched_migration_job, { id: job.id });\n const jobRange = `[${job.min_value}, ${job.max_value}]`;\n const migrationRange = `[${this.migration.min_value}, ${this.migration.max_value}]`;\n this.log(`Started job ${job.id} for range ${jobRange} in ${migrationRange}`);\n }\n\n private serializeJobData(data: unknown) {\n if (data == null) return null;\n\n // Return JSON-stringified data. Convert BigInts to strings.\n return JSON.stringify(data, (_key, value) => {\n if (typeof value === 'bigint') return value.toString();\n return value;\n });\n }\n\n private async finishJob(\n job: BatchedMigrationJobRow,\n status: Extract<BatchedMigrationJobStatus, 'failed' | 'succeeded'>,\n data?: unknown,\n ) {\n await queryAsync(sql.finish_batched_migration_job, {\n id: job.id,\n status,\n data: this.serializeJobData(data),\n });\n this.log(`Job ${job.id} finished with status '${status}'`);\n }\n\n private async getOrCreateNextMigrationJob(\n migration: BatchedMigrationRow,\n ): Promise<BatchedMigrationJobRow | null> {\n const nextBatchBounds = await this.getNextBatchBounds(migration);\n if (nextBatchBounds) {\n return await queryRow(\n sql.insert_batched_migration_job,\n {\n batched_migration_id: migration.id,\n min_value: nextBatchBounds[0],\n max_value: nextBatchBounds[1],\n },\n BatchedMigrationJobRowSchema,\n );\n } else {\n // Pick up any old pending jobs from this migration. These will only exist if\n // an admin manually elected to retry all failed jobs; we'll never automatically\n // transition failed jobs back to pending.\n return await queryOptionalRow(\n sql.select_first_pending_batched_migration_job,\n { batched_migration_id: migration.id },\n BatchedMigrationJobRowSchema,\n );\n }\n }\n\n private async runMigrationJob(\n migration: BatchedMigrationRow,\n migrationImplementation: BatchedMigrationImplementation,\n ) {\n const nextJob = await this.getOrCreateNextMigrationJob(migration);\n if (nextJob) {\n await this.startJob(nextJob);\n\n let error = null;\n try {\n // We'll only handle errors thrown by the migration itself. If any of\n // our own execution machinery throws an error, we'll let it bubble up.\n await migrationImplementation.execute(nextJob.min_value, nextJob.max_value);\n } catch (err) {\n error = err;\n }\n\n if (error) {\n logger.error(\n `Error running job ${nextJob.id} for batched migration ${migration.filename}`,\n error,\n );\n await this.finishJob(nextJob, 'failed', { error: serializeError(error) });\n } else {\n await this.finishJob(nextJob, 'succeeded');\n }\n } else {\n await this.finishRunningMigration(migration);\n }\n }\n\n async run({\n signal,\n iterations,\n durationMs,\n }: { signal?: AbortSignal; iterations?: number; durationMs?: number } = {}) {\n let iterationCount = 0;\n const endTime = durationMs ? Date.now() + durationMs : null;\n while (\n !signal?.aborted &&\n (iterations ? iterationCount < iterations : true) &&\n (endTime ? Date.now() < endTime : true) &&\n (this.migrationStatus === 'running' || this.migrationStatus === 'finalizing')\n ) {\n await this.runMigrationJob(this.migration, this.migrationImplementation);\n iterationCount += 1;\n // Always refresh the status so we can detect if the migration was marked\n // as paused by another process.\n await this.refreshMigrationStatus(this.migration);\n }\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"batched-migration-runner.js","sourceRoot":"","sources":["../../src/batched-migrations/batched-migration-runner.ts"],"names":[],"mappings":"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","sourcesContent":["import { loadSqlEquiv, queryAsync, queryRow, queryOptionalRow } from '@prairielearn/postgres';\nimport { logger } from '@prairielearn/logger';\nimport { serializeError } from 'serialize-error';\nimport { z } from 'zod';\n\nimport {\n BatchedMigrationStatus,\n BatchedMigrationRow,\n updateBatchedMigrationStatus,\n BatchedMigrationStatusSchema,\n BatchedMigrationImplementation,\n} from './batched-migration.js';\nimport {\n BatchedMigrationJobRowSchema,\n BatchedMigrationJobStatus,\n BatchedMigrationJobRow,\n} from './batched-migration-job.js';\n\nconst sql = loadSqlEquiv(import.meta.filename);\n\ninterface BatchedMigrationRunnerOptions {\n logProgress?: boolean;\n}\n\nexport class BatchedMigrationRunner {\n private options: BatchedMigrationRunnerOptions;\n private migration: BatchedMigrationRow;\n private migrationImplementation: BatchedMigrationImplementation;\n private migrationStatus: BatchedMigrationStatus;\n\n constructor(\n migration: BatchedMigrationRow,\n migrationImplementation: BatchedMigrationImplementation,\n options: BatchedMigrationRunnerOptions = {},\n ) {\n this.options = options;\n this.migration = migration;\n this.migrationImplementation = migrationImplementation;\n this.migrationStatus = migration.status;\n }\n\n private log(message: string, ...meta: any[]) {\n if (this.options.logProgress) {\n logger.info(`[${this.migration.filename}] ${message}`, ...meta);\n }\n }\n\n private async hasIncompleteJobs(migration: BatchedMigrationRow): Promise<boolean> {\n return await queryRow(\n sql.batched_migration_has_incomplete_jobs,\n { batched_migration_id: migration.id },\n z.boolean(),\n );\n }\n\n private async hasFailedJobs(migration: BatchedMigrationRow): Promise<boolean> {\n return await queryRow(\n sql.batched_migration_has_failed_jobs,\n { batched_migration_id: migration.id },\n z.boolean(),\n );\n }\n\n private async refreshMigrationStatus(migration: BatchedMigrationRow) {\n this.migrationStatus = await queryRow(\n sql.get_migration_status,\n {\n id: migration.id,\n },\n BatchedMigrationStatusSchema,\n );\n }\n\n private async finishRunningMigration(migration: BatchedMigrationRow) {\n // Safety check: if there are any pending jobs, don't mark this\n // migration as finished.\n if (await this.hasIncompleteJobs(migration)) {\n this.log(`Incomplete jobs found, not marking as finished`);\n return;\n }\n\n const hasFailedJobs = await this.hasFailedJobs(migration);\n const finalStatus = hasFailedJobs ? 'failed' : 'succeeded';\n await updateBatchedMigrationStatus(migration.id, finalStatus);\n this.log(`Finished with status '${finalStatus}'`);\n }\n\n private async getNextBatchBounds(\n migration: BatchedMigrationRow,\n ): Promise<null | [bigint, bigint]> {\n const lastJob = await queryOptionalRow(\n sql.select_last_batched_migration_job,\n { batched_migration_id: migration.id },\n BatchedMigrationJobRowSchema,\n );\n\n const nextMin = lastJob ? lastJob.max_value + 1n : migration.min_value;\n if (nextMin > migration.max_value) return null;\n\n let nextMax = nextMin + BigInt(migration.batch_size) - 1n;\n if (nextMax > migration.max_value) nextMax = migration.max_value;\n\n return [nextMin, nextMax];\n }\n\n private async startJob(job: BatchedMigrationJobRow) {\n await queryAsync(sql.start_batched_migration_job, { id: job.id });\n const jobRange = `[${job.min_value}, ${job.max_value}]`;\n const migrationRange = `[${this.migration.min_value}, ${this.migration.max_value}]`;\n this.log(`Started job ${job.id} for range ${jobRange} in ${migrationRange}`);\n }\n\n private serializeJobData(data: unknown) {\n if (data == null) return null;\n\n // Return JSON-stringified data. Convert BigInts to strings.\n return JSON.stringify(data, (_key, value) => {\n if (typeof value === 'bigint') return value.toString();\n return value;\n });\n }\n\n private async finishJob(\n job: BatchedMigrationJobRow,\n status: Extract<BatchedMigrationJobStatus, 'failed' | 'succeeded'>,\n data?: unknown,\n ) {\n await queryAsync(sql.finish_batched_migration_job, {\n id: job.id,\n status,\n data: this.serializeJobData(data),\n });\n this.log(`Job ${job.id} finished with status '${status}'`);\n }\n\n private async getOrCreateNextMigrationJob(\n migration: BatchedMigrationRow,\n ): Promise<BatchedMigrationJobRow | null> {\n const nextBatchBounds = await this.getNextBatchBounds(migration);\n if (nextBatchBounds) {\n return await queryRow(\n sql.insert_batched_migration_job,\n {\n batched_migration_id: migration.id,\n min_value: nextBatchBounds[0],\n max_value: nextBatchBounds[1],\n },\n BatchedMigrationJobRowSchema,\n );\n } else {\n // Pick up any old pending jobs from this migration. These will only exist if\n // an admin manually elected to retry all failed jobs; we'll never automatically\n // transition failed jobs back to pending.\n return await queryOptionalRow(\n sql.select_first_pending_batched_migration_job,\n { batched_migration_id: migration.id },\n BatchedMigrationJobRowSchema,\n );\n }\n }\n\n private async runMigrationJob(\n migration: BatchedMigrationRow,\n migrationImplementation: BatchedMigrationImplementation,\n ) {\n const nextJob = await this.getOrCreateNextMigrationJob(migration);\n if (nextJob) {\n await this.startJob(nextJob);\n\n let error = null;\n try {\n // We'll only handle errors thrown by the migration itself. If any of\n // our own execution machinery throws an error, we'll let it bubble up.\n await migrationImplementation.execute(nextJob.min_value, nextJob.max_value);\n } catch (err) {\n error = err;\n }\n\n if (error) {\n logger.error(\n `Error running job ${nextJob.id} for batched migration ${migration.filename}`,\n error,\n );\n await this.finishJob(nextJob, 'failed', { error: serializeError(error) });\n } else {\n await this.finishJob(nextJob, 'succeeded');\n }\n } else {\n await this.finishRunningMigration(migration);\n }\n }\n\n async run({\n signal,\n iterations,\n durationMs,\n }: { signal?: AbortSignal; iterations?: number; durationMs?: number } = {}) {\n let iterationCount = 0;\n const endTime = durationMs ? Date.now() + durationMs : null;\n while (\n !signal?.aborted &&\n (iterations ? iterationCount < iterations : true) &&\n (endTime ? Date.now() < endTime : true) &&\n (this.migrationStatus === 'running' || this.migrationStatus === 'finalizing')\n ) {\n await this.runMigrationJob(this.migration, this.migrationImplementation);\n iterationCount += 1;\n // Always refresh the status so we can detect if the migration was marked\n // as paused by another process.\n await this.refreshMigrationStatus(this.migration);\n }\n }\n}\n"]}
|
|
@@ -1,43 +1,18 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
if (k2 === undefined) k2 = k;
|
|
11
|
-
o[k2] = m[k];
|
|
12
|
-
}));
|
|
13
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
-
}) : function(o, v) {
|
|
16
|
-
o["default"] = v;
|
|
17
|
-
});
|
|
18
|
-
var __importStar = (this && this.__importStar) || function (mod) {
|
|
19
|
-
if (mod && mod.__esModule) return mod;
|
|
20
|
-
var result = {};
|
|
21
|
-
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
22
|
-
__setModuleDefault(result, mod);
|
|
23
|
-
return result;
|
|
24
|
-
};
|
|
25
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
-
const chai_1 = require("chai");
|
|
27
|
-
const postgres_1 = require("@prairielearn/postgres");
|
|
28
|
-
const namedLocks = __importStar(require("@prairielearn/named-locks"));
|
|
29
|
-
const error = __importStar(require("@prairielearn/error"));
|
|
30
|
-
const batched_migration_1 = require("./batched-migration");
|
|
31
|
-
const batched_migration_job_1 = require("./batched-migration-job");
|
|
32
|
-
const batched_migration_runner_1 = require("./batched-migration-runner");
|
|
33
|
-
const index_1 = require("../index");
|
|
34
|
-
const postgresTestUtils = (0, postgres_1.makePostgresTestUtils)({
|
|
1
|
+
import { assert } from 'chai';
|
|
2
|
+
import { makePostgresTestUtils, queryAsync, queryRow, queryRows } from '@prairielearn/postgres';
|
|
3
|
+
import * as namedLocks from '@prairielearn/named-locks';
|
|
4
|
+
import * as error from '@prairielearn/error';
|
|
5
|
+
import { BatchedMigrationRowSchema, insertBatchedMigration, makeBatchedMigration, updateBatchedMigrationStatus, } from './batched-migration.js';
|
|
6
|
+
import { BatchedMigrationJobRowSchema } from './batched-migration-job.js';
|
|
7
|
+
import { BatchedMigrationRunner } from './batched-migration-runner.js';
|
|
8
|
+
import { SCHEMA_MIGRATIONS_PATH, init } from '../index.js';
|
|
9
|
+
const postgresTestUtils = makePostgresTestUtils({
|
|
35
10
|
database: 'prairielearn_migrations',
|
|
36
11
|
});
|
|
37
12
|
function makeTestBatchMigration() {
|
|
38
13
|
let executionCount = 0;
|
|
39
14
|
let failingIds = [];
|
|
40
|
-
return
|
|
15
|
+
return makeBatchedMigration({
|
|
41
16
|
async getParameters() {
|
|
42
17
|
return {
|
|
43
18
|
min: 1n,
|
|
@@ -64,20 +39,20 @@ function makeTestBatchMigration() {
|
|
|
64
39
|
});
|
|
65
40
|
}
|
|
66
41
|
async function getBatchedMigration(migrationId) {
|
|
67
|
-
return await
|
|
42
|
+
return await queryRow('SELECT * FROM batched_migrations WHERE id = $id;', { id: migrationId }, BatchedMigrationRowSchema);
|
|
68
43
|
}
|
|
69
44
|
async function getBatchedMigrationJobs(migrationId) {
|
|
70
|
-
return await
|
|
45
|
+
return await queryRows('SELECT * FROM batched_migration_jobs WHERE batched_migration_id = $batched_migration_id ORDER BY id ASC;', { batched_migration_id: migrationId }, BatchedMigrationJobRowSchema);
|
|
71
46
|
}
|
|
72
47
|
async function resetFailedBatchedMigrationJobs(migrationId) {
|
|
73
|
-
await
|
|
48
|
+
await queryAsync("UPDATE batched_migration_jobs SET status = 'pending', updated_at = CURRENT_TIMESTAMP WHERE batched_migration_id = $batched_migration_id AND status = 'failed'", {
|
|
74
49
|
batched_migration_id: migrationId,
|
|
75
50
|
});
|
|
76
51
|
}
|
|
77
52
|
async function insertTestBatchedMigration() {
|
|
78
53
|
const migrationImplementation = makeTestBatchMigration();
|
|
79
54
|
const parameters = await migrationImplementation.getParameters();
|
|
80
|
-
const migration = await
|
|
55
|
+
const migration = await insertBatchedMigration({
|
|
81
56
|
project: 'test',
|
|
82
57
|
filename: '20230406184103_test_batch_migration.js',
|
|
83
58
|
timestamp: '20230406184103',
|
|
@@ -96,7 +71,7 @@ describe('BatchedMigrationExecutor', () => {
|
|
|
96
71
|
await namedLocks.init(postgresTestUtils.getPoolConfig(), (err) => {
|
|
97
72
|
throw err;
|
|
98
73
|
});
|
|
99
|
-
await
|
|
74
|
+
await init([SCHEMA_MIGRATIONS_PATH], 'prairielearn_migrations');
|
|
100
75
|
});
|
|
101
76
|
beforeEach(async () => {
|
|
102
77
|
await postgresTestUtils.resetDatabase();
|
|
@@ -108,78 +83,78 @@ describe('BatchedMigrationExecutor', () => {
|
|
|
108
83
|
it('runs one iteration of a batched migration', async () => {
|
|
109
84
|
const migration = await insertTestBatchedMigration();
|
|
110
85
|
const migrationImplementation = makeTestBatchMigration();
|
|
111
|
-
const executor = new
|
|
86
|
+
const executor = new BatchedMigrationRunner(migration, migrationImplementation);
|
|
112
87
|
await executor.run({ iterations: 1 });
|
|
113
88
|
const jobs = await getBatchedMigrationJobs(migration.id);
|
|
114
|
-
|
|
89
|
+
assert.lengthOf(jobs, 1);
|
|
115
90
|
const finalMigration = await getBatchedMigration(migration.id);
|
|
116
|
-
|
|
117
|
-
|
|
91
|
+
assert.equal(finalMigration.status, 'running');
|
|
92
|
+
assert.equal(migrationImplementation.executionCount, 1);
|
|
118
93
|
});
|
|
119
94
|
it('runs an entire batched migration', async () => {
|
|
120
95
|
const migration = await insertTestBatchedMigration();
|
|
121
96
|
const migrationImplementation = makeTestBatchMigration();
|
|
122
|
-
const runner = new
|
|
97
|
+
const runner = new BatchedMigrationRunner(migration, migrationImplementation);
|
|
123
98
|
await runner.run();
|
|
124
99
|
const jobs = await getBatchedMigrationJobs(migration.id);
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
100
|
+
assert.lengthOf(jobs, 10);
|
|
101
|
+
assert.equal(jobs[0].min_value, 1n);
|
|
102
|
+
assert.equal(jobs[0].max_value, 1000n);
|
|
103
|
+
assert.equal(jobs.at(-1)?.min_value, 9001n);
|
|
104
|
+
assert.equal(jobs.at(-1)?.max_value, 10000n);
|
|
105
|
+
assert.isTrue(jobs.every((job) => job.started_at !== null));
|
|
106
|
+
assert.isTrue(jobs.every((job) => job.finished_at !== null));
|
|
107
|
+
assert.isTrue(jobs.every((job) => job.status === 'succeeded'));
|
|
108
|
+
assert.isTrue(jobs.every((job) => job.attempts === 1));
|
|
134
109
|
const finalMigration = await getBatchedMigration(migration.id);
|
|
135
|
-
|
|
110
|
+
assert.equal(finalMigration.status, 'succeeded');
|
|
136
111
|
});
|
|
137
112
|
it('handles failing execution', async () => {
|
|
138
113
|
let migration = await insertTestBatchedMigration();
|
|
139
114
|
const migrationImplementation = makeTestBatchMigration();
|
|
140
115
|
migrationImplementation.setFailingIds([1n, 5010n]);
|
|
141
|
-
const runner = new
|
|
116
|
+
const runner = new BatchedMigrationRunner(migration, migrationImplementation);
|
|
142
117
|
await runner.run();
|
|
143
118
|
const jobs = await getBatchedMigrationJobs(migration.id);
|
|
144
119
|
const failedJobs = jobs.filter((job) => job.status === 'failed');
|
|
145
120
|
const successfulJobs = jobs.filter((job) => job.status === 'succeeded');
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
121
|
+
assert.lengthOf(jobs, 10);
|
|
122
|
+
assert.lengthOf(failedJobs, 2);
|
|
123
|
+
assert.lengthOf(successfulJobs, 8);
|
|
124
|
+
assert.equal(migrationImplementation.executionCount, 10);
|
|
125
|
+
assert.isTrue(jobs.every((job) => job.attempts === 1));
|
|
151
126
|
failedJobs.forEach((job) => {
|
|
152
127
|
const jobData = job.data;
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
128
|
+
assert.isObject(jobData);
|
|
129
|
+
assert.isObject(jobData.error);
|
|
130
|
+
assert.hasAllKeys(jobData.error, ['name', 'message', 'stack', 'data', 'status']);
|
|
131
|
+
assert.equal(jobData.error.name, 'Error');
|
|
132
|
+
assert.equal(jobData.error.message, 'Execution failure');
|
|
133
|
+
assert.equal(jobData.error.data.start, job.min_value.toString());
|
|
134
|
+
assert.equal(jobData.error.data.end, job.max_value.toString());
|
|
160
135
|
});
|
|
161
136
|
const failedMigration = await getBatchedMigration(migration.id);
|
|
162
|
-
|
|
137
|
+
assert.equal(failedMigration.status, 'failed');
|
|
163
138
|
// Retry the failed jobs; ensure they succeed this time.
|
|
164
139
|
await resetFailedBatchedMigrationJobs(migration.id);
|
|
165
|
-
migration = await
|
|
140
|
+
migration = await updateBatchedMigrationStatus(migration.id, 'running');
|
|
166
141
|
migrationImplementation.setFailingIds([]);
|
|
167
|
-
const retryRunner = new
|
|
142
|
+
const retryRunner = new BatchedMigrationRunner(migration, migrationImplementation);
|
|
168
143
|
await retryRunner.run();
|
|
169
144
|
const finalJobs = await getBatchedMigrationJobs(migration.id);
|
|
170
145
|
const finalFailedJobs = finalJobs.filter((job) => job.status === 'failed');
|
|
171
146
|
const finalSuccessfulJobs = finalJobs.filter((job) => job.status === 'succeeded');
|
|
172
147
|
const retriedJobs = finalJobs.filter((job) => job.attempts === 2);
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
148
|
+
assert.lengthOf(finalJobs, 10);
|
|
149
|
+
assert.lengthOf(finalFailedJobs, 0);
|
|
150
|
+
assert.lengthOf(finalSuccessfulJobs, 10);
|
|
151
|
+
assert.lengthOf(retriedJobs, 2);
|
|
152
|
+
assert.isTrue(finalJobs.every((job) => job.data === null));
|
|
178
153
|
migration = await getBatchedMigration(migration.id);
|
|
179
|
-
|
|
154
|
+
assert.equal(migration.status, 'succeeded');
|
|
180
155
|
// The runner should have run only the previously failed jobs, which
|
|
181
156
|
// works out to 2 additional execution.
|
|
182
|
-
|
|
157
|
+
assert.equal(migrationImplementation.executionCount, 12);
|
|
183
158
|
});
|
|
184
159
|
});
|
|
185
160
|
//# sourceMappingURL=batched-migration-runner.test.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"batched-migration-runner.test.js","sourceRoot":"","sources":["../../src/batched-migrations/batched-migration-runner.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { assert } from 'chai';\nimport { makePostgresTestUtils, queryAsync, queryRow, queryRows } from '@prairielearn/postgres';\nimport * as namedLocks from '@prairielearn/named-locks';\nimport * as error from '@prairielearn/error';\n\nimport {\n BatchedMigrationRowSchema,\n insertBatchedMigration,\n makeBatchedMigration,\n updateBatchedMigrationStatus,\n} from './batched-migration';\nimport { BatchedMigrationJobRowSchema } from './batched-migration-job';\nimport { BatchedMigrationRunner } from './batched-migration-runner';\nimport { SCHEMA_MIGRATIONS_PATH, init } from '../index';\n\nconst postgresTestUtils = makePostgresTestUtils({\n database: 'prairielearn_migrations',\n});\n\nfunction makeTestBatchMigration() {\n let executionCount = 0;\n let failingIds: bigint[] = [];\n\n return makeBatchedMigration({\n async getParameters() {\n return {\n min: 1n,\n max: 10000n,\n batchSize: 1000,\n };\n },\n async execute(start: bigint, end: bigint) {\n executionCount += 1;\n const shouldFail = failingIds.some((id) => id >= start && id <= end);\n if (shouldFail) {\n // Throw an error with some data to make sure it gets persisted. We\n // specifically use BigInt values here to make sure that they are\n // correctly serialized to strings.\n throw new error.AugmentedError('Execution failure', { data: { start, end } });\n }\n },\n setFailingIds(ids: bigint[]) {\n failingIds = ids;\n },\n get executionCount() {\n return executionCount;\n },\n });\n}\n\nasync function getBatchedMigration(migrationId: string) {\n return await queryRow(\n 'SELECT * FROM batched_migrations WHERE id = $id;',\n { id: migrationId },\n BatchedMigrationRowSchema,\n );\n}\n\nasync function getBatchedMigrationJobs(migrationId: string) {\n return await queryRows(\n 'SELECT * FROM batched_migration_jobs WHERE batched_migration_id = $batched_migration_id ORDER BY id ASC;',\n { batched_migration_id: migrationId },\n BatchedMigrationJobRowSchema,\n );\n}\n\nasync function resetFailedBatchedMigrationJobs(migrationId: string) {\n await queryAsync(\n \"UPDATE batched_migration_jobs SET status = 'pending', updated_at = CURRENT_TIMESTAMP WHERE batched_migration_id = $batched_migration_id AND status = 'failed'\",\n {\n batched_migration_id: migrationId,\n },\n );\n}\n\nasync function insertTestBatchedMigration() {\n const migrationImplementation = makeTestBatchMigration();\n const parameters = await migrationImplementation.getParameters();\n const migration = await insertBatchedMigration({\n project: 'test',\n filename: '20230406184103_test_batch_migration.js',\n timestamp: '20230406184103',\n batch_size: parameters.batchSize,\n min_value: parameters.min,\n max_value: parameters.max,\n status: 'running',\n });\n if (!migration) throw new Error('Failed to insert batched migration');\n return migration;\n}\n\ndescribe('BatchedMigrationExecutor', () => {\n before(async () => {\n await postgresTestUtils.createDatabase();\n await namedLocks.init(postgresTestUtils.getPoolConfig(), (err) => {\n throw err;\n });\n await init([SCHEMA_MIGRATIONS_PATH], 'prairielearn_migrations');\n });\n\n beforeEach(async () => {\n await postgresTestUtils.resetDatabase();\n });\n\n after(async () => {\n await namedLocks.close();\n await postgresTestUtils.dropDatabase();\n });\n\n it('runs one iteration of a batched migration', async () => {\n const migration = await insertTestBatchedMigration();\n\n const migrationImplementation = makeTestBatchMigration();\n const executor = new BatchedMigrationRunner(migration, migrationImplementation);\n await executor.run({ iterations: 1 });\n\n const jobs = await getBatchedMigrationJobs(migration.id);\n assert.lengthOf(jobs, 1);\n\n const finalMigration = await getBatchedMigration(migration.id);\n assert.equal(finalMigration.status, 'running');\n\n assert.equal(migrationImplementation.executionCount, 1);\n });\n\n it('runs an entire batched migration', async () => {\n const migration = await insertTestBatchedMigration();\n\n const migrationImplementation = makeTestBatchMigration();\n const runner = new BatchedMigrationRunner(migration, migrationImplementation);\n await runner.run();\n\n const jobs = await getBatchedMigrationJobs(migration.id);\n assert.lengthOf(jobs, 10);\n assert.equal(jobs[0].min_value, 1n);\n assert.equal(jobs[0].max_value, 1000n);\n assert.equal(jobs.at(-1)?.min_value, 9001n);\n assert.equal(jobs.at(-1)?.max_value, 10000n);\n assert.isTrue(jobs.every((job) => job.started_at !== null));\n assert.isTrue(jobs.every((job) => job.finished_at !== null));\n assert.isTrue(jobs.every((job) => job.status === 'succeeded'));\n assert.isTrue(jobs.every((job) => job.attempts === 1));\n\n const finalMigration = await getBatchedMigration(migration.id);\n assert.equal(finalMigration.status, 'succeeded');\n });\n\n it('handles failing execution', async () => {\n let migration = await insertTestBatchedMigration();\n\n const migrationImplementation = makeTestBatchMigration();\n migrationImplementation.setFailingIds([1n, 5010n]);\n const runner = new BatchedMigrationRunner(migration, migrationImplementation);\n await runner.run();\n\n const jobs = await getBatchedMigrationJobs(migration.id);\n const failedJobs = jobs.filter((job) => job.status === 'failed');\n const successfulJobs = jobs.filter((job) => job.status === 'succeeded');\n assert.lengthOf(jobs, 10);\n assert.lengthOf(failedJobs, 2);\n assert.lengthOf(successfulJobs, 8);\n assert.equal(migrationImplementation.executionCount, 10);\n assert.isTrue(jobs.every((job) => job.attempts === 1));\n failedJobs.forEach((job) => {\n const jobData = job.data as any;\n assert.isObject(jobData);\n assert.isObject(jobData.error);\n assert.hasAllKeys(jobData.error, ['name', 'message', 'stack', 'data', 'status']);\n assert.equal(jobData.error.name, 'Error');\n assert.equal(jobData.error.message, 'Execution failure');\n assert.equal(jobData.error.data.start, job.min_value.toString());\n assert.equal(jobData.error.data.end, job.max_value.toString());\n });\n\n const failedMigration = await getBatchedMigration(migration.id);\n assert.equal(failedMigration.status, 'failed');\n\n // Retry the failed jobs; ensure they succeed this time.\n await resetFailedBatchedMigrationJobs(migration.id);\n migration = await updateBatchedMigrationStatus(migration.id, 'running');\n\n migrationImplementation.setFailingIds([]);\n const retryRunner = new BatchedMigrationRunner(migration, migrationImplementation);\n await retryRunner.run();\n\n const finalJobs = await getBatchedMigrationJobs(migration.id);\n const finalFailedJobs = finalJobs.filter((job) => job.status === 'failed');\n const finalSuccessfulJobs = finalJobs.filter((job) => job.status === 'succeeded');\n const retriedJobs = finalJobs.filter((job) => job.attempts === 2);\n assert.lengthOf(finalJobs, 10);\n assert.lengthOf(finalFailedJobs, 0);\n assert.lengthOf(finalSuccessfulJobs, 10);\n assert.lengthOf(retriedJobs, 2);\n assert.isTrue(finalJobs.every((job) => job.data === null));\n\n migration = await getBatchedMigration(migration.id);\n assert.equal(migration.status, 'succeeded');\n\n // The runner should have run only the previously failed jobs, which\n // works out to 2 additional execution.\n assert.equal(migrationImplementation.executionCount, 12);\n });\n});\n"]}
|
|
1
|
+
{"version":3,"file":"batched-migration-runner.test.js","sourceRoot":"","sources":["../../src/batched-migrations/batched-migration-runner.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { assert } from 'chai';\nimport { makePostgresTestUtils, queryAsync, queryRow, queryRows } from '@prairielearn/postgres';\nimport * as namedLocks from '@prairielearn/named-locks';\nimport * as error from '@prairielearn/error';\n\nimport {\n BatchedMigrationRowSchema,\n insertBatchedMigration,\n makeBatchedMigration,\n updateBatchedMigrationStatus,\n} from './batched-migration.js';\nimport { BatchedMigrationJobRowSchema } from './batched-migration-job.js';\nimport { BatchedMigrationRunner } from './batched-migration-runner.js';\nimport { SCHEMA_MIGRATIONS_PATH, init } from '../index.js';\n\nconst postgresTestUtils = makePostgresTestUtils({\n database: 'prairielearn_migrations',\n});\n\nfunction makeTestBatchMigration() {\n let executionCount = 0;\n let failingIds: bigint[] = [];\n\n return makeBatchedMigration({\n async getParameters() {\n return {\n min: 1n,\n max: 10000n,\n batchSize: 1000,\n };\n },\n async execute(start: bigint, end: bigint) {\n executionCount += 1;\n const shouldFail = failingIds.some((id) => id >= start && id <= end);\n if (shouldFail) {\n // Throw an error with some data to make sure it gets persisted. We\n // specifically use BigInt values here to make sure that they are\n // correctly serialized to strings.\n throw new error.AugmentedError('Execution failure', { data: { start, end } });\n }\n },\n setFailingIds(ids: bigint[]) {\n failingIds = ids;\n },\n get executionCount() {\n return executionCount;\n },\n });\n}\n\nasync function getBatchedMigration(migrationId: string) {\n return await queryRow(\n 'SELECT * FROM batched_migrations WHERE id = $id;',\n { id: migrationId },\n BatchedMigrationRowSchema,\n );\n}\n\nasync function getBatchedMigrationJobs(migrationId: string) {\n return await queryRows(\n 'SELECT * FROM batched_migration_jobs WHERE batched_migration_id = $batched_migration_id ORDER BY id ASC;',\n { batched_migration_id: migrationId },\n BatchedMigrationJobRowSchema,\n );\n}\n\nasync function resetFailedBatchedMigrationJobs(migrationId: string) {\n await queryAsync(\n \"UPDATE batched_migration_jobs SET status = 'pending', updated_at = CURRENT_TIMESTAMP WHERE batched_migration_id = $batched_migration_id AND status = 'failed'\",\n {\n batched_migration_id: migrationId,\n },\n );\n}\n\nasync function insertTestBatchedMigration() {\n const migrationImplementation = makeTestBatchMigration();\n const parameters = await migrationImplementation.getParameters();\n const migration = await insertBatchedMigration({\n project: 'test',\n filename: '20230406184103_test_batch_migration.js',\n timestamp: '20230406184103',\n batch_size: parameters.batchSize,\n min_value: parameters.min,\n max_value: parameters.max,\n status: 'running',\n });\n if (!migration) throw new Error('Failed to insert batched migration');\n return migration;\n}\n\ndescribe('BatchedMigrationExecutor', () => {\n before(async () => {\n await postgresTestUtils.createDatabase();\n await namedLocks.init(postgresTestUtils.getPoolConfig(), (err) => {\n throw err;\n });\n await init([SCHEMA_MIGRATIONS_PATH], 'prairielearn_migrations');\n });\n\n beforeEach(async () => {\n await postgresTestUtils.resetDatabase();\n });\n\n after(async () => {\n await namedLocks.close();\n await postgresTestUtils.dropDatabase();\n });\n\n it('runs one iteration of a batched migration', async () => {\n const migration = await insertTestBatchedMigration();\n\n const migrationImplementation = makeTestBatchMigration();\n const executor = new BatchedMigrationRunner(migration, migrationImplementation);\n await executor.run({ iterations: 1 });\n\n const jobs = await getBatchedMigrationJobs(migration.id);\n assert.lengthOf(jobs, 1);\n\n const finalMigration = await getBatchedMigration(migration.id);\n assert.equal(finalMigration.status, 'running');\n\n assert.equal(migrationImplementation.executionCount, 1);\n });\n\n it('runs an entire batched migration', async () => {\n const migration = await insertTestBatchedMigration();\n\n const migrationImplementation = makeTestBatchMigration();\n const runner = new BatchedMigrationRunner(migration, migrationImplementation);\n await runner.run();\n\n const jobs = await getBatchedMigrationJobs(migration.id);\n assert.lengthOf(jobs, 10);\n assert.equal(jobs[0].min_value, 1n);\n assert.equal(jobs[0].max_value, 1000n);\n assert.equal(jobs.at(-1)?.min_value, 9001n);\n assert.equal(jobs.at(-1)?.max_value, 10000n);\n assert.isTrue(jobs.every((job) => job.started_at !== null));\n assert.isTrue(jobs.every((job) => job.finished_at !== null));\n assert.isTrue(jobs.every((job) => job.status === 'succeeded'));\n assert.isTrue(jobs.every((job) => job.attempts === 1));\n\n const finalMigration = await getBatchedMigration(migration.id);\n assert.equal(finalMigration.status, 'succeeded');\n });\n\n it('handles failing execution', async () => {\n let migration = await insertTestBatchedMigration();\n\n const migrationImplementation = makeTestBatchMigration();\n migrationImplementation.setFailingIds([1n, 5010n]);\n const runner = new BatchedMigrationRunner(migration, migrationImplementation);\n await runner.run();\n\n const jobs = await getBatchedMigrationJobs(migration.id);\n const failedJobs = jobs.filter((job) => job.status === 'failed');\n const successfulJobs = jobs.filter((job) => job.status === 'succeeded');\n assert.lengthOf(jobs, 10);\n assert.lengthOf(failedJobs, 2);\n assert.lengthOf(successfulJobs, 8);\n assert.equal(migrationImplementation.executionCount, 10);\n assert.isTrue(jobs.every((job) => job.attempts === 1));\n failedJobs.forEach((job) => {\n const jobData = job.data as any;\n assert.isObject(jobData);\n assert.isObject(jobData.error);\n assert.hasAllKeys(jobData.error, ['name', 'message', 'stack', 'data', 'status']);\n assert.equal(jobData.error.name, 'Error');\n assert.equal(jobData.error.message, 'Execution failure');\n assert.equal(jobData.error.data.start, job.min_value.toString());\n assert.equal(jobData.error.data.end, job.max_value.toString());\n });\n\n const failedMigration = await getBatchedMigration(migration.id);\n assert.equal(failedMigration.status, 'failed');\n\n // Retry the failed jobs; ensure they succeed this time.\n await resetFailedBatchedMigrationJobs(migration.id);\n migration = await updateBatchedMigrationStatus(migration.id, 'running');\n\n migrationImplementation.setFailingIds([]);\n const retryRunner = new BatchedMigrationRunner(migration, migrationImplementation);\n await retryRunner.run();\n\n const finalJobs = await getBatchedMigrationJobs(migration.id);\n const finalFailedJobs = finalJobs.filter((job) => job.status === 'failed');\n const finalSuccessfulJobs = finalJobs.filter((job) => job.status === 'succeeded');\n const retriedJobs = finalJobs.filter((job) => job.attempts === 2);\n assert.lengthOf(finalJobs, 10);\n assert.lengthOf(finalFailedJobs, 0);\n assert.lengthOf(finalSuccessfulJobs, 10);\n assert.lengthOf(retriedJobs, 2);\n assert.isTrue(finalJobs.every((job) => job.data === null));\n\n migration = await getBatchedMigration(migration.id);\n assert.equal(migration.status, 'succeeded');\n\n // The runner should have run only the previously failed jobs, which\n // works out to 2 additional execution.\n assert.equal(migrationImplementation.executionCount, 12);\n });\n});\n"]}
|