- Request from any client first reaches application load balancer which works on round robin principle which rotates connection requests among web servers in the order that requests are received.
- Each application/replica is connected to its own redis key-value store and can handle SET / GET requests forwarded by the load balancer.
- Every application/replica is connected to a central total-order-multicast server. An application can emit a broadcast message event to a tom server, then the tom server broadcasts this message to all its connected clients. And the clients which connected to tom server are our replicas itself.
- Find the detailed code under
/sequential/app.js
- Implemented as a REST API endpoint, reads the key from its own redis kvstore and immediately returns the value.
app.get("/api/get/:key", async (req, res) => {
/*
Query the redis kvstore with key and sends back the value to the load balancer which in turn sends back to client.
*/
});
- Find the detailed code under
/sequential/app.js
- This works on sockets as we cannot send the response immediately, it places a broadcast request to the tom server.
handleWrite
is executed when the tom controller sends the message to the application. If the current server is the initiator of this message then the response is sent back to the load balancer which in turn sends back to client.
async function handleWrite(data) {
/*
write in the local redis key value store
*/
if (initiator == PORT) {
io.emit("set-response", { ...data, message });
}
}
socket.on("set", (data) => {
console.log(`SET recieved from load balancer on App:${PORT}`);
tom.broadcast({
event: "set",
key: data.key,
value: data.value,
userId: data.userId,
initiator: PORT,
});
});
- Find the detailed code under
/linearizable/app.js
- This works on sockets as we cannot send the response immediately, it places a broadcast request to the tom server.
handleRead
is executed when the tom controller sends the message to the application. If the current server is the initiator of this message then the response is sent back to the load balancer which in turn sends back to client.
async function handleRead(data) {
/*
read from the local redis key value store
*/
if (initiator == PORT) {
io.emit("get-response", { ...data, value, message });
}
}
socket.on("get", (data) => {
console.log(`GET recieved from load balancer on App:${PORT}`);
tom.broadcast({
event: "get",
key: data.key,
userId: data.userId,
initiator: PORT,
});
});
- Find the detailed code under
/linearizable/app.js
- This works same as explained in sequential SET.
- Find the detailed code under
/eventual/app.js
- This works same as explained in sequential GET.
- Find the detailed code under
/eventual/app.js
- Implemented as a REST API endpoint, writes the key value pair to its own redis kvstore and immediately returns the response.
- After the response is returned it places a broadcast request to the tom server. This is done to ensure that there are no conflicting updates across the replicas.
- For example instead of total-order-broadcast if we used a simple update request. Let say
replica-1
updates to(k1, v1)
and sendsupdate(k1, v1)
toreplica-2
and similarlyreplica-2
updates to(k2, v2)
and sendsupdate(k2, v2)
toreplica-1
. Therefore,replica-1
ends with(k2, v2)
andreplica-2
with(k1, v1)
. Hence to avoid this conflict we use total order broadcast and we dont wait for the broadcast process to complete as we only need eventual consistency.
app.post("/api/set", async (req, res) => {
const { key, value } = req.body;
const result = await handleWrite(req.body);
tom.broadcast({
event: "set",
key,
value,
});
res.status(200).json({
message: result.message,
performedBy: `Request performed by App:${PORT}`,
});
});
There are 5 users/clients running parallely, the test suite makes multiple requests and records the responses receieved for each client.
User-1:
- Makes a SET request to replica
3001
withkey = name
andvalue = batman
- Makes a GET request to replica
3002
withkey = name
, even before the SET response is received. - We immediately received a response with
value = null
. From this we can understand that the SET request we made earlier still did not reach the replica3002
. - Next we received the success response for the SET request made earlier.
User-2:
- Makes a SET request to replica
3001
withkey = name
andvalue = superman
- Makes a GET request to replica
3002
withkey = name
, even before the SET response is received. - We immediately received a response with
value = null
. From this we can understand that the SET request we made earlier still did not reach the replica3002
. - Next we received the success response for the SET request made earlier.
User-3:
- Makes 5 continuous GET requests to replica
3001
. Each request is made with a gap of 600 ms. - Initially the first two requests returned
null
. This means the GET request is too fast that none of the SET requests from other users still did not complete yet. - The third request returned
value = superman
. This means the SET request from user2 could have happened before SET request from user1. - The fourth request returned
value = batman
. This means the SET request from user1 reached the application. - The fifth request returned
value = batman
. This is expected becausebatman
was the last value we set. - We can see that user1 and user2 were concurrent. user1 sent
superman
and user2 sentbatman
. there could be any orderset(name, superman)
followed byset(name, batman)
or viceversa. This order is decided by our total order broadcast protocol.
User-4 and User-5:
- user4 and user5 makes 5 continuous GET requests to replica
3002
and3003
with a gap of 600 ms respectively. - We can clearly observe the values transitioning from
null
tosuperman
tobatman
simillar to that we have seen in user3. Hence total order is ensured at each replica as the order of operations are same.
-
Following are the requests sent. All the requests are sent immediately without waiting for any of their responses. The recorded responses are observed in the above image.
get(fruit)
to replica3001
set(fruit, apple)
to replica3001
with a delay of 10msset(fruit, mango)
to replica3002
set(fruit, banana)
to replica3003
get(fruit)
at replica3001
get(fruit)
at replica3001
with delay of 20ms -
We can interpret the following order of requests performed from the responses receieved:
get(fruit)
,set(fruit, mango)
,set(fruit, banana)
,get(fruit)
,set(fruit, apple)
andget(fruit)
. -
Talking about the last but one
get(fruit)
which returnedbanana
.banana
was set at replica3003
after it completed the broadcasts at all other replicas then only the currentget(fruit)
was performed at replica3001
, this shows that all set/get operations follow the total-order, hence we received the changed fruit value. -
The last
get(fruit)
returnedapple
. we can see thatset(fruit, apple)
was the last set operation because it had a delay of 10ms, andget(fruit)
had a delay of 20ms therefore, we receievedapple
.
User-1:
- Makes a SET request to replica
3001
withkey = vehicle and value = bike.
- We immediately recieve a successful response from
3001
. And internally the replicas become eventually consistent.
User-2:
- Makes a SET request to replica
3002
withkey = vehicle
andvalue = car
. - We immediately recieve a successful response from
3002
. And internally the replicas become eventually consistent.
User-3:
- Makes 5 continuous GET requests to replica
3001
. Each request is made with a gap of 500 ms. - The first four requests returned
bike
and last request returnedcar
. - We can assume that due to user1 the value became bike at
3001
and eventually value becomescar
so that all the replicas are consistent with same valuecar
. We will see the other user responses to confirm this.
User-4:
- Makes 5 continuous GET requests to replica
3002
. Each request is made with a gap of 500 ms. - The first three requests returned
car
and the 4th request returnedbike
then the 5th request returnedcar
. - We can assume that due to user2 the value became
car
at3002
and eventually due to broadcast from user1 it becamebike
and then due to broadcast from itself it ends up with valuecar
.
User-5:
- Makes 5 continuous GET requests to replica
3003
. Each request is made with a gap of 500 ms. - The first three requests returned
null
and the 4th request returnedbike
then the 5th request returnedcar
. - As there were no local requests to
3003
the initial values arenull
(stale). Eventually due to the broadcasted messages from user1 and user2 the value changed tobike
and thencar
respectively.
We can clearly see that all the replicas eventually converge to a consistent value bike
.