Skip to content
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

Issues with cross-process concurrency #20

Closed
sheerun opened this issue Jul 23, 2014 · 10 comments
Closed

Issues with cross-process concurrency #20

sheerun opened this issue Jul 23, 2014 · 10 comments

Comments

@sheerun
Copy link

sheerun commented Jul 23, 2014

It's connected with sindresorhus/insight#22

Configstore doesn't lock config storage before writing to it. It can result in invalid YAML when instantiating configstore in different processes concurrently.

If configstore encounters invalid YAML, it cleans the store, resulting in sindresorhus/insight#22

The solution is to use file lock around any filesystem call (mkdir, write, read).

@sindresorhus
Copy link
Owner

When I created this I assumed the file system did atomic writes, but clearly not. Seems weird that fs can't handle this for you.

@dfreeman
Copy link

I spent a little time looking at this today. Unfortunately, it looks like the only way to incorporate a file lock would be to make the entire configstore API asynchronous.

Given that this is already a fairly small library, a change like that would essentially amount to a complete rewrite. There's also something to be said for the simplicity of the synchronous interface for use cases that aren't concerned with concurrent access.

Do you have any thoughts on what the best path forward on this might be? I'm considering the possibility of just forking/building an asynchronous concurrent-access-safe version of configstore, but wanted to get your insights first.

@sindresorhus
Copy link
Owner

I don't see why it would have to be async. Can you elaborate?

@dfreeman
Copy link

If the lock is already held by another process when we go to read/write the config file, then the two options are either to give up entirely, or to wait some amount of time and check the lock again.

In the "just give up" option, clients of the library have to be prepared to handle a "lock already taken" exception potentially being raised by any configstore operation. They then have to implement their own logic to either manually retry after some period of time or otherwise handle the failure. This avoids data corruption, but pushes the problem of actually dealing with concurrent use on to the users of the library.

For the "automatically retry" option, we don't want to just hammer the filesystem in a loop checking the lock, so we need to wait some period of time between checks (proper-lockfile does this with nice exponential backoff). The only way I can think of to do this synchronously would be to roll our own file lock implementation that uses fibers or some other native extension to do a synchronous wait. Both the complexity of building a home-grown file lock and introducing the need for native bindings feel like points against that idea.

There could definitely be another alternative I'm not seeing, but the only other one I've thought of is an asynchronous interface.

@sindresorhus
Copy link
Owner

Another alternative could be to use atomic file writing and just let the last concurrent write win. One concurrent write will overwrite another, but that might not be such a big problem?

// @SBoudrias @sheerun

@SBoudrias
Copy link
Contributor

I think having the last concurrent write win is good enough.

About the point about the automatically retry, the dummy implementation is to lock the process with a loop: while (now < t300msLater) {}. This is blocking the process - but that's kind of the point of a synchronous API. That would be way easier than starting to play with fibers.

@sheerun
Copy link
Author

sheerun commented Jul 26, 2015

I have no idea how to implement "last concurrent write win" behavior :) With node-proper-lockfile it's probably feasible to implement "first concurrent write win", because subsequent calls to lockSync would throw an error..

@sindresorhus
Copy link
Owner

@sheerun We can use atomic file writing: https://github.com/iarna/write-file-atomic It works by writing to a temporary file first, then renaming and overwriting the actual file when done. We would then get "last concurrent write win" for free.

@sheerun
Copy link
Author

sheerun commented Jul 26, 2015

That's certainly better than corrupted configuration files :)

@dfreeman
Copy link

@SBoudrias Blocking the process is the point of a synchronous API, but using a busy loop that pegs one CPU core seems less than ideal.

@sindresorhus Atomic writes definitely solve the corruption problem, and like you mentioned, it's easy to drop in a package to handle that. If you decide just avoiding total corruption is good enough for this project, then 👍.

In casual testing, though, the race condition I hit more often was caused by the read-mutate-write sequences in set and del – competing processes end up clobbering one another's changes when those steps interleave, so you get data loss. Not as dire as straight-up corrupting the file, but has a way bigger window it can happen in, so ¯_(ツ)_/¯

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants