db-migrate 1.4.0 → 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/dml.js +5 -0
- package/lib/jobs.js +134 -9
- package/lib/state.js +6 -0
- package/lib/walker.js +67 -2
- package/lib/work.js +17 -3
- package/package.json +1 -1
package/lib/dml.js
CHANGED
package/lib/jobs.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const crypto = require('crypto');
|
|
4
4
|
const log = require('db-migrate-shared').log;
|
|
5
|
+
const State = require('./state');
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Background migrations, dml migrations with _meta.background, are not run
|
|
@@ -20,6 +21,12 @@ const log = require('db-migrate-shared').log;
|
|
|
20
21
|
* in the foreground, see State
|
|
21
22
|
* h: hash of the migration file when it started
|
|
22
23
|
* err: why it failed
|
|
24
|
+
*
|
|
25
|
+
* Migrations take precedence over jobs. Whoever holds the migration lock
|
|
26
|
+
* pauses the jobs before migrating: no job is taken anymore, the running
|
|
27
|
+
* ones stop after their current batch, and are continued once the lock is
|
|
28
|
+
* released. The pause is stored with the jobs, so taking a job and pausing
|
|
29
|
+
* never miss each other. It holds as long as its holder holds the lock.
|
|
23
30
|
*/
|
|
24
31
|
|
|
25
32
|
const JOBS = '__dbmigrate_jobs__';
|
|
@@ -28,26 +35,30 @@ const nonce = () => crypto.randomBytes(8).toString('hex');
|
|
|
28
35
|
|
|
29
36
|
const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
|
|
30
37
|
|
|
38
|
+
const WAIT = { wait: true };
|
|
39
|
+
|
|
31
40
|
const read = async (pdriver, internals) => {
|
|
32
41
|
const row = await pdriver._getKV(internals.migrationState, JOBS);
|
|
33
|
-
|
|
42
|
+
const state = row ? JSON.parse(row.value) : { jobs: {} };
|
|
43
|
+
return { row, state, jobs: state.jobs };
|
|
34
44
|
};
|
|
35
45
|
|
|
36
46
|
const list = async (pdriver, internals) => (await read(pdriver, internals)).jobs;
|
|
37
47
|
|
|
38
48
|
/**
|
|
39
|
-
* Changes the jobs by mutate, retried until no other worker
|
|
40
|
-
* between. mutate returns false to leave them as they are.
|
|
49
|
+
* Changes the jobs by mutate(jobs, state), retried until no other worker
|
|
50
|
+
* wrote in between. mutate returns false to leave them as they are.
|
|
41
51
|
*/
|
|
42
52
|
const change = async (pdriver, internals, mutate) => {
|
|
43
53
|
for (let attempt = 0; ; attempt++) {
|
|
44
|
-
const { row, jobs } = await read(pdriver, internals);
|
|
54
|
+
const { row, state, jobs } = await read(pdriver, internals);
|
|
45
55
|
|
|
46
|
-
if (mutate(jobs) === false) {
|
|
56
|
+
if (mutate(jobs, state) === false) {
|
|
47
57
|
return jobs;
|
|
48
58
|
}
|
|
49
59
|
|
|
50
|
-
|
|
60
|
+
state.n = nonce();
|
|
61
|
+
const value = JSON.stringify(state);
|
|
51
62
|
|
|
52
63
|
if (!row) {
|
|
53
64
|
try {
|
|
@@ -108,19 +119,61 @@ const register = async (pdriver, internals, file) => {
|
|
|
108
119
|
log.info(`[jobs] ${file.name} runs in the background`);
|
|
109
120
|
};
|
|
110
121
|
|
|
122
|
+
/**
|
|
123
|
+
* Whether the jobs are paused by a migration. A pause holds as long as its
|
|
124
|
+
* holder holds the migration lock and keeps it alive, the lock row did not
|
|
125
|
+
* stay the same for the lock timeout, measured on our clock in seen.
|
|
126
|
+
*
|
|
127
|
+
* Resolves to the pause, or null.
|
|
128
|
+
*/
|
|
129
|
+
const paused = async (pdriver, internals, seen = {}) => {
|
|
130
|
+
const { state } = await read(pdriver, internals);
|
|
131
|
+
|
|
132
|
+
if (!state.pause) {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const lock = await State.readLock(pdriver, internals);
|
|
137
|
+
if (!lock || State.holder(lock) !== state.pause.ID) {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const now = Date.now();
|
|
142
|
+
const last = seen.lock;
|
|
143
|
+
if (!last || last.value !== lock.value || String(last.run_on) !== String(lock.run_on)) {
|
|
144
|
+
seen.lock = { value: lock.value, run_on: lock.run_on, since: now };
|
|
145
|
+
} else if (now - last.since >= State.lockOptions(internals).timeout) {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return state.pause;
|
|
150
|
+
};
|
|
151
|
+
|
|
111
152
|
/**
|
|
112
153
|
* Takes the first job to run, in the order of their names. A running job is
|
|
113
154
|
* taken over if it did not change for timeout ms, measured on our clock in
|
|
114
155
|
* seen. Jobs after a blocking job wait until it is done.
|
|
115
156
|
*
|
|
116
|
-
* Resolves to the name of the job taken, or
|
|
157
|
+
* Resolves to the name of the job taken, null if there is none, or WAIT
|
|
158
|
+
* while the jobs are paused.
|
|
117
159
|
*/
|
|
118
160
|
const claim = async (pdriver, internals, id, seen, timeout) => {
|
|
119
161
|
const now = Date.now();
|
|
162
|
+
const valid = await paused(pdriver, internals, seen);
|
|
120
163
|
let taken = null;
|
|
164
|
+
let wait = false;
|
|
121
165
|
|
|
122
|
-
await change(pdriver, internals, jobs => {
|
|
166
|
+
await change(pdriver, internals, (jobs, state) => {
|
|
123
167
|
taken = null;
|
|
168
|
+
// a pause found invalid before is ignored, one set in between holds
|
|
169
|
+
wait = !!state.pause && (!!valid || state.pause.n !== seen.invalid);
|
|
170
|
+
if (state.pause && !valid) {
|
|
171
|
+
seen.invalid = seen.invalid || state.pause.n;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (wait) {
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
124
177
|
|
|
125
178
|
for (const name of Object.keys(jobs).sort()) {
|
|
126
179
|
const job = jobs[name];
|
|
@@ -154,9 +207,81 @@ const claim = async (pdriver, internals, id, seen, timeout) => {
|
|
|
154
207
|
return false;
|
|
155
208
|
});
|
|
156
209
|
|
|
210
|
+
if (wait) {
|
|
211
|
+
return WAIT;
|
|
212
|
+
}
|
|
213
|
+
|
|
157
214
|
return taken;
|
|
158
215
|
};
|
|
159
216
|
|
|
217
|
+
const isRunning = (job, id) => job.s === 'running' && job.ID !== id;
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Pauses the jobs for the migrations of the lock holder id, and waits until
|
|
221
|
+
* the running jobs stopped. A job whose worker does not respond for the lock
|
|
222
|
+
* timeout is not waited for, it is continued by another worker later.
|
|
223
|
+
*/
|
|
224
|
+
const pause = async (pdriver, internals, id) => {
|
|
225
|
+
const { timeout, interval } = State.lockOptions(internals);
|
|
226
|
+
const seen = {};
|
|
227
|
+
|
|
228
|
+
await change(pdriver, internals, (jobs, state) => {
|
|
229
|
+
state.pause = { ID: id, n: nonce() };
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
for (;;) {
|
|
233
|
+
const now = Date.now();
|
|
234
|
+
const jobs = await list(pdriver, internals);
|
|
235
|
+
const running = Object.keys(jobs).filter(name => {
|
|
236
|
+
const job = jobs[name];
|
|
237
|
+
|
|
238
|
+
if (!isRunning(job, id)) {
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const last = seen[name];
|
|
243
|
+
if (!last || last.n !== job.n) {
|
|
244
|
+
seen[name] = { n: job.n, since: now };
|
|
245
|
+
return true;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return now - last.since < timeout;
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
if (!running.length) {
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
log.info(
|
|
256
|
+
`[jobs] waiting for ${running.length} background job(s) to pause: ` +
|
|
257
|
+
running.join(', ')
|
|
258
|
+
);
|
|
259
|
+
// they stop after their current batch, usually soon
|
|
260
|
+
await delay(Math.min(interval, 200));
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Forgets a job, once its migration is reverted.
|
|
266
|
+
*/
|
|
267
|
+
const remove = (pdriver, internals, name) =>
|
|
268
|
+
change(pdriver, internals, jobs => {
|
|
269
|
+
if (!jobs[name]) {
|
|
270
|
+
return false;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
delete jobs[name];
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
const resume = (pdriver, internals, id) =>
|
|
277
|
+
change(pdriver, internals, (jobs, state) => {
|
|
278
|
+
if (!state.pause || state.pause.ID !== id) {
|
|
279
|
+
return false;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
delete state.pause;
|
|
283
|
+
});
|
|
284
|
+
|
|
160
285
|
/**
|
|
161
286
|
* Changes a job we run. If another worker took it over in between, lost is
|
|
162
287
|
* called and nothing is changed.
|
|
@@ -174,4 +299,4 @@ const update = (pdriver, internals, name, id, mutate, lost = () => {}) =>
|
|
|
174
299
|
job.n = nonce();
|
|
175
300
|
});
|
|
176
301
|
|
|
177
|
-
module.exports = { JOBS, list, register, claim, update };
|
|
302
|
+
module.exports = { JOBS, WAIT, list, register, claim, update, paused, pause, resume, remove };
|
package/lib/state.js
CHANGED
|
@@ -155,6 +155,12 @@ const claim = (row, driver, internals) => {
|
|
|
155
155
|
module.exports = {
|
|
156
156
|
createConcurrently,
|
|
157
157
|
|
|
158
|
+
// the id of this process in the lock, the holder of a lock row, see Jobs
|
|
159
|
+
ID,
|
|
160
|
+
holder,
|
|
161
|
+
lockOptions,
|
|
162
|
+
readLock: read,
|
|
163
|
+
|
|
158
164
|
// for tests, simulates a process which died while holding the lock
|
|
159
165
|
__stopHeartbeat: stopHeartbeat,
|
|
160
166
|
|
package/lib/walker.js
CHANGED
|
@@ -127,10 +127,20 @@ Walker.prototype = {
|
|
|
127
127
|
return run(toRun);
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
+
let paused = false;
|
|
130
131
|
try {
|
|
132
|
+
// migrations take precedence over the jobs of background migrations
|
|
133
|
+
await Jobs.pause(this._pdriver, this.internals, State.ID);
|
|
134
|
+
paused = true;
|
|
131
135
|
await State.reloadSchema(this._pdriver, this.internals);
|
|
132
136
|
return await run(await determine());
|
|
133
137
|
} finally {
|
|
138
|
+
if (paused) {
|
|
139
|
+
await Jobs.resume(this._pdriver, this.internals, State.ID).catch(err =>
|
|
140
|
+
log.warn(`[jobs] could not resume the jobs: ${err.message}`)
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
134
144
|
await State.release(this._pdriver, this.internals);
|
|
135
145
|
}
|
|
136
146
|
},
|
|
@@ -158,6 +168,52 @@ Walker.prototype = {
|
|
|
158
168
|
});
|
|
159
169
|
},
|
|
160
170
|
|
|
171
|
+
/**
|
|
172
|
+
* The migrations of the jobs, newest first.
|
|
173
|
+
*/
|
|
174
|
+
_jobFiles: async function () {
|
|
175
|
+
if (this.prefix !== 'migration' || !this._pdriver || typeof this._pdriver._getKV !== 'function') {
|
|
176
|
+
return [];
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const jobs = await Jobs.list(this._pdriver, this.internals);
|
|
180
|
+
const names = Object.keys(jobs);
|
|
181
|
+
if (!names.length) {
|
|
182
|
+
return [];
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const files = await File.loadFromFileystem(this.directory, this.prefix, this.internals);
|
|
186
|
+
return names
|
|
187
|
+
.sort()
|
|
188
|
+
.reverse()
|
|
189
|
+
.map(name => {
|
|
190
|
+
const file = files.find(f => f.name === name);
|
|
191
|
+
if (!file) {
|
|
192
|
+
throw new Error(
|
|
193
|
+
`The migration of the background job "${name}" is missing, it ` +
|
|
194
|
+
'can not be reverted'
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
file.job = true;
|
|
199
|
+
return file;
|
|
200
|
+
});
|
|
201
|
+
},
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Reverts what the job of a background migration did so far, it was
|
|
205
|
+
* paused by taking the lock, and forgets the job.
|
|
206
|
+
*/
|
|
207
|
+
_revertJob: async function (file) {
|
|
208
|
+
log.info(`${this.title}reverting the background job of ${file.name}`);
|
|
209
|
+
this.internals.modSchema = { i: {}, c: {}, f: {}, s: [] };
|
|
210
|
+
|
|
211
|
+
if (!this.internals.dryRun) {
|
|
212
|
+
await Dml.down(this, this.driver, file);
|
|
213
|
+
await Jobs.remove(this._pdriver, this.internals, file.name);
|
|
214
|
+
}
|
|
215
|
+
},
|
|
216
|
+
|
|
161
217
|
_registerJob: async function (file) {
|
|
162
218
|
Dml.checkType(file);
|
|
163
219
|
|
|
@@ -291,8 +347,13 @@ Walker.prototype = {
|
|
|
291
347
|
this.prefix,
|
|
292
348
|
this._driver,
|
|
293
349
|
this.internals
|
|
294
|
-
).then(completedFiles =>
|
|
295
|
-
dbmUtil.filterDown(
|
|
350
|
+
).then(async completedFiles =>
|
|
351
|
+
dbmUtil.filterDown(
|
|
352
|
+
// jobs of background migrations are the latest, still running
|
|
353
|
+
(await this._jobFiles()).concat(completedFiles),
|
|
354
|
+
partialName,
|
|
355
|
+
count
|
|
356
|
+
)
|
|
296
357
|
);
|
|
297
358
|
|
|
298
359
|
if (this.internals.check) {
|
|
@@ -309,6 +370,10 @@ Walker.prototype = {
|
|
|
309
370
|
|
|
310
371
|
return Promise.each(toRun, file => {
|
|
311
372
|
log.verbose(this.title + 'preparing to run down:', file.name);
|
|
373
|
+
if (file.job) {
|
|
374
|
+
return this._revertJob(file).catch(failedIn(file));
|
|
375
|
+
}
|
|
376
|
+
|
|
312
377
|
const _meta = file.get()._meta || {};
|
|
313
378
|
const version = _meta.version || 1;
|
|
314
379
|
this.internals.modSchema = { i: {}, c: {}, f: {}, s: [] };
|
package/lib/work.js
CHANGED
|
@@ -85,7 +85,7 @@ const hashFile = file =>
|
|
|
85
85
|
.update(require('fs').readFileSync(file.path))
|
|
86
86
|
.digest('hex');
|
|
87
87
|
|
|
88
|
-
const runJob = async (walker, name, id, options, stopping, result) => {
|
|
88
|
+
const runJob = async (walker, name, id, options, stopping, result, seen) => {
|
|
89
89
|
const internals = walker.internals;
|
|
90
90
|
const pdriver = walker._pdriver;
|
|
91
91
|
let lost = false;
|
|
@@ -153,7 +153,15 @@ const runJob = async (walker, name, id, options, stopping, result) => {
|
|
|
153
153
|
{
|
|
154
154
|
batch: options.batch,
|
|
155
155
|
pause: options.pause,
|
|
156
|
-
stopping: () => lost || stopping()
|
|
156
|
+
stopping: () => lost || stopping(),
|
|
157
|
+
yielding: async () => {
|
|
158
|
+
if (await Jobs.paused(pdriver, internals, seen)) {
|
|
159
|
+
log.info(`[jobs] ${name} pauses for migrations`);
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
157
165
|
},
|
|
158
166
|
{
|
|
159
167
|
rollingBack: () => update(job => { job.rb = 1; }),
|
|
@@ -227,8 +235,14 @@ module.exports = function executeWork (internals, config, opts = {}) {
|
|
|
227
235
|
options.timeout
|
|
228
236
|
);
|
|
229
237
|
|
|
238
|
+
if (name === Jobs.WAIT) {
|
|
239
|
+
// paused by migrations, the jobs continue afterwards
|
|
240
|
+
await delay(options.interval);
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
|
|
230
244
|
if (name) {
|
|
231
|
-
await runJob(walker, name, id, options, stopping, result);
|
|
245
|
+
await runJob(walker, name, id, options, stopping, result, seen);
|
|
232
246
|
continue;
|
|
233
247
|
}
|
|
234
248
|
|