Photo Cache Ring

HarddesignAlso known as Consistent Hash Ring

A photo site keeps thumbnails on a handful of cache servers. With hash(key) mod N, adding a fifth server to four would move about 4 in 5 keys to a different server, and that flood of cache misses would land on the database at once. A hash ring moves only the keys next to the new server's points. Each server is placed at several points on the ring (virtual nodes) so the load evens out, and a key can be stored on the next few distinct servers clockwise, so a copy survives when one server dies.

This is the ring from Consistent Hashing, used to spread keys across cache servers (Caching) and to choose replicas in leaderless key-value stores (Design a Key-Value Store).

Your task

The ring has positions 0 to 232 − 1. Positions come from ringHash(s), a 32-bit FNV-1a hash followed by a mixing step. It is given in the starter code in both languages, so JavaScript and Python agree exactly; use it as it is.

Write a class HashRing:

  • constructor(vnodes): every server gets vnodes points.
  • addNode(name): add the server's points. Point i (from 0 to vnodes − 1) has the label name + "#" + i and sits at ringHash(label). Adding a server that is already on the ring does nothing.
  • removeNode(name): remove all of that server's points. Removing a server that is not on the ring does nothing.
  • owners(key, count): start at position ringHash(key) and walk clockwise: the first point at or after that position, then the next, wrapping from the highest position back to the lowest. Collect server names in the order you meet them, skipping servers already collected, until you have count names or have seen every server. Return the list. An empty ring gives [].

If two points share a position, the one whose label sorts first (comparing character codes) comes first clockwise.

class HashRing

Examples

Example 1
Input
new HashRing(2)
.addNode("cache-a")
.addNode("cache-b")
.addNode("cache-c")
.owners("sun.jpg", 1)
.owners("sun.jpg", 2)
.owners("photo-3", 3)
.removeNode("cache-b")
.owners("sun.jpg", 1)
.owners("photo-3", 3)
Output
[null, null, null, null, ["cache-b"], ["cache-b", "cache-c"], ["cache-a", "cache-c", "cache-b"], null, ["cache-c"], ["cache-a", "cache-c"]]
Why

In millions, the six points sit at a#0 25.1, a#1 152.6, c#1 414.5, b#0 616.2, b#1 1,845.2 and c#0 3,752.1. "sun.jpg" hashes to 476.3, so the first point clockwise is b#0: owner cache-b. For two owners the walk goes on to b#1, which is cache-b again and is skipped, then c#0: cache-c. "photo-3" hashes to 3,939.4, past the last point, so the walk wraps to a#0, then a#1 (skipped), c#1, b#0. After cache-b leaves, "sun.jpg" moves to the next point, c#0, while "photo-3" still starts at cache-a: only keys that cache-b owned have moved. With two servers left, asking for 3 owners returns 2.

Example 2
Input
new HashRing(1)
.owners("cat.jpg", 2)
.addNode("solo")
.addNode("solo")
.owners("cat.jpg", 2)
.removeNode("ghost")
.owners("cat.jpg", 1)
Output
[null, [], null, null, ["solo"], null, ["solo"]]
Why

An empty ring has no owners. Adding "solo" twice still leaves one point, so even with 2 owners requested only one server exists. Removing a server that was never added changes nothing.

Constraints

  • 1 ≤ vnodes ≤ 50
  • At most 20 servers on the ring at once; names are 1 to 20 letters, digits or hyphens
  • Keys are 1 to 40 printable ASCII characters
  • 1 ≤ count ≤ 20
  • At most 1,000 calls per test
Start with brute force

For each lookup you could compute the clockwise distance from the key to every point, (point − key) mod 232, and sort. That is correct but costs O(P log P) per lookup for P points: with 20 servers × 50 points, 1,000 points sorted for every single cache read.

Keeping the points sorted once, when servers join or leave (rare), turns each lookup into a binary search plus a short walk.

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