@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.
Files changed (60) hide show
  1. package/.mocharc.cjs +3 -0
  2. package/CHANGELOG.md +14 -0
  3. package/dist/batched-migrations/batched-migration-job.js +18 -22
  4. package/dist/batched-migrations/batched-migration-job.js.map +1 -1
  5. package/dist/batched-migrations/batched-migration-runner.d.ts +2 -2
  6. package/dist/batched-migrations/batched-migration-runner.js +22 -26
  7. package/dist/batched-migrations/batched-migration-runner.js.map +1 -1
  8. package/dist/batched-migrations/batched-migration-runner.test.js +53 -78
  9. package/dist/batched-migrations/batched-migration-runner.test.js.map +1 -1
  10. package/dist/batched-migrations/batched-migration.js +30 -41
  11. package/dist/batched-migrations/batched-migration.js.map +1 -1
  12. package/dist/batched-migrations/batched-migrations-runner.d.ts +1 -1
  13. package/dist/batched-migrations/batched-migrations-runner.js +32 -67
  14. package/dist/batched-migrations/batched-migrations-runner.js.map +1 -1
  15. package/dist/batched-migrations/batched-migrations-runner.test.js +41 -69
  16. package/dist/batched-migrations/batched-migrations-runner.test.js.map +1 -1
  17. package/dist/batched-migrations/fixtures/20230406184103_successful_migration.js +2 -4
  18. package/dist/batched-migrations/fixtures/20230406184103_successful_migration.js.map +1 -1
  19. package/dist/batched-migrations/fixtures/20230406184107_failing_migration.d.ts +9 -0
  20. package/dist/batched-migrations/fixtures/20230406184107_failing_migration.js +14 -0
  21. package/dist/batched-migrations/fixtures/20230406184107_failing_migration.js.map +1 -0
  22. package/dist/batched-migrations/fixtures/20230407230446_no_rows_migration.js +2 -4
  23. package/dist/batched-migrations/fixtures/20230407230446_no_rows_migration.js.map +1 -1
  24. package/dist/batched-migrations/index.d.ts +3 -3
  25. package/dist/batched-migrations/index.js +3 -17
  26. package/dist/batched-migrations/index.js.map +1 -1
  27. package/dist/index.d.ts +2 -2
  28. package/dist/index.js +4 -22
  29. package/dist/index.js.map +1 -1
  30. package/dist/load-migrations.js +6 -16
  31. package/dist/load-migrations.js.map +1 -1
  32. package/dist/load-migrations.test.js +16 -44
  33. package/dist/load-migrations.test.js.map +1 -1
  34. package/dist/migrations/fixtures/20230407210430_insert_user.js +3 -6
  35. package/dist/migrations/fixtures/20230407210430_insert_user.js.map +1 -1
  36. package/dist/migrations/index.d.ts +1 -1
  37. package/dist/migrations/index.js +1 -5
  38. package/dist/migrations/index.js.map +1 -1
  39. package/dist/migrations/migrations.d.ts +1 -1
  40. package/dist/migrations/migrations.js +25 -57
  41. package/dist/migrations/migrations.js.map +1 -1
  42. package/dist/migrations/migrations.test.js +12 -17
  43. package/dist/migrations/migrations.test.js.map +1 -1
  44. package/package.json +8 -7
  45. package/src/batched-migrations/batched-migration-job.ts +1 -1
  46. package/src/batched-migrations/batched-migration-runner.test.ts +4 -4
  47. package/src/batched-migrations/batched-migration-runner.ts +3 -3
  48. package/src/batched-migrations/batched-migration.ts +1 -1
  49. package/src/batched-migrations/batched-migrations-runner.test.ts +8 -8
  50. package/src/batched-migrations/batched-migrations-runner.ts +4 -4
  51. package/src/batched-migrations/fixtures/20230406184103_successful_migration.ts +1 -1
  52. package/src/batched-migrations/fixtures/{20230406184107_failing_migration.js → 20230406184107_failing_migration.ts} +2 -3
  53. package/src/batched-migrations/fixtures/20230407230446_no_rows_migration.ts +1 -1
  54. package/src/batched-migrations/index.ts +7 -7
  55. package/src/index.ts +7 -7
  56. package/src/load-migrations.test.ts +1 -1
  57. package/src/migrations/index.ts +1 -1
  58. package/src/migrations/migrations.test.ts +2 -2
  59. package/src/migrations/migrations.ts +2 -2
  60. package/tsconfig.json +4 -1
package/.mocharc.cjs ADDED
@@ -0,0 +1,3 @@
1
+ module.exports = {
2
+ 'node-option': ['import=tsx/esm'],
3
+ };
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
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.selectRecentJobsWithStatus = exports.BatchedMigrationJobRowSchema = exports.BatchedMigrationJobStatusSchema = void 0;
4
- const zod_1 = require("zod");
5
- const postgres_1 = require("@prairielearn/postgres");
6
- const sql = (0, postgres_1.loadSqlEquiv)(__filename);
7
- exports.BatchedMigrationJobStatusSchema = zod_1.z.enum(['pending', 'failed', 'succeeded']);
8
- exports.BatchedMigrationJobRowSchema = zod_1.z.object({
9
- id: zod_1.z.string(),
10
- batched_migration_id: zod_1.z.string(),
11
- min_value: zod_1.z.bigint({ coerce: true }),
12
- max_value: zod_1.z.bigint({ coerce: true }),
13
- status: exports.BatchedMigrationJobStatusSchema,
14
- attempts: zod_1.z.number(),
15
- created_at: zod_1.z.date(),
16
- updated_at: zod_1.z.date(),
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 (0, postgres_1.queryRows)(sql.select_recent_jobs_with_status, { batched_migration_id: batchedMigrationId, status, limit }, exports.BatchedMigrationJobRowSchema);
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":";;;AAAA,6BAAwB;AACxB,qDAAiE;AAEjE,MAAM,GAAG,GAAG,IAAA,uBAAY,EAAC,UAAU,CAAC,CAAC;AAExB,QAAA,+BAA+B,GAAG,OAAC,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,QAAQ,EAAE,WAAW,CAAC,CAAC,CAAC;AAG7E,QAAA,4BAA4B,GAAG,OAAC,CAAC,MAAM,CAAC;IACnD,EAAE,EAAE,OAAC,CAAC,MAAM,EAAE;IACd,oBAAoB,EAAE,OAAC,CAAC,MAAM,EAAE;IAChC,SAAS,EAAE,OAAC,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;IACrC,SAAS,EAAE,OAAC,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;IACrC,MAAM,EAAE,uCAA+B;IACvC,QAAQ,EAAE,OAAC,CAAC,MAAM,EAAE;IACpB,UAAU,EAAE,OAAC,CAAC,IAAI,EAAE;IACpB,UAAU,EAAE,OAAC,CAAC,IAAI,EAAE;IACpB,UAAU,EAAE,OAAC,CAAC,IAAI,EAAE,CAAC,QAAQ,EAAE;IAC/B,WAAW,EAAE,OAAC,CAAC,IAAI,EAAE,CAAC,QAAQ,EAAE;IAChC,IAAI,EAAE,OAAC,CAAC,OAAO,EAAE;CAClB,CAAC,CAAC;AAGI,KAAK,UAAU,0BAA0B,CAC9C,kBAA0B,EAC1B,MAAiC,EACjC,KAAa;IAEb,OAAO,MAAM,IAAA,oBAAS,EACpB,GAAG,CAAC,8BAA8B,EAClC,EAAE,oBAAoB,EAAE,kBAAkB,EAAE,MAAM,EAAE,KAAK,EAAE,EAC3D,oCAA4B,CAC7B,CAAC;AACJ,CAAC;AAVD,gEAUC","sourcesContent":["import { z } from 'zod';\nimport { loadSqlEquiv, queryRows } from '@prairielearn/postgres';\n\nconst sql = loadSqlEquiv(__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
+ {"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
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.BatchedMigrationRunner = void 0;
4
- const postgres_1 = require("@prairielearn/postgres");
5
- const logger_1 = require("@prairielearn/logger");
6
- const serialize_error_1 = require("serialize-error");
7
- const zod_1 = require("zod");
8
- const batched_migration_1 = require("./batched-migration");
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
- logger_1.logger.info(`[${this.migration.filename}] ${message}`, ...meta);
21
+ logger.info(`[${this.migration.filename}] ${message}`, ...meta);
25
22
  }
26
23
  }
27
24
  async hasIncompleteJobs(migration) {
28
- return await (0, postgres_1.queryRow)(sql.batched_migration_has_incomplete_jobs, { batched_migration_id: migration.id }, zod_1.z.boolean());
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 (0, postgres_1.queryRow)(sql.batched_migration_has_failed_jobs, { batched_migration_id: migration.id }, zod_1.z.boolean());
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 (0, postgres_1.queryRow)(sql.get_migration_status, {
31
+ this.migrationStatus = await queryRow(sql.get_migration_status, {
35
32
  id: migration.id,
36
- }, batched_migration_1.BatchedMigrationStatusSchema);
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 (0, batched_migration_1.updateBatchedMigrationStatus)(migration.id, finalStatus);
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 (0, postgres_1.queryOptionalRow)(sql.select_last_batched_migration_job, { batched_migration_id: migration.id }, batched_migration_job_1.BatchedMigrationJobRowSchema);
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 (0, postgres_1.queryAsync)(sql.start_batched_migration_job, { id: job.id });
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 (0, postgres_1.queryAsync)(sql.finish_batched_migration_job, {
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 (0, postgres_1.queryRow)(sql.insert_batched_migration_job, {
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
- }, batched_migration_job_1.BatchedMigrationJobRowSchema);
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 (0, postgres_1.queryOptionalRow)(sql.select_first_pending_batched_migration_job, { batched_migration_id: migration.id }, batched_migration_job_1.BatchedMigrationJobRowSchema);
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
- logger_1.logger.error(`Error running job ${nextJob.id} for batched migration ${migration.filename}`, error);
115
- await this.finishJob(nextJob, 'failed', { error: (0, serialize_error_1.serializeError)(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
- "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
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 (0, batched_migration_1.makeBatchedMigration)({
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 (0, postgres_1.queryRow)('SELECT * FROM batched_migrations WHERE id = $id;', { id: migrationId }, batched_migration_1.BatchedMigrationRowSchema);
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 (0, postgres_1.queryRows)('SELECT * FROM batched_migration_jobs WHERE batched_migration_id = $batched_migration_id ORDER BY id ASC;', { batched_migration_id: migrationId }, batched_migration_job_1.BatchedMigrationJobRowSchema);
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 (0, postgres_1.queryAsync)("UPDATE batched_migration_jobs SET status = 'pending', updated_at = CURRENT_TIMESTAMP WHERE batched_migration_id = $batched_migration_id AND status = 'failed'", {
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 (0, batched_migration_1.insertBatchedMigration)({
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 (0, index_1.init)([index_1.SCHEMA_MIGRATIONS_PATH], 'prairielearn_migrations');
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 batched_migration_runner_1.BatchedMigrationRunner(migration, migrationImplementation);
86
+ const executor = new BatchedMigrationRunner(migration, migrationImplementation);
112
87
  await executor.run({ iterations: 1 });
113
88
  const jobs = await getBatchedMigrationJobs(migration.id);
114
- chai_1.assert.lengthOf(jobs, 1);
89
+ assert.lengthOf(jobs, 1);
115
90
  const finalMigration = await getBatchedMigration(migration.id);
116
- chai_1.assert.equal(finalMigration.status, 'running');
117
- chai_1.assert.equal(migrationImplementation.executionCount, 1);
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 batched_migration_runner_1.BatchedMigrationRunner(migration, migrationImplementation);
97
+ const runner = new BatchedMigrationRunner(migration, migrationImplementation);
123
98
  await runner.run();
124
99
  const jobs = await getBatchedMigrationJobs(migration.id);
125
- chai_1.assert.lengthOf(jobs, 10);
126
- chai_1.assert.equal(jobs[0].min_value, 1n);
127
- chai_1.assert.equal(jobs[0].max_value, 1000n);
128
- chai_1.assert.equal(jobs.at(-1)?.min_value, 9001n);
129
- chai_1.assert.equal(jobs.at(-1)?.max_value, 10000n);
130
- chai_1.assert.isTrue(jobs.every((job) => job.started_at !== null));
131
- chai_1.assert.isTrue(jobs.every((job) => job.finished_at !== null));
132
- chai_1.assert.isTrue(jobs.every((job) => job.status === 'succeeded'));
133
- chai_1.assert.isTrue(jobs.every((job) => job.attempts === 1));
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
- chai_1.assert.equal(finalMigration.status, 'succeeded');
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 batched_migration_runner_1.BatchedMigrationRunner(migration, migrationImplementation);
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
- chai_1.assert.lengthOf(jobs, 10);
147
- chai_1.assert.lengthOf(failedJobs, 2);
148
- chai_1.assert.lengthOf(successfulJobs, 8);
149
- chai_1.assert.equal(migrationImplementation.executionCount, 10);
150
- chai_1.assert.isTrue(jobs.every((job) => job.attempts === 1));
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
- chai_1.assert.isObject(jobData);
154
- chai_1.assert.isObject(jobData.error);
155
- chai_1.assert.hasAllKeys(jobData.error, ['name', 'message', 'stack', 'data', 'status']);
156
- chai_1.assert.equal(jobData.error.name, 'Error');
157
- chai_1.assert.equal(jobData.error.message, 'Execution failure');
158
- chai_1.assert.equal(jobData.error.data.start, job.min_value.toString());
159
- chai_1.assert.equal(jobData.error.data.end, job.max_value.toString());
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
- chai_1.assert.equal(failedMigration.status, 'failed');
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 (0, batched_migration_1.updateBatchedMigrationStatus)(migration.id, 'running');
140
+ migration = await updateBatchedMigrationStatus(migration.id, 'running');
166
141
  migrationImplementation.setFailingIds([]);
167
- const retryRunner = new batched_migration_runner_1.BatchedMigrationRunner(migration, migrationImplementation);
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
- chai_1.assert.lengthOf(finalJobs, 10);
174
- chai_1.assert.lengthOf(finalFailedJobs, 0);
175
- chai_1.assert.lengthOf(finalSuccessfulJobs, 10);
176
- chai_1.assert.lengthOf(retriedJobs, 2);
177
- chai_1.assert.isTrue(finalJobs.every((job) => job.data === null));
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
- chai_1.assert.equal(migration.status, 'succeeded');
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
- chai_1.assert.equal(migrationImplementation.executionCount, 12);
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"]}