Node.js race conditions
nodejsdesignpatterns.com
nodejsdesignpatterns.com
Take go, use gomaxprocs and set it to 1. Spawn a bunch of goroutines that share memory. And easily you'll have race conditions if you're not careful.
Readability of code > everything else.
https://github.com/golang/go/blob/master/src/sync/mutex.go
You can easily see what Go does here. It's not magic.
In general you highlight the problem though; without the central datastore ensuring atomicity, this isn't really solving anything.
I.e., if this app had multiple instances, you STILL have a problem, even with this solution, since nothing guarantees that another -instance- hasn't also loaded the old data.
That said, mutexes are also not always wrong. Compare-and-swap is good, ensuring atomicity is good, but sometimes a mutex is what you need, especially if you're fixing legacy code with concurrency issues.
I don't think a race condition is inherently an error. If you can design your program to accommodate races, including even data races, then allowing them and not spending time on protecting against them can be a useful performance optimisation.
It's only a bug if the race causes you to get into a state that you don't want.
Shouldn't be necessary if things are coded appropriately in the first place, and maybe better to just refactor, but...
If you're doing nothing with a promise (awaiting, or catching), then you have bad code.
You can read more about that here: https://palantir.github.io/tslint/rules/no-floating-promises...
> This can cause unexpected and/or non-deterministic behavior depending on external timing factors.
But, as this whole thread is about, sometimes you don't mind race conditions. So it can ok to acknowledge but ignore this advice if you are aware of how this race conditions matters to your codebase.
Learn the rules, but know when to break them.
The philosophy of SC and WritableConsumableStream is that the main trunk logic of your system/module should declared as a single serial stream and any parallel logic should explicitly branch off from it.
With traditional event listeners, there is no such thing as a 'main trunk' of the logic... The entry points (event listeners) are scattered all over the place and this can cause a lot of possible state permutations when business logic is executed inside those listeners (since you can't control the timing of when those event handlers are called).
This kind of programming is a good alternative to functional programming as it offers a safe way to asynchronously mutate state.
One thing I wish the article mentioned: In the given example, another solution would have been to make the balance change safe. E.g. `addBalance()` or `updateBalanceIfEqual(old, new)`. Requiring a mutex around each call site that needs to be used correctly (and keeps being used correctly over time) seems like a pretty fragile way out.
As the article hinted you have to either update the balance in one query or use a lock, like an optimistic lock.
Optimistic lock could have also been used instead of the mutex.
If you update the query, a lock is unnecessary.
I think it's fair to assume the parent was inferring that if you were using a DB transaction, you'd rewrite the logic to use it, rather than keeping the read and the write separate from each other, and therefore make a transaction pointless.
In which case, locks (like those mentioned in the article) don't work. Because instance A reads 0, instance B reads 0, instance A writes 50, instance B writes 50. Totally serialized on each instance, totally unpredictable in when they hit the DB.
Just like you have a lock serving as a synchronization point on an instance, in the case of multiple instances, you want to push the synchronization point down into a single instance at the data layer (which you often have when using a traditional RDBMS). But RDBMS' don't have locks, per se, for that synchronization; they have transactions that serve a similar purpose.
But, regardless of where that synchronization is, you have to ensure the update logic stays there. Having an interface that loads data, and saves data, at the application layer, ensures that a DB layer lock won't work, either. You have to change it to have the DB actually read and update the data to benefit from the synchronization the DB transaction provides you.
So, as I said before...a lock (at the application layer) is neither sufficient nor necessary. A DB transaction (with the implied update of the query necessary to make a transaction mean anything) is both.
The other is briefly mentioned with a link to Wikipedia and is a optimistic lock that is to be used with your database.
I talked about two use cases for optimistic locking, one applied with a database, that with will work with any number of instances, because the synchronization check lives in the database and not in the instance.
My other case was to use an optimistic lock instead of the mutex example, that will still only work with single instance because the synchronization check is local to the instance. (Edit: wrong, it could actually work there too, if saveBalance() was changed)
Optimistic locking is quite simple to implement, just check that the value has not changed before writing, if it has you have to reread the value again.
Databases do support locks, usually called pessimistic locking (opposite of optimistic locking). In a RDBMS that is usually a table lock or a row lock.
Table locks are usually bad for performance and row locks better, however both are clumsy to work with, this is where the simplicity of optimistic locking comes in.
Where pessimistic locking is part of the RDBMS itself, optimistic locking is something that you implement yourself on top of the RDBMS.
Optimistic locks are lightweight (optimal case one read and one write) and easy to implement and understand.
In the example in the article I would have argued for optimistic locking on all cases and it would have worked with multiple instances.
Transactions and locks does not serve a similar purpose.
This problem requires a compare-and-swap primitive, not mutexes.
I think you're possibly confusing 'race condition' with 'data race'. You need shared state for a data race, but not for a race condition.
For example if you send a request to two web services, then wait for responses from either, then that's a race condition on which result comes back first, because the non-deterministic timing can change program behaviour. And that's not even necessarily a problem or a bug.
You can easily get race conditions even in a system that tries to minimise shared state, such as Erlang, even though they protect you from data races.
If you can do that, just share an "account" object. Calling account.balance+=50 doesn't require a mutex.
Is it just me? Firefox seems to load them just fine.
Do you have JS disabled?