Skip to content

Commit

Permalink
Adding soft lock feature
Browse files Browse the repository at this point in the history
  • Loading branch information
daveboulard committed May 4, 2021
1 parent 4906ff2 commit cb3a8b7
Show file tree
Hide file tree
Showing 7 changed files with 264 additions and 8 deletions.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,12 @@ module.exports = {
// Enable the algorithm to create a checksum of the file contents and use that in the comparison to determin
// if the file should be run. Requires that scripts are coded to be run multiple times.
useFileHash: false

// The mongodb collection where the lock will be created.
lockCollectionName: "changelog_lock",

// The value in seconds for the TTL index that will be used for the lock. Value of 0 will disable the feature.
lockTtl: 0
};
````

Expand Down
17 changes: 15 additions & 2 deletions lib/actions/down.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,29 @@
const _ = require("lodash");
const { promisify } = require("util");
const fnArgs = require('fn-args');
const fnArgs = require("fn-args");

const status = require("./status");
const config = require("../env/config");
const migrationsDir = require("../env/migrationsDir");
const hasCallback = require('../utils/has-callback');
const hasCallback = require("../utils/has-callback");
const lock = require("../utils/lock");

module.exports = async (db, client) => {
const downgraded = [];
const statusItems = await status(db);
const appliedItems = statusItems.filter(item => item.appliedAt !== "PENDING");
const lastAppliedItem = _.last(appliedItems);

if (await lock.exist(db)) {
throw new Error("Could not migrate down, a lock is in place.");
}

try {
await lock.activate(db);
} catch(err) {
throw new Error(`Could not create a lock: ${err.message}`);
}

if (lastAppliedItem) {
try {
const migration = await migrationsDir.loadMigration(lastAppliedItem.fileName);
Expand All @@ -26,6 +37,7 @@ module.exports = async (db, client) => {
}

} catch (err) {
await lock.clear(db);
throw new Error(
`Could not migrate down ${lastAppliedItem.fileName}: ${err.message}`
);
Expand All @@ -40,5 +52,6 @@ module.exports = async (db, client) => {
}
}

await lock.clear(db);
return downgraded;
};
17 changes: 15 additions & 2 deletions lib/actions/up.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,29 @@
const _ = require("lodash");
const pEachSeries = require("p-each-series");
const { promisify } = require("util");
const fnArgs = require('fn-args');
const fnArgs = require("fn-args");

const status = require("./status");
const config = require("../env/config");
const migrationsDir = require("../env/migrationsDir");
const hasCallback = require('../utils/has-callback');
const hasCallback = require("../utils/has-callback");
const lock = require("../utils/lock");

module.exports = async (db, client) => {
const statusItems = await status(db);
const pendingItems = _.filter(statusItems, { appliedAt: "PENDING" });
const migrated = [];

if (await lock.exist(db)) {
throw new Error("Could not migrate up, a lock is in place.");
}

try {
await lock.activate(db);
} catch(err) {
throw new Error(`Could not create a lock: ${err.message}`);
}

const migrateItem = async item => {
try {
const migration = await migrationsDir.loadMigration(item.fileName);
Expand All @@ -31,6 +42,7 @@ module.exports = async (db, client) => {
);
error.stack = err.stack;
error.migrated = migrated;
await lock.clear(db);
throw error;
}

Expand All @@ -49,5 +61,6 @@ module.exports = async (db, client) => {
};

await pEachSeries(pendingItems, migrateItem);
await lock.clear(db);
return migrated;
};
42 changes: 42 additions & 0 deletions lib/utils/lock.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
const config = require('../env/config');

async function getLockCollection(db) {
const { lockCollectionName, lockTtl } = await config.read();
if (lockTtl <= 0) {
return null;
}

const lockCollection = db.collection(lockCollectionName);
lockCollection.createIndex({ createdAt: 1 }, { expireAfterSeconds: lockTtl });
return lockCollection;
}

async function exist(db) {
const lockCollection = await getLockCollection(db);
if (!lockCollection) {
return false;
}
const foundLocks = await lockCollection.find({}).toArray();

return foundLocks.length > 0;
}

async function activate(db) {
const lockCollection = await getLockCollection(db);
if (lockCollection) {
await lockCollection.insertOne({ createdAt: new Date() });
}
}

async function clear(db) {
const lockCollection = await getLockCollection(db);
if (lockCollection) {
await lockCollection.deleteMany({});
}
}

module.exports = {
exist,
activate,
clear,
}
6 changes: 6 additions & 0 deletions samples/migrate-mongo-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ const config = {
// The mongodb collection where the applied changes are stored. Only edit this when really necessary.
changelogCollectionName: "changelog",

// The mongodb collection where the lock will be created.
lockCollectionName: "changelog_lock",

// The value in seconds for the TTL index that will be used for the lock. Value of 0 will disable the feature.
lockTtl: 0,

// The file extension to create migrations and search for in migration dir
migrationFileExtension: ".js",

Expand Down
92 changes: 90 additions & 2 deletions test/actions/down.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ describe("down", () => {
let down;
let status;
let config;
let lock;
let migrationsDir;
let db;
let client;
let migration;
let changelogCollection;
let changelogLockCollection;

function mockStatus() {
return sinon.stub().returns(
Expand All @@ -31,7 +33,11 @@ describe("down", () => {
function mockConfig() {
return {
shouldExist: sinon.stub().returns(Promise.resolve()),
read: sinon.stub().returns({ changelogCollectionName: "changelog" })
read: sinon.stub().returns({
changelogCollectionName: "changelog",
lockCollectionName: "changelog_lock",
lockTtl: 10
})
};
}

Expand All @@ -45,6 +51,7 @@ describe("down", () => {
const mock = {};
mock.collection = sinon.stub();
mock.collection.withArgs("changelog").returns(changelogCollection);
mock.collection.withArgs("changelog_lock").returns(changelogLockCollection);
return mock;
}

Expand All @@ -66,24 +73,48 @@ describe("down", () => {
};
}

function mockChangelogLockCollection() {
const findStub = {
toArray: () => {
return [];
}
}

return {
insertOne: sinon.stub().returns(Promise.resolve()),
createIndex: sinon.stub().returns(Promise.resolve()),
find: sinon.stub().returns(findStub),
deleteMany: sinon.stub().returns(Promise.resolve()),
}
}

function loadDownWithInjectedMocks() {
return proxyquire("../../lib/actions/down", {
"./status": status,
"../env/config": config,
"../env/migrationsDir": migrationsDir
"../env/migrationsDir": migrationsDir,
"../utils/lock": lock,
});
}

function loadLockWithInjectedMocks() {
return proxyquire("../../lib/utils/lock", {
"../env/config": config
});
}

beforeEach(() => {
migration = mockMigration();
changelogCollection = mockChangelogCollection();
changelogLockCollection = mockChangelogLockCollection();

status = mockStatus();
config = mockConfig();
migrationsDir = mockMigrationsDir();
db = mockDb();
client = mockClient();

lock = loadLockWithInjectedMocks();
down = loadDownWithInjectedMocks();
});

Expand Down Expand Up @@ -179,4 +210,61 @@ describe("down", () => {
const items = await down(db);
expect(items).to.deep.equal(["20160609113225-last_migration.js"]);
});

it("should lock if feature is enabled", async() => {
await down(db);
expect(changelogLockCollection.createIndex.called).to.equal(true);
expect(changelogLockCollection.find.called).to.equal(true);
expect(changelogLockCollection.insertOne.called).to.equal(true);
expect(changelogLockCollection.deleteMany.called).to.equal(true);
});

it("should ignore lock if feature is disabled", async() => {
config.read = sinon.stub().returns({
changelogCollectionName: "changelog",
lockCollectionName: "changelog_lock",
lockTtl: 0
});
const findStub = {
toArray: () => {
return [{ createdAt: new Date() }];
}
}
changelogLockCollection.find.returns(findStub);

await down(db);
expect(changelogLockCollection.createIndex.called).to.equal(false);
expect(changelogLockCollection.find.called).to.equal(false);
});

it("should yield an error when unable to create a lock", async() => {
changelogLockCollection.insertOne.returns(Promise.reject(new Error("Kernel panic")));

try {
await down(db);
expect.fail("Error was not thrown");
} catch (err) {
expect(err.message).to.deep.equal(
"Could not create a lock: Kernel panic"
);
}
});

it("should yield an error when changelog is locked", async() => {
const findStub = {
toArray: () => {
return [{ createdAt: new Date() }];
}
}
changelogLockCollection.find.returns(findStub);

try {
await down(db);
expect.fail("Error was not thrown");
} catch (err) {
expect(err.message).to.deep.equal(
"Could not migrate down, a lock is in place."
);
}
});
});
Loading

0 comments on commit cb3a8b7

Please sign in to comment.