Checkout Server Rotation

MediumdesignAlso known as Round-Robin and Least-Connections Load Balancer

An online shop runs its checkout service on a few identical servers behind a load balancer. Most requests take a few milliseconds, but a checkout that waits on a card payment can hold its connection for seconds. Health checks mark servers down and up again, and a server that goes down still has to finish the checkouts it already holds.

The balancer can hand out requests in turn (round robin) or send each one to the server with the fewest open connections (least connections). These are the two strategies from Load Balancing; you build both behind one switch so you can compare them.

Your task

Write a class Balancer:

  • constructor(servers, strategy): servers lists distinct server names in a fixed order; all start up, with 0 open connections. strategy is "round-robin" or "least-connections".
  • route(): pick a server that is up, add one open connection to it, and return its name. If every server is down, return null (None in Python) and change nothing.
  • finish(server): one connection on that server closes. Ignore it if the server has 0 open connections.
  • setHealth(server, up): mark the server up (true) or down (false). Its open connections stay open either way.
  • open(server): return the server's number of open connections.

Round robin keeps a cursor that starts at position 0. route looks at the server at the cursor, then the next ones, wrapping around the end of the list, and picks the first that is up. The cursor then moves to the position just after the picked server.

Least connections picks the up server with the fewest open connections; on a tie, the one that comes first in servers.

class Balancer

Examples

Example 1
Input
new Balancer(["s1", "s2", "s3"], "round-robin")
.route()
.route()
.setHealth("s2", false)
.route()
.route()
.route()
.open("s3")
Output
[null, "s1", "s2", null, "s3", "s1", "s3", 2]
Why

Round robin hands out s1, then s2. Then s2 is marked down. The cursor is on s3, so s3 is next, and the cursor wraps to s1. After s1 the cursor is on s2, which is down, so the balancer skips to s3. s3 now has 2 open connections because nothing has finished.

Example 2
Input
new Balancer(["s1", "s2", "s3"], "least-connections")
.route()
.route()
.route()
.finish("s2")
.route()
.route()
.setHealth("s2", false)
.route()
Output
[null, "s1", "s2", "s3", null, "s2", "s1", null, "s3"]
Why

All start at 0, so ties go to the first: s1, then s2, then s3, one connection each. s2 finishes a request and drops to 0, so it gets the next one. Now all three have 1, and the tie goes to s1, which reaches 2. Then s2 is marked down. Of the servers still up, s3 has 1 open connection and s1 has 2, so s3 gets the request.

Constraints

  • 1 ≤ servers.length ≤ 50
  • Server names are distinct, non-empty strings; finish, setHealth and open always name a server from the list.
  • At most 2,000 calls per test
Start with brute force

A first version of round robin often keeps a counter, starts at servers[counter % n], steps forward with a while loop until it finds a server that is up, and then adds 1 to the counter. It loops forever when every server is down. It also gives the server after a dead one a double share: with a, b, c and b down, counter 1 lands on b and moves on to c, then counter 2 lands on c again.

Scanning forward from a cursor that sits just after the last pick fixes both: the scan is bounded by n, and the turn passes on in order. With at most 50 servers, an O(n) scan per request is fine; a balancer with thousands of backends would keep a heap or buckets of connection counts for least connections.

Hints

0 of 4 shown

Walkthrough and solution

Unlocks when you pass every test. Stuck for a while? Looking is fine too, as long as you then close it and write the code yourself.

Ctrl+Enter runs

Run your code to see results here. Run checks the examples. Submit checks every test, including hidden edge cases.

Write code