50.003 - Concurrency and Concurrent Testing¶
Learning Outcomes¶
- Eliminate race condition in web application via transaction
- Perform concurrent testing
Concurrency¶
Concurrency is ubiquitous in modern software systems, web application, mobile applications, chat bots, etcs are required to support multiple and concurrent users acess. As we learned from other modules, such 50.001 and 50.005, software concurrency imposes many challenges, e.g. race conditions, deadlock, starvation and etc.
Codes that exhibits correct behavior when being executed sequentially are not necessarily correct when being executed concurrently or interleaved by instructions. This implies that we need a different testing strategy when hunting for bugs in concurrent software system.
Race condition in JavaScript¶
Racing condition examples found in the literatures involve the usage of threads and shared resources. Since JavaScript does not offer multi-threading operation, one may optimistically think that programs written in JavaScript will not suffer from the curse of race condition and its related consequences. Unfortunately, such a optimism is 100% wrong in reality.
- a
get_bal()
promise builder function which returns a promise that resolves on the account's balance. - a
set_bal()
promise builder function which returns a promise that updates the account's balance.
Next we define a transfer()
function which transfer some amount from two accounts, by calling get_bal()
and set_bal()
Finally, we define our main function and the aftermath observation
main()
function, we sequentially transfer 100 from b
to a
then transfer 50 from c
to a
.
We observe the closing balances as 250 100 100
.
This is fine because the two transfer operations were carried out in sequence, (enforced by two await
).
Suppose we change the main
function as follows,
or
150 100 100
. A racing condition is triggered.
One may argue that the above variants of main()
are unusual, after some code review, we perhaps can eliminate the bugs. However, in the event that the two transfer operations are triggered from separate server events, e.g. from two different HTTP request handlers, such a racing condition is inevitable.
Concurrent Testing¶
Now we are convinced that the racing condition is ubiqitious, the next question is to how to test our codes such that we can unearth the potential critical sections with racing condition.
For instance, in the echo app model file message.js
we would like to define a new operation which first search for a message by the timestamp in the database, update it if the message exists.
In the event of concurrent access, such a find-and-update operation can lead to a racing condition.
In other languages like Java, we can create test case with multiple threads running the concurrent section multiple of times and validate the final result is a semantically correct one.
In JavaScript, we can construct such a concurrent test case using Promise.all()
,
'2009-01-01:00:00:00'
by appending an a
. After running instances of the task concurrently, we should have the message msg aaa
.
However, running the above test multiple of times, we find it fails sometimes, with the following error.
To fix the racing condition on the database level.¶
We can address such a data racing condition at the database level. In many modern database systems, we find database transaction typically useful in managing concurrent read and update combo operations.
In a nutshell, SQL statements enclosed byBEGIN TRANSACTION; ... COMMIT;
are to be executed by the datbase systems atomically and in isolation. Informally speaking, the enclosed statements should not be interleaved with other statements that cause a racing condition. In the event a racing condition is detected, a database exception is raised and the query statements are rejected. A retry or an abort is required.
Let's rewrite the above unsafe update as follows,
-
we instantiate a specific connection object from the
db.pool
object. We replace all the use ofdb.pool.query()
byconnection.query()
.If we use
db.pool.query()
directly, each time a new connection is instantiated and there is no way to ensure the set of queries are enclosed in the same transaction block. -
We then declare the transaction isolation level.
SERIALIZABLE
means any READ-WRITE operation pairs applied on the same table from different transactions are considered in-conflict. For details please refer tohttps://dev.mysql.com/doc/refman/8.0/en/innodb-transaction-isolation-levels.html#isolevel_read-uncommitted
- We enclose the critical section with the
START TRANSACTION
andCOMMIT
. In case of a conflict is deteced, we rollback the transaction.
With the above changes, our concurrent test should be always passed .
Further reading on transactions¶
- https://mongoosejs.com/docs/transactions.html
- https://sequelize.org/docs/v6/other-topics/transactions/