Problem contributed by @asdf
Implement a HashRing, a consistent hashing ring that routes chat sessions to servers.
Description
Design a distributed chat load balancer using a consistent hashing ring — a circle of integers in the range [0, 2^32). Both servers and chats are assigned positions using a hash function. A chat is routed to the first server clockwise from its position on the ring, with wraparound.
This problem is divided into multiple parts. Each part builds on the previous one.
A hash function is provided — treat it as a black box:
int hash_fn(string key); // provided, do not implement
Tiebreak rule (applies to all parts): When scanning the ring clockwise, distinct physical servers are visited in clockwise order starting from the primary. If two ring positions collide on the same hash, ties are broken lexicographically by server_id. Test data will not rely on collision tiebreaks for correctness, but your implementation must be deterministic.
Constructor
HashRing()
- Initializes an empty hash ring with no servers.
Part 1: Basic Consistent Hashing
add_server(server_id)
- Adds a server to the ring at position
hash_fn(server_id). - If the server already exists, do nothing.
remove_server(server_id)
- Removes the server from the ring.
- If the server does not exist, do nothing.
get_server(chat_id) -> str
- Hashes
chat_idand returns the first server clockwise from that position on the ring.
Part 2: Virtual Nodes
A single ring position per server leads to uneven load distribution. Extend your implementation to support virtual nodes: each physical server gets v positions on the ring at:
hash_fn(server_id + "#0"), hash_fn(server_id + "#1"), ..., hash_fn(server_id + "#v-1")
Modify add_server and remove_server to handle virtual nodes. get_server behavior is unchanged — it still returns the physical server id.
add_server(server_id, v)
- Adds a server with
vvirtual nodes at the positions described above. - If the server already exists, do nothing.
- The single-argument
add_server(server_id)from Part 1 is equivalent toadd_server(server_id, 1)with the position computed fromhash_fn(server_id)directly (no#0suffix).
remove_server(server_id)
- Removes the server and all of its virtual nodes from the ring.
Part 3: Server Affinity
Extend routing to support affinity-based fallback. When looking up a chat, instead of always returning the primary ring server, scan up to k distinct physical servers clockwise (including the primary) and prefer servers that have previously handled this chat.
record_affinity(server_id, chat_id)
- Records that
server_idhas an affinity forchat_id.
get_server_v3(chat_id, k) -> str
- Scans up to
kdistinct physical servers clockwise fromchat_id's ring position (including the primary). - If
kis greater than the number of distinct physical servers on the ring, the scan visits every server exactly once. - If any scanned server has a recorded affinity for
chat_id, return the first such server in clockwise order. - Otherwise, return the primary ring server as in Part 2.
Affinities are tied to a server's identity. If a server is removed, its affinities are discarded; re-adding the same server_id later starts with no affinities.
Part 4: Memory-Aware Routing
Each server now tracks two memory tiers:
- VRAM: chat sessions loaded in GPU memory.
- RAM: chat sessions loaded in regular memory.
Replace affinity tracking from Part 3 with explicit memory state. When routing a chat_id, apply the following priority logic:
- VRAM hit on primary — If the primary ring server has
chat_idin VRAM, route there. - RAM hit on primary — Else if the primary ring server has
chat_idin RAM, route there. - Scan fallback — Else scan up to
kdistinct physical servers clockwise (including the primary).- If any have
chat_idin VRAM, route to the first one in clockwise order. - Else if any have
chat_idin RAM, route to the first one in clockwise order.
- If any have
- Default — If no match is found, route to the primary ring server.
load_to_vram(server_id, chat_id)
- Marks
chat_idas loaded in VRAM onserver_id. - If
chat_idwas in RAM on the same server, it is removed from RAM.
load_to_ram(server_id, chat_id)
- Marks
chat_idas loaded in RAM onserver_id. - If
chat_idwas in VRAM on the same server, it is removed from VRAM.
A chat may exist in VRAM or RAM on a given server, but not both.
get_server_v4(chat_id, k) -> str
- Returns the best server for
chat_idusing the priority logic above, scanning up tokdistinct physical servers clockwise.
Memory state, like affinity, is tied to a server's identity. If a server is removed, its VRAM and RAM contents are discarded; re-adding the same server_id later starts with empty memory.
Notes
All
server_idandchat_idvalues consist of lowercase letters, digits, and underscores.The ring is guaranteed to be non-empty at the time of any
GET,GET3, orGET4operation.record_affinity,load_to_vram, andload_to_ramare only called withserver_idvalues that currently exist on the ring.You do not need to declare or define a hash function of your own. It is defined in the judge.
Input format: The first line contains
N, the number of operations. Each of the nextNlines is one operation. The operation name maps to methods as follows:ADD server_id→add_server(server_id)ADD server_id v→add_server(server_id, v)REMOVE server_id→remove_server(server_id)GET chat_id→get_server(chat_id)AFFINITY server_id chat_id→record_affinity(server_id, chat_id)GET3 chat_id k→get_server_v3(chat_id, k)VRAM server_id chat_id→load_to_vram(server_id, chat_id)RAM server_id chat_id→load_to_ram(server_id, chat_id)GET4 chat_id k→get_server_v4(chat_id, k)
Output format: For each
GET,GET3, orGET4, print the resolvedserver_idon its own line. All other operations produce no output.
Examples
8
ADD alpha
ADD beta
ADD gamma
GET chat_1
GET chat_2
GET chat_3
GET chat_4
GET chat_5gamma
gamma
gamma
gamma
gamma4
ADD s1
GET chat_a
GET chat_b
GET chat_cs1
s1
s1