db-migrate 1.2.0 → 1.4.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/README.md +9 -8
- package/api.js +27 -46
- package/lib/commands/generated.js +3 -1
- package/lib/commands/helper/seeder.js +29 -0
- package/lib/commands/run.js +11 -13
- package/lib/commands/seed.js +5 -9
- package/lib/commands/set-default-argv.js +4 -1
- package/lib/commands/undo-seed.js +5 -9
- package/lib/commands/work.js +33 -0
- package/lib/dml.js +1115 -0
- package/lib/driver/index.js +2 -0
- package/lib/executors/versioned/v2.js +18 -78
- package/lib/jobs.js +177 -0
- package/lib/recovery.js +83 -0
- package/lib/seed.js +199 -0
- package/lib/walker.js +51 -3
- package/lib/work.js +260 -0
- package/package.json +4 -4
package/lib/work.js
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const Promise = require('bluebird');
|
|
6
|
+
const log = require('db-migrate-shared').log;
|
|
7
|
+
const State = require('./state');
|
|
8
|
+
const File = require('./file');
|
|
9
|
+
const Jobs = require('./jobs');
|
|
10
|
+
const Dml = require('./dml');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Runs the jobs of background migrations, see Jobs.
|
|
14
|
+
*
|
|
15
|
+
* parallel: jobs run at once by this worker, each on connections of its own
|
|
16
|
+
* pause: ms to wait between two batches of a job
|
|
17
|
+
* batch: rows per batch, unless the instruction sets its own
|
|
18
|
+
* interval: ms between looking for new jobs
|
|
19
|
+
* timeout: ms after which a job of a worker which stopped responding is
|
|
20
|
+
* taken over, the heartbeat renews it every third of it
|
|
21
|
+
* watch: keep looking for new jobs, instead of ending once none is left
|
|
22
|
+
*
|
|
23
|
+
* Returns { done, stop }. done resolves with the jobs done and failed, once
|
|
24
|
+
* no job is left, or after stop() was called. stop() lets the running jobs
|
|
25
|
+
* stop after their current batch, they are continued later.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
const DEFAULTS = {
|
|
29
|
+
parallel: 1,
|
|
30
|
+
pause: 0,
|
|
31
|
+
batch: undefined,
|
|
32
|
+
interval: 5000,
|
|
33
|
+
timeout: 60000,
|
|
34
|
+
watch: false
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
|
|
38
|
+
|
|
39
|
+
const connect = async (internals, config) => {
|
|
40
|
+
const Migrator = require('./walker');
|
|
41
|
+
const index = require('../connect');
|
|
42
|
+
|
|
43
|
+
const walker = await index.connect(
|
|
44
|
+
{
|
|
45
|
+
config: config.getCurrent().settings,
|
|
46
|
+
internals,
|
|
47
|
+
prefix: 'migration'
|
|
48
|
+
},
|
|
49
|
+
Migrator
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
walker.directory = path.resolve(internals.argv['migrations-dir']);
|
|
53
|
+
await walker.createMigrationsTable();
|
|
54
|
+
|
|
55
|
+
if (!State.supportsLock(walker._pdriver)) {
|
|
56
|
+
walker.driver.close(() => {});
|
|
57
|
+
throw new Error(
|
|
58
|
+
'Background migrations need a driver supporting the migration lock'
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return walker;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const close = walker =>
|
|
66
|
+
new Promise(resolve => walker.driver.close(() => resolve()));
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The interrupted run of a job, like State.startMigration reports it.
|
|
70
|
+
*/
|
|
71
|
+
const interruption = (job, hash) =>
|
|
72
|
+
job.step > 0
|
|
73
|
+
? {
|
|
74
|
+
step: job.step,
|
|
75
|
+
learned: job.learned,
|
|
76
|
+
done: job.done,
|
|
77
|
+
rollback: job.rb === 1,
|
|
78
|
+
changed: !!job.h && !!hash && job.h !== hash
|
|
79
|
+
}
|
|
80
|
+
: null;
|
|
81
|
+
|
|
82
|
+
const hashFile = file =>
|
|
83
|
+
crypto
|
|
84
|
+
.createHash('sha256')
|
|
85
|
+
.update(require('fs').readFileSync(file.path))
|
|
86
|
+
.digest('hex');
|
|
87
|
+
|
|
88
|
+
const runJob = async (walker, name, id, options, stopping, result) => {
|
|
89
|
+
const internals = walker.internals;
|
|
90
|
+
const pdriver = walker._pdriver;
|
|
91
|
+
let lost = false;
|
|
92
|
+
const update = mutate =>
|
|
93
|
+
Jobs.update(pdriver, internals, name, id, mutate, () => {
|
|
94
|
+
lost = true;
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const files = await File.loadFromFileystem(
|
|
98
|
+
walker.directory,
|
|
99
|
+
walker.prefix,
|
|
100
|
+
internals
|
|
101
|
+
);
|
|
102
|
+
const file = files.find(f => f.name === name);
|
|
103
|
+
|
|
104
|
+
if (!file) {
|
|
105
|
+
await update(job => {
|
|
106
|
+
job.s = 'failed';
|
|
107
|
+
job.ID = 0;
|
|
108
|
+
job.err = 'the migration file is missing';
|
|
109
|
+
});
|
|
110
|
+
throw new Error(`The migration file of the job ${name} is missing`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const heartbeat = setInterval(() => {
|
|
114
|
+
update(() => {}).catch(err => log.error(err.message));
|
|
115
|
+
}, Math.max(Math.floor(options.timeout / 3), 1));
|
|
116
|
+
heartbeat.unref();
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
const jobs = await Jobs.list(pdriver, internals);
|
|
120
|
+
const hash = hashFile(file);
|
|
121
|
+
const interrupted = interruption(jobs[name], hash);
|
|
122
|
+
|
|
123
|
+
await State.reloadSchema(pdriver, internals);
|
|
124
|
+
const mig = await pdriver._getKV(internals.migrationState, name);
|
|
125
|
+
internals.modSchema = Object.assign(
|
|
126
|
+
{ i: {}, c: {}, f: {}, s: [] },
|
|
127
|
+
mig ? JSON.parse(mig.value) : {}
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
if (!interrupted) {
|
|
131
|
+
await update(job => {
|
|
132
|
+
job.h = hash;
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
log.info(
|
|
137
|
+
`[jobs] ${interrupted ? 'continuing' : 'running'} ${name}`
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
const progress = {
|
|
141
|
+
step: op => update(job => { job.step = op; }),
|
|
142
|
+
learned: op => update(job => { job.learned = op; }),
|
|
143
|
+
done: op => update(job => { job.done = op; })
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
await Dml.execute(
|
|
147
|
+
walker,
|
|
148
|
+
file,
|
|
149
|
+
internals,
|
|
150
|
+
pdriver,
|
|
151
|
+
interrupted,
|
|
152
|
+
progress,
|
|
153
|
+
{
|
|
154
|
+
batch: options.batch,
|
|
155
|
+
pause: options.pause,
|
|
156
|
+
stopping: () => lost || stopping()
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
rollingBack: () => update(job => { job.rb = 1; }),
|
|
160
|
+
restart: () =>
|
|
161
|
+
update(job => {
|
|
162
|
+
Object.assign(job, { step: 0, learned: 0, done: 0, rb: 0, h: hash });
|
|
163
|
+
})
|
|
164
|
+
}
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
await Promise.promisify(walker.writeMigrationRecord.bind(walker))(file);
|
|
168
|
+
await update((job, jobs) => {
|
|
169
|
+
delete jobs[name];
|
|
170
|
+
});
|
|
171
|
+
log.info(`[jobs] ${name} is done`);
|
|
172
|
+
result.done.push(name);
|
|
173
|
+
} catch (err) {
|
|
174
|
+
if (lost) {
|
|
175
|
+
log.warn(`[jobs] ${name} was taken over by another worker`);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (err && err.stopped) {
|
|
180
|
+
await update(job => {
|
|
181
|
+
job.s = 'queued';
|
|
182
|
+
job.ID = 0;
|
|
183
|
+
});
|
|
184
|
+
log.info(`[jobs] ${name} stopped, it continues with the next run`);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// a job rolled back starts over, else it continues after the steps
|
|
189
|
+
// which can not be reverted
|
|
190
|
+
await update(job => {
|
|
191
|
+
job.s = 'failed';
|
|
192
|
+
job.ID = 0;
|
|
193
|
+
job.err = (err && err.message) || String(err);
|
|
194
|
+
if (err && err.rolledBack) {
|
|
195
|
+
Object.assign(job, { step: 0, learned: 0, done: 0, rb: 0 });
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
log.error(`[jobs] ${name} failed: ${(err && err.message) || err}`);
|
|
199
|
+
result.failed.push({ name, error: err });
|
|
200
|
+
} finally {
|
|
201
|
+
clearInterval(heartbeat);
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
module.exports = function executeWork (internals, config, opts = {}) {
|
|
206
|
+
const options = Object.assign({}, DEFAULTS, opts);
|
|
207
|
+
const id = crypto.randomBytes(16).toString('base64');
|
|
208
|
+
const seen = {};
|
|
209
|
+
const result = { done: [], failed: [] };
|
|
210
|
+
let stopped = false;
|
|
211
|
+
const stopping = () => stopped;
|
|
212
|
+
|
|
213
|
+
const slot = async () => {
|
|
214
|
+
// internals of its own, each slot migrates on its own connections
|
|
215
|
+
const own = Object.assign({}, internals, {
|
|
216
|
+
argv: Object.assign({}, internals.argv)
|
|
217
|
+
});
|
|
218
|
+
const walker = await connect(own, config);
|
|
219
|
+
|
|
220
|
+
try {
|
|
221
|
+
while (!stopping()) {
|
|
222
|
+
const name = await Jobs.claim(
|
|
223
|
+
walker._pdriver,
|
|
224
|
+
own,
|
|
225
|
+
id,
|
|
226
|
+
seen,
|
|
227
|
+
options.timeout
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
if (name) {
|
|
231
|
+
await runJob(walker, name, id, options, stopping, result);
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (!options.watch) {
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
await delay(options.interval);
|
|
240
|
+
}
|
|
241
|
+
} finally {
|
|
242
|
+
await close(walker);
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
const slots = [];
|
|
247
|
+
for (let i = 0; i < Math.max(Number(options.parallel) || 1, 1); i++) {
|
|
248
|
+
slots.push(slot());
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const done = Promise.all(slots).then(() => result);
|
|
252
|
+
|
|
253
|
+
return {
|
|
254
|
+
done,
|
|
255
|
+
stop: () => {
|
|
256
|
+
stopped = true;
|
|
257
|
+
return done;
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
};
|
package/package.json
CHANGED
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"programmable",
|
|
32
32
|
"api"
|
|
33
33
|
],
|
|
34
|
-
"version": "1.
|
|
34
|
+
"version": "1.4.0",
|
|
35
35
|
"bugs": {
|
|
36
36
|
"url": "https://github.com/db-migrate/node-db-migrate/issues"
|
|
37
37
|
},
|
|
@@ -57,9 +57,9 @@
|
|
|
57
57
|
"@hapi/code": "9.x",
|
|
58
58
|
"@hapi/eslint-plugin": "^6.0.0",
|
|
59
59
|
"@hapi/lab": "25.x",
|
|
60
|
-
"db-migrate-base": "^2.
|
|
61
|
-
"db-migrate-mysql": "^3.
|
|
62
|
-
"db-migrate-sqlite3": "^1.
|
|
60
|
+
"db-migrate-base": "^2.5.0",
|
|
61
|
+
"db-migrate-mysql": "^3.2.0",
|
|
62
|
+
"db-migrate-sqlite3": "^1.2.1",
|
|
63
63
|
"eslint": "^7.32.0",
|
|
64
64
|
"eslint-config-standard": "^14.1.1",
|
|
65
65
|
"eslint-plugin-import": "^2.26.0",
|