db-migrate 1.0.0-beta.35 → 1.0.0-beta.36

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.
@@ -17,7 +17,9 @@ module.exports = function (internals, isModule) {
17
17
  'vcseeder-dir': internals.cwd + '/VCSeeder',
18
18
  'staticseeder-dir': internals.cwd + '/Seeder',
19
19
  'ignore-completed-migrations': false,
20
- 'backup-state': false
20
+ 'backup-state': false,
21
+ 'lock-timeout': 60000,
22
+ 'lock-interval': 1000
21
23
  };
22
24
 
23
25
  if (!isModule) {
@@ -116,6 +118,17 @@ module.exports = function (internals, isModule) {
116
118
  .string('log-level')
117
119
  .describe('backup-state', 'For use with fix, to take a backup of the current state before continuing.')
118
120
  .boolean('backup-state')
121
+ .describe(
122
+ 'lock-timeout',
123
+ 'Milliseconds without any sign of life after which a migration lock' +
124
+ ' held by another process is considered stale and taken over.'
125
+ )
126
+ .number('lock-timeout')
127
+ .describe(
128
+ 'lock-interval',
129
+ 'Milliseconds between checks while waiting for the migration lock.'
130
+ )
131
+ .number('lock-interval')
119
132
  .parse();
120
133
  } else {
121
134
  internals.argv = Object.assign(defaultConfig, internals.cmdOptions);
package/lib/state.js CHANGED
@@ -1,68 +1,284 @@
1
1
  const MSTATE = '__dbmigrate_state__';
2
2
  const SSTATE = '__dbmigrate_schema__';
3
- const RUNON = 'run_on';
4
3
 
5
4
  const log = require('db-migrate-shared').log;
6
5
  const fs = require('fs').promises;
7
6
  const path = require('path');
8
7
  const crypto = require('crypto');
8
+ const { performance } = require('perf_hooks');
9
9
 
10
10
  const ID = crypto.randomBytes(32).toString('base64');
11
+
12
+ const LOCK_TIMEOUT = 60000;
13
+ const LOCK_INTERVAL = 1000;
14
+
15
+ /**
16
+ * active: a lock session is running, from acquire until release, even if
17
+ * the lock got lost in between
18
+ * owner: we currently hold the lock
19
+ * current: the last MSTATE row we wrote and verified, the expected
20
+ * value for our next compare and swap
21
+ * queue: serializes our own writes, so heartbeat and state updates
22
+ * never race each other on the same expected row
23
+ */
24
+ let active = false;
11
25
  let owner = false;
26
+ let current = null;
27
+ let heartbeat = null;
28
+ let queue = Promise.resolve();
29
+
30
+ const nonce = () => crypto.randomBytes(8).toString('hex');
31
+
32
+ const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
33
+
34
+ const lockOptions = internals => {
35
+ const argv = internals.argv || {};
36
+ return {
37
+ timeout: Number(argv['lock-timeout']) || LOCK_TIMEOUT,
38
+ interval: Number(argv['lock-interval']) || LOCK_INTERVAL
39
+ };
40
+ };
41
+
42
+ const read = (driver, internals) =>
43
+ driver._getKV(internals.migrationState, MSTATE);
44
+
45
+ const holder = row => JSON.parse(row.value).s.ID;
46
+
47
+ const heldByOther = row => {
48
+ const id = holder(row);
49
+ return !!id && id !== ID;
50
+ };
51
+
52
+ /**
53
+ * Atomic update of the lock row. The where clause contains the value we
54
+ * have read before, which carries the date and a nonce renewed on every
55
+ * write. If anybody else wrote in between, the update matches no row.
56
+ * Whoever wins, sees his own value on re-read.
57
+ */
58
+ const cas = (driver, internals, expected, value) =>
59
+ driver._updateKVC(
60
+ internals.migrationState,
61
+ MSTATE,
62
+ value,
63
+ 'value',
64
+ expected.value
65
+ );
66
+
67
+ /**
68
+ * Concurrent CREATE TABLE IF NOT EXISTS can still collide (e.g. on pg_type
69
+ * in postgres) when several processes start on an empty database. The
70
+ * second attempt finds the table and is a noop.
71
+ */
72
+ const createConcurrently = async create => {
73
+ try {
74
+ await create();
75
+ } catch (err) {
76
+ log.verbose('[state] create failed, retrying once', err.message);
77
+ await create();
78
+ }
79
+ };
80
+
81
+ /**
82
+ * The stored schema starts as '{}', keep the structure the walker expects.
83
+ */
84
+ const parseSchema = value =>
85
+ Object.assign({ i: {}, c: {}, f: {}, e: {} }, JSON.parse(value));
86
+
87
+ const stopHeartbeat = () => {
88
+ if (heartbeat) {
89
+ clearInterval(heartbeat);
90
+ heartbeat = null;
91
+ }
92
+ };
93
+
94
+ const write = (driver, internals, mutate) => {
95
+ const run = queue.then(async () => {
96
+ if (!owner) {
97
+ throw new Error('[state] the migration lock is not held by this process');
98
+ }
99
+
100
+ const state = JSON.parse(current.value);
101
+ Object.assign(state.s, { ID, date: new Date(), n: nonce() });
102
+ mutate(state.s);
103
+
104
+ const value = JSON.stringify(state);
105
+ await cas(driver, internals, current, value);
106
+ const row = await read(driver, internals);
107
+
108
+ if (!row || row.value !== value) {
109
+ owner = false;
110
+ current = null;
111
+ stopHeartbeat();
112
+ throw new Error('[state] lost the migration lock to another process');
113
+ }
114
+
115
+ current = row;
116
+ });
117
+
118
+ queue = run.catch(() => {});
119
+ return run;
120
+ };
121
+
122
+ const claim = (row, driver, internals) => {
123
+ if (!row || holder(row) !== ID) {
124
+ return false;
125
+ }
126
+
127
+ active = true;
128
+ owner = true;
129
+ current = row;
130
+
131
+ const { timeout } = lockOptions(internals);
132
+ heartbeat = setInterval(() => {
133
+ write(driver, internals, () => {}).catch(err => log.error(err.message));
134
+ }, Math.max(Math.floor(timeout / 3), 1));
135
+ heartbeat.unref();
136
+
137
+ return true;
138
+ };
12
139
 
13
140
  module.exports = {
141
+ createConcurrently,
142
+
143
+ // for tests, simulates a process which died while holding the lock
144
+ __stopHeartbeat: stopHeartbeat,
145
+
14
146
  /**
15
- * Lock state will add the basic state controller an ID
16
- * and a process controlled date.
17
- * It is important that all systems are in sync. We will use the
18
- * database clock instead if possible.
147
+ * Drivers declare that their state methods were verified for locking,
148
+ * with an atomic _updateKVC and run_on set by the database clock.
19
149
  */
20
- lockState: async function (driver, state, internals) {
21
- if (!state) {
22
- owner = true;
23
- return driver._insertKV(
24
- internals.migrationState,
25
- MSTATE,
26
- JSON.stringify({
27
- s: {
28
- step: 0,
29
- fin: 0,
30
- ID,
31
- date: new Date()
32
- }
33
- })
34
- );
35
- } else {
36
- let runOn = state.run_on;
37
- state = state.value;
38
- state = JSON.parse(state);
150
+ supportsLock: function (driver) {
151
+ return !!(
152
+ driver &&
153
+ driver._meta &&
154
+ driver._meta.supports &&
155
+ driver._meta.supports.locking === true
156
+ );
157
+ },
39
158
 
40
- if (!state.s.ID || state.s.ID !== ID) {
41
- owner = true;
42
- state.s.ID = ID;
159
+ /**
160
+ * Try to acquire the migration lock once. Resolves to true if this process
161
+ * is the owner afterwards. A lock held by another process is only taken
162
+ * over, if it is the exact row waitForRelease reported as stale.
163
+ */
164
+ acquire: async function (driver, internals, stale = null, retries = 3) {
165
+ if (owner) {
166
+ return true;
167
+ }
168
+
169
+ const row = await read(driver, internals);
170
+
171
+ if (!row) {
172
+ try {
173
+ await driver._insertKV(
174
+ internals.migrationState,
175
+ MSTATE,
176
+ JSON.stringify({
177
+ s: { step: 0, fin: 1, ID, date: new Date(), n: nonce() }
178
+ })
179
+ );
180
+ } catch (err) {
181
+ // somebody else inserted the row first, the re-read decides
182
+ log.verbose('[state] lock insert failed', err.message);
43
183
  }
44
184
 
45
- const d = new Date();
185
+ return claim(await read(driver, internals), driver, internals);
186
+ }
46
187
 
47
- state.s.date = d;
48
- await driver._updateKVC(
49
- internals.migrationState,
50
- MSTATE,
51
- JSON.stringify(state),
52
- RUNON,
53
- runOn
188
+ if (heldByOther(row)) {
189
+ if (!stale || stale.value !== row.value) {
190
+ return false;
191
+ }
192
+
193
+ const { s } = JSON.parse(row.value);
194
+ log.warn(
195
+ `[state] taking over a stale migration lock from ${s.date}` +
196
+ (s.fin === 0
197
+ ? `, the previous run was interrupted during a migration at step ${s.step}`
198
+ : '')
199
+ );
200
+ }
201
+
202
+ const state = JSON.parse(row.value);
203
+ Object.assign(state.s, { ID, date: new Date(), n: nonce() });
204
+ await cas(driver, internals, row, JSON.stringify(state));
205
+
206
+ const after = await read(driver, internals);
207
+ if (claim(after, driver, internals)) {
208
+ return true;
209
+ }
210
+
211
+ // Nobody holds the lock, still our update did not apply. Either another
212
+ // process acquired and released in between, or the compare and swap does
213
+ // not work with this driver at all, which must not end in a busy loop.
214
+ if (after && !heldByOther(after)) {
215
+ if (retries > 0) {
216
+ return module.exports.acquire(driver, internals, null, retries - 1);
217
+ }
218
+
219
+ throw new Error(
220
+ '[state] could not acquire the migration lock although it is free, ' +
221
+ 'the driver does not seem to support compare and swap on the state'
54
222
  );
223
+ }
55
224
 
56
- state = await driver._getKV(internals.migrationState, MSTATE);
225
+ return false;
226
+ },
57
227
 
58
- runOn = state.run_on;
59
- state = state.value;
60
- state = JSON.parse(state);
228
+ /**
229
+ * Wait until the lock is released. A lock whose row did not change for
230
+ * lock-timeout ms is considered stale, its holder is gone. This is
231
+ * measured on our own monotonic clock, so no clocks have to be in sync.
232
+ *
233
+ * Resolves to the stale row, or null if the lock was released properly.
234
+ */
235
+ waitForRelease: async function (driver, internals) {
236
+ const { timeout, interval } = lockOptions(internals);
237
+ let seen = null;
238
+ let since = 0;
239
+
240
+ for (;;) {
241
+ const row = await read(driver, internals);
242
+
243
+ if (!row || !heldByOther(row)) {
244
+ return null;
245
+ }
61
246
 
62
- if (state.s.ID !== ID) {
63
- owner = false;
247
+ const now = performance.now();
248
+ if (
249
+ !seen ||
250
+ seen.value !== row.value ||
251
+ String(seen.run_on) !== String(row.run_on)
252
+ ) {
253
+ seen = row;
254
+ since = now;
255
+ } else if (now - since >= timeout) {
256
+ return row;
64
257
  }
258
+
259
+ await delay(interval);
260
+ }
261
+ },
262
+
263
+ release: async function (driver, internals) {
264
+ stopHeartbeat();
265
+
266
+ if (!owner) {
267
+ active = false;
268
+ return;
269
+ }
270
+
271
+ try {
272
+ await write(driver, internals, s => {
273
+ s.ID = 0;
274
+ });
275
+ } catch (err) {
276
+ log.warn(err.message);
65
277
  }
278
+
279
+ active = false;
280
+ owner = false;
281
+ current = null;
66
282
  },
67
283
 
68
284
  isOwner: function () {
@@ -70,7 +286,7 @@ module.exports = {
70
286
  },
71
287
 
72
288
  init: async function (driver, internals, { emptyState, backupState }) {
73
- await driver._createKV(internals.migrationState);
289
+ await createConcurrently(() => driver._createKV(internals.migrationState));
74
290
  const _schema = await driver._getKV(internals.migrationState, SSTATE);
75
291
 
76
292
  if (_schema && backupState) {
@@ -91,21 +307,34 @@ module.exports = {
91
307
 
92
308
  const schema = emptyState !== true ? _schema : null;
93
309
  if (schema) {
94
- internals.schema = JSON.parse(schema.value);
310
+ internals.schema = parseSchema(schema.value);
95
311
  } else if (!emptyState) {
96
- await driver._insertKV(internals.migrationState, SSTATE, '{}');
312
+ try {
313
+ await driver._insertKV(internals.migrationState, SSTATE, '{}');
314
+ } catch (err) {
315
+ // another process initialized the state at the same time
316
+ const row = await driver._getKV(internals.migrationState, SSTATE);
317
+ if (!row) {
318
+ throw err;
319
+ }
320
+
321
+ internals.schema = parseSchema(row.value);
322
+ }
97
323
  }
324
+ },
98
325
 
99
- if (!internals.dryRun) {
100
- const state = await driver._getKV(internals.migrationState, MSTATE);
101
- return module.exports.lockState(driver, state, internals);
326
+ /**
327
+ * Another process may have migrated while we were waiting for the lock,
328
+ * so the schema loaded during init is outdated.
329
+ */
330
+ reloadSchema: async function (driver, internals) {
331
+ const schema = await driver._getKV(internals.migrationState, SSTATE);
332
+ if (schema) {
333
+ internals.schema = parseSchema(schema.value);
102
334
  }
103
-
104
- return Promise.resolve();
105
335
  },
106
336
 
107
337
  startMigration: async function (driver, file, internals) {
108
- const state = await driver._getKV(internals.migrationState, MSTATE);
109
338
  const mig = await driver._getKV(internals.migrationState, file.name);
110
339
 
111
340
  if (mig && mig.value !== '{}') {
@@ -116,7 +345,10 @@ module.exports = {
116
345
  return Promise.resolve();
117
346
  }
118
347
 
119
- await module.exports.lockState(driver, state, internals);
348
+ await module.exports.progress(driver, internals, s => {
349
+ s.step = 0;
350
+ s.fin = 0;
351
+ });
120
352
 
121
353
  if (!mig) {
122
354
  return driver._insertKV(
@@ -129,64 +361,55 @@ module.exports = {
129
361
  }
130
362
  },
131
363
 
132
- update: async function (driver, file, state, internals) {
133
- log.verbose(`[state] update state`);
134
- if (internals.dryRun) {
135
- return Promise.resolve();
364
+ /**
365
+ * Write migration progress into the lock row. While holding the lock this
366
+ * is a compare and swap, which fails if we lost the lock. Without a lock
367
+ * (driver does not support it) the row is written unconditionally.
368
+ */
369
+ progress: async function (driver, internals, mutate) {
370
+ if (active) {
371
+ return write(driver, internals, mutate);
136
372
  }
137
373
 
138
- await driver._updateKV(internals.migrationState, SSTATE, JSON.stringify(internals.schema));
374
+ const row = await read(driver, internals);
375
+ const state = row
376
+ ? JSON.parse(row.value)
377
+ : { s: { step: 0, fin: 0, ID: 0 } };
378
+ state.s.date = new Date();
379
+ mutate(state.s);
380
+
381
+ if (!row) {
382
+ return driver._insertKV(
383
+ internals.migrationState,
384
+ MSTATE,
385
+ JSON.stringify(state)
386
+ );
387
+ }
139
388
 
140
389
  return driver._updateKV(
141
390
  internals.migrationState,
142
- file.name,
391
+ MSTATE,
143
392
  JSON.stringify(state)
144
393
  );
145
394
  },
146
395
 
147
- get: function (driver, file, internals) {
148
- return driver._getKV(internals.migrationState, file.name);
149
- },
150
-
151
- tick: async function (driver, internals) {
396
+ update: async function (driver, file, state, internals) {
397
+ log.verbose(`[state] update state`);
152
398
  if (internals.dryRun) {
153
399
  return Promise.resolve();
154
400
  }
155
401
 
156
- let state = await driver._getKV(
157
- internals.migrationState,
158
- MSTATE
159
- );
160
-
161
- let runOn = state.run_on;
162
- state = state.value;
163
- state = JSON.parse(state);
164
-
165
- if (!state.s.ID || state.s.ID !== ID) {
166
- owner = true;
167
- state.s.ID = ID;
168
- }
169
-
170
- const d = new Date();
402
+ await driver._updateKV(internals.migrationState, SSTATE, JSON.stringify(internals.schema));
171
403
 
172
- state.s.date = d;
173
- await driver._updateKVC(
404
+ return driver._updateKV(
174
405
  internals.migrationState,
175
- MSTATE,
176
- JSON.stringify(state),
177
- RUNON,
178
- runOn
406
+ file.name,
407
+ JSON.stringify(state)
179
408
  );
409
+ },
180
410
 
181
- state = await driver._getKV(internals.migrationState, MSTATE);
182
-
183
- runOn = state.run_on;
184
- state = state.value;
185
- state = JSON.parse(state);
186
-
187
- if (state.s.ID !== ID) {
188
- owner = false;
189
- }
411
+ get: function (driver, file, internals) {
412
+ return driver._getKV(internals.migrationState, file.name);
190
413
  },
191
414
 
192
415
  step: async function (driver, step, internals) {
@@ -195,18 +418,9 @@ module.exports = {
195
418
  return Promise.resolve();
196
419
  }
197
420
 
198
- let { value: state } = await driver._getKV(
199
- internals.migrationState,
200
- MSTATE
201
- );
202
- state = JSON.parse(state);
203
- state.s.date = new Date();
204
- state.s.step = step;
205
- return driver._updateKV(
206
- internals.migrationState,
207
- MSTATE,
208
- JSON.stringify(state)
209
- );
421
+ return module.exports.progress(driver, internals, s => {
422
+ s.step = step;
423
+ });
210
424
  },
211
425
 
212
426
  deleteState: function (driver, file, internals) {
@@ -218,21 +432,9 @@ module.exports = {
218
432
  return Promise.resolve();
219
433
  }
220
434
 
221
- let { value: state } = await driver._getKV(
222
- internals.migrationState,
223
- MSTATE
224
- );
225
- state = JSON.parse(state);
226
- state.s.date = new Date();
227
- state.s.fin = 1;
228
-
229
- // unlock from current process
230
- state.s.ID = 0;
231
-
232
- return driver._updateKV(
233
- internals.migrationState,
234
- MSTATE,
235
- JSON.stringify(state)
236
- );
435
+ // the lock itself stays until the walker releases it
436
+ return module.exports.progress(driver, internals, s => {
437
+ s.fin = 1;
438
+ });
237
439
  }
238
440
  };
package/lib/walker.js CHANGED
@@ -63,7 +63,9 @@ const Walker = function (driver, directory, mode, intern, prefix, opts = {}) {
63
63
  Walker.prototype = {
64
64
  createTables: async function (options) {
65
65
  await State.init(this._pdriver, this.internals, options);
66
- return this._driver._createList(this.internals.migrationTable);
66
+ return State.createConcurrently(() =>
67
+ this._driver._createList(this.internals.migrationTable)
68
+ );
67
69
  },
68
70
 
69
71
  createMigrationsTable: function (options = {}) {
@@ -90,6 +92,52 @@ Walker.prototype = {
90
92
  return this.createTables(options);
91
93
  },
92
94
 
95
+ /**
96
+ * Determine what to run first, only lock when there is actually something
97
+ * to run. If another process holds the lock, wait for its release and
98
+ * determine again, the other process might have done the work already.
99
+ * Once we hold the lock, state and pending migrations are read again.
100
+ */
101
+ _withLock: async function (determine, run) {
102
+ if (this.prefix !== 'migration' || this.internals.dryRun) {
103
+ return run(await determine());
104
+ }
105
+
106
+ if (!State.supportsLock(this._pdriver)) {
107
+ log.warn(
108
+ this.title +
109
+ 'Your database driver does not support migration locking, ' +
110
+ 'concurrent processes are not prevented from migrating at the ' +
111
+ 'same time. Update your driver to a version supporting it.'
112
+ );
113
+ return run(await determine());
114
+ }
115
+
116
+ let stale = null;
117
+ let toRun = await determine();
118
+
119
+ while (toRun.length > 0) {
120
+ if (await State.acquire(this._pdriver, this.internals, stale)) {
121
+ break;
122
+ }
123
+
124
+ log.info(this.title + 'Waiting for the migration lock of another process');
125
+ stale = await State.waitForRelease(this._pdriver, this.internals);
126
+ toRun = await determine();
127
+ }
128
+
129
+ if (toRun.length === 0) {
130
+ return run(toRun);
131
+ }
132
+
133
+ try {
134
+ await State.reloadSchema(this._pdriver, this.internals);
135
+ return await run(await determine());
136
+ } finally {
137
+ await State.release(this._pdriver, this.internals);
138
+ }
139
+ },
140
+
93
141
  writeMigrationRecord: function (migration, callback) {
94
142
  const onComplete = err => {
95
143
  if (err) {
@@ -149,96 +197,84 @@ Walker.prototype = {
149
197
  up: function (options, callback) {
150
198
  const partialName = options.destination;
151
199
  const count = options.count;
152
- const files = Promise.all([
153
- File.loadFromFileystem(this.directory, this.prefix, this.internals),
154
- File.loadFromDatabase(
155
- this.directory,
156
- this.prefix,
157
- this._driver,
158
- this.internals
159
- )
160
- ]).spread((allFiles, completedFiles) => {
161
- const toRun = dbmUtil.filterUp(
162
- allFiles,
163
- completedFiles,
164
- partialName,
165
- count
200
+ const determine = () =>
201
+ Promise.all([
202
+ File.loadFromFileystem(this.directory, this.prefix, this.internals),
203
+ File.loadFromDatabase(
204
+ this.directory,
205
+ this.prefix,
206
+ this._driver,
207
+ this.internals
208
+ )
209
+ ]).spread((allFiles, completedFiles) =>
210
+ dbmUtil.filterUp(allFiles, completedFiles, partialName, count)
166
211
  );
167
212
 
168
- if (toRun.length === 0) {
169
- log.info(this.title + 'Nothing to run');
170
- }
171
-
172
- if (this.internals.check) {
173
- const toRunNames = toRun.map(migration => migration.name);
174
- log.info(this.title + 'run:', toRunNames);
175
- return toRunNames;
176
- }
177
-
178
- return toRun;
179
- });
180
-
181
213
  if (this.internals.check) {
182
- return files.nodeify(callback);
214
+ return determine()
215
+ .then(toRun => this._checkResult(toRun))
216
+ .nodeify(callback);
183
217
  }
184
218
 
185
- return files
186
- .each(file => {
187
- log.verbose(this.title + 'preparing to run up:', file.name);
188
- const _meta = file.get()._meta || {};
189
- const version = _meta.version || 1;
190
- this.internals.modSchema = { i: {}, c: {}, f: {}, s: [] };
191
- return require(`./executors/versioned/v${version}`).up(
192
- this,
193
- this.driver,
194
- file
195
- );
219
+ return Promise.resolve(
220
+ this._withLock(determine, toRun => {
221
+ if (toRun.length === 0) {
222
+ log.info(this.title + 'Nothing to run');
223
+ }
224
+
225
+ return Promise.each(toRun, file => {
226
+ log.verbose(this.title + 'preparing to run up:', file.name);
227
+ const _meta = file.get()._meta || {};
228
+ const version = _meta.version || 1;
229
+ this.internals.modSchema = { i: {}, c: {}, f: {}, s: [] };
230
+ return require(`./executors/versioned/v${version}`).up(
231
+ this,
232
+ this.driver,
233
+ file
234
+ );
235
+ });
196
236
  })
197
- .nodeify(callback);
237
+ ).nodeify(callback);
198
238
  },
199
239
 
200
240
  down: function (options, callback) {
201
241
  const partialName = options.destination;
202
242
  const count = options.count;
203
-
204
- const files = File.loadFromDatabase(
205
- this.directory,
206
- this.prefix,
207
- this._driver,
208
- this.internals
209
- ).then(completedFiles => {
210
- const toRun = dbmUtil.filterDown(completedFiles, partialName, count);
211
-
212
- if (toRun.length === 0) {
213
- log.info(this.title + 'Nothing to run');
214
- }
215
-
216
- if (this.internals.check) {
217
- const toRunNames = toRun.map(migration => migration.name);
218
- log.info(this.title + 'run:', toRunNames);
219
- return toRunNames;
220
- }
221
-
222
- return toRun;
223
- });
243
+ const determine = () =>
244
+ File.loadFromDatabase(
245
+ this.directory,
246
+ this.prefix,
247
+ this._driver,
248
+ this.internals
249
+ ).then(completedFiles =>
250
+ dbmUtil.filterDown(completedFiles, partialName, count)
251
+ );
224
252
 
225
253
  if (this.internals.check) {
226
- return files.nodeify(callback);
254
+ return determine()
255
+ .then(toRun => this._checkResult(toRun))
256
+ .nodeify(callback);
227
257
  }
228
258
 
229
- return files
230
- .each(file => {
231
- log.verbose(this.title + 'preparing to run down:', file.name);
232
- const _meta = file.get()._meta || {};
233
- const version = _meta.version || 1;
234
- this.internals.modSchema = { i: {}, c: {}, f: {}, s: [] };
235
- return require(`./executors/versioned/v${version}`).down(
236
- this,
237
- this.driver,
238
- file
239
- );
259
+ return Promise.resolve(
260
+ this._withLock(determine, toRun => {
261
+ if (toRun.length === 0) {
262
+ log.info(this.title + 'Nothing to run');
263
+ }
264
+
265
+ return Promise.each(toRun, file => {
266
+ log.verbose(this.title + 'preparing to run down:', file.name);
267
+ const _meta = file.get()._meta || {};
268
+ const version = _meta.version || 1;
269
+ this.internals.modSchema = { i: {}, c: {}, f: {}, s: [] };
270
+ return require(`./executors/versioned/v${version}`).down(
271
+ this,
272
+ this.driver,
273
+ file
274
+ );
275
+ });
240
276
  })
241
- .nodeify(callback);
277
+ ).nodeify(callback);
242
278
  },
243
279
 
244
280
  fix: function (options, callback) {
@@ -255,49 +291,56 @@ Walker.prototype = {
255
291
  return a.localeCompare(b);
256
292
  };
257
293
 
258
- const files = File.loadFromDatabase(
259
- this.directory,
260
- this.prefix,
261
- this._driver,
262
- this.internals
263
- ).then(completedFiles => {
264
- const toRun = dbmUtil.filterDown(completedFiles, partialName, count).sort(sortFn);
265
-
266
- if (toRun.length === 0) {
267
- log.info(this.title + 'Nothing to run');
268
- }
269
-
270
- if (this.internals.check) {
271
- const toRunNames = toRun.map(migration => migration.name);
272
- log.info(this.title + 'run:', toRunNames);
273
- return toRunNames;
274
- }
275
-
276
- return toRun;
277
- });
294
+ const determine = () =>
295
+ File.loadFromDatabase(
296
+ this.directory,
297
+ this.prefix,
298
+ this._driver,
299
+ this.internals
300
+ ).then(completedFiles =>
301
+ dbmUtil.filterDown(completedFiles, partialName, count).sort(sortFn)
302
+ );
278
303
 
279
304
  if (this.internals.check) {
280
- return files.nodeify(callback);
305
+ return determine()
306
+ .then(toRun => this._checkResult(toRun))
307
+ .nodeify(callback);
281
308
  }
282
309
 
283
- return files
284
- .each(file => {
285
- log.verbose(this.title + 'preparing to run fix:', file.name);
286
- const _meta = file.get()._meta || {};
287
- const version = _meta.version || 1;
288
- if (version < 2) {
289
- log.warn(`${this.title} skipping "${file.name}" as v1 migrations do not maintain a schema`);
290
- return Promise.resolve();
310
+ return Promise.resolve(
311
+ this._withLock(determine, toRun => {
312
+ if (toRun.length === 0) {
313
+ log.info(this.title + 'Nothing to run');
291
314
  }
292
315
 
293
- this.internals.modSchema = { i: {}, c: {}, f: {}, s: [] };
294
- return require(`./executors/versioned/v${version}`).fix(
295
- this,
296
- this.driver,
297
- file
298
- );
316
+ return Promise.each(toRun, file => {
317
+ log.verbose(this.title + 'preparing to run fix:', file.name);
318
+ const _meta = file.get()._meta || {};
319
+ const version = _meta.version || 1;
320
+ if (version < 2) {
321
+ log.warn(`${this.title} skipping "${file.name}" as v1 migrations do not maintain a schema`);
322
+ return Promise.resolve();
323
+ }
324
+
325
+ this.internals.modSchema = { i: {}, c: {}, f: {}, s: [] };
326
+ return require(`./executors/versioned/v${version}`).fix(
327
+ this,
328
+ this.driver,
329
+ file
330
+ );
331
+ });
299
332
  })
300
- .nodeify(callback);
333
+ ).nodeify(callback);
334
+ },
335
+
336
+ _checkResult: function (toRun) {
337
+ if (toRun.length === 0) {
338
+ log.info(this.title + 'Nothing to run');
339
+ }
340
+
341
+ const toRunNames = toRun.map(migration => migration.name);
342
+ log.info(this.title + 'run:', toRunNames);
343
+ return toRunNames;
301
344
  },
302
345
 
303
346
  check: function (options, callback) {
package/package.json CHANGED
@@ -31,7 +31,7 @@
31
31
  "programable",
32
32
  "api"
33
33
  ],
34
- "version": "1.0.0-beta.35",
34
+ "version": "1.0.0-beta.36",
35
35
  "engines": {
36
36
  "node": ">=8.0.0"
37
37
  },
package/tttt.js DELETED
@@ -1,2 +0,0 @@
1
-
2
-