-
Notifications
You must be signed in to change notification settings - Fork 165
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Feature/soft lock #262
base: master
Are you sure you want to change the base?
Feature/soft lock #262
Conversation
This can be very useful. |
This feature is very interesting! |
@daveboulard There is a conflict with the README.md file. Please resolve this. |
6ef54c9
to
cb3a8b7
Compare
@daveboulard Great feature, thank you! |
@daveboulard Thanks a lot for this feature ! |
I've integrated your PR into my fork: |
Could you shed some light on the status of this feature? |
What is missing to merge this? It's 3 years since the PR was opened. I would be very interested in this feature. |
@seppevs please merge, will help a lot of people running multi instance environments |
I'm actually surprised that a migration tool does not have locking functionality. This means that migrations will run multiple times in any horizontally scaled environment, not only is that inefficient but could possibly break the migrations if the migration script does not account for this. That being said I think the implementation here is fundamentally flawed in that it introduces a mutex lock that is not thread safe, introducing a race condition where multiple instances of a service can create multiple locks and enter the locked section of code. What I believe would provide an atomic lock operation is using upsert with async function activate(db) {
const lockCollection = await getLockCollection(db)
if (!lockCollection) throw new Error('Cannot get lock collection')
// This command simultaneously checks if a lock exists and inserts one if no lock exists in a single atomic operation
const { upsertedCount } = await lockCollection.update(
{ lock: true }, // Some static value to match on
{ $setOnInsert: { lock: true, createdAt: new Date() } }, // $setOnInsert will not update any existing locks
{ upsert: true }
)
return upsertedCount === 1
} Then when using this lock we only need to perform a single action to 1. Check if the lock exists and 2. Create the lock if it doesn't exist const lockObtained = await lock.activate(db)
if (!lockObtained) {
throw new Error("Could not migrate up, a lock is in place.");
}
console.log('This code is now locked and thread safe')
await lock.clear(db) |
note: I'm not the author of this PR or know the author. Node is single-threaded, so I don't think the argument applies here. We've been running this code for over a year or more at this point in a h-scaled environment without any issues. Without the code, we do experience the issue this code was designed to fix (which is why we started to use it). |
@theogravity Yes Node is single threaded, but multiple node instances are not "thread safe" when operating on an external DB. In the case of a single node instance, locks are not needed. For example, when using node cluster. Each worker is operating on a separate thread. So it is possible that 2 instances will reach the The same applies for any horizontally scaled environment such as micro services. I'm not saying this implementation will not work, I'm sure it will work fine >99% of the time. BUT there is still a race condition here and could lead to unexpected issues. |
I don't think that's an issue when you look into how mongodb does locking: Doing an update would use an intent exclusive write lock against the collection "This means, that if the update operation would take a considerable amount of time, any find operation would be blocked during the duration of the exclusive lock is in place." It says "considerable amount of time", but doesn't specify what happens if it doesn't take a considerable amount of time though. All in all, it definitely doesn't hurt to include your code as extra safety. Considering we have an h-scaled application that performs a ton of read / writes outside of migrations from multiple node processes, you would think this would cause a huge amount of issues, but we just don't see them and that's probably thanks to mongo's locking mechanism under the hood. |
Yes, mongodb locks while doing the update which is why the solution proposed by SeanReece is safe. It doesn't lock beween the find and the insert so the current code change isn't really safe. |
When automating the migration process this tool might really comes in handy and alleviate a lot of pain.
Especially in a micro-services architecture with a lot of environments.
Nevertheless, depending on the deployment process, one might have to trigger the automated migration just right before the start of the application.
And if there are multiple applications running in parallel, multiple deployment might occur in parallel. Thus, the risk of having migration running in parallel is real and can lead to a lot of drama.
So, in order to avoid that, I propose a little feature which I would call "soft lock" relying on the TTL indexes mongodb provides (https://docs.mongodb.com/manual/core/index-ttl/).
The idea is to have a collection which should contain maximum one document. A fetch on this collection is done at the start of a migration.
When the process finishes - or has failed - this document is removed.
If anything goes wrong and the lock is not cleared, the TTL index will take care of it.
npm test
passes and has 100% coverage