This repository contains the solved project for the second Distributed Computing assignment at the University of Aveiro. The goal is to implement a small distributed hash table using the Chord protocol.
The implementation creates a ring of UDP nodes that store key-value pairs. Each node is responsible for a range of hashed keys, forwards requests for keys owned by other nodes, and keeps a finger table so lookups can skip around the ring instead of moving only to the immediate successor.
By completing this assignment, you should learn how to:
- organize nodes in a consistent-hashing ring;
- decide whether a key belongs to a node's responsibility interval;
- exchange protocol messages over UDP sockets;
- encode application messages with Pickle;
- insert and retrieve key-value pairs from a DHT;
- keep Chord successor and predecessor pointers stable; and
- use finger tables to improve remote lookup routing.
The final solution supports:
- creation of a Chord ring with one or more nodes;
- joining new nodes through an existing DHT node;
- consistent hashing for node addresses and object keys;
- storage of key-value pairs on the responsible node;
- retrieval of values from any node in the ring;
- forwarding of
PUTandGETrequests when another node is responsible; - periodic stabilization using predecessor and successor information;
- notification of successors so predecessor pointers stay current;
- finger table construction and refresh; and
- finger-table based forwarding for remote
PUT,GET, and successor lookup.
All DHT messages run over UDP. Messages are Python dictionaries serialized with Pickle before being sent on the socket.
Nodes and keys are mapped to integer identifiers with dht_hash. The ring uses
a 10-bit identifier space by default, so identifiers range from 0 to 1023.
A node owns the interval after its predecessor up to and including its own identifier:
(predecessor_id, node_id]
Because the identifier space is circular, interval checks must also support the
wrap-around case where the predecessor has a larger identifier than the current
node. The contains(begin, end, node) helper implements this ring interval
logic.
The DHT protocol uses these messages:
| Operation | Destination | Message |
|---|---|---|
| Insert key-value pair | Any node | {"method": "PUT", "args": {"key": key, "value": value}} |
| Forward insert | Responsible node or next hop | {"method": "PUT", "args": {"key": key, "value": value, "from": client_addr}} |
| Insert response | Client | {"method": "ACK"} |
| Retrieve value | Any node | {"method": "GET", "args": {"key": key}} |
| Forward retrieve | Responsible node or next hop | {"method": "GET", "args": {"key": key, "from": client_addr}} |
| Retrieve response | Client | {"method": "ACK", "args": value} |
| Join ring | Any DHT node | {"method": "JOIN_REQ", "args": {"addr": addr, "id": id}} |
| Join response | New node | {"method": "JOIN_REP", "args": {"successor_id": succ_id, "successor_addr": succ_addr}} |
| Notify successor | Successor | {"method": "NOTIFY", "args": {"predecessor_id": id, "predecessor_addr": addr}} |
| Ask for predecessor | Successor | {"method": "PREDECESSOR"} |
| Stabilize response | Requesting node | {"method": "STABILIZE", "args": pred_id} |
| Ask for successor of id | Closest known node or successor | {"method": "SUCCESSOR", "args": {"id": id, "from": addr}} |
| Successor response | Requesting node | {"method": "SUCCESSOR_REP", "args": {"req_id": req_id, "successor_id": succ_id, "successor_addr": succ_addr}} |
sequenceDiagram
participant C as Client
participant A as Node A
participant B as Node B
participant S as Successor
participant N as New Node
N->>A: JOIN_REQ with address and id
A-->>N: JOIN_REP with successor
A->>S: PREDECESSOR
S-->>A: STABILIZE with predecessor id
A->>S: NOTIFY with A as predecessor
A->>B: SUCCESSOR request for finger table id
B-->>A: SUCCESSOR_REP with closest successor
C->>A: PUT key and value
alt A owns the hashed key
A-->>C: ACK
else Another node owns the hashed key
A->>B: Forward PUT with client address
B-->>C: ACK
end
C->>A: GET key
alt A owns the hashed key
A-->>C: ACK with value
else Another node owns the hashed key
A->>B: Forward GET with client address
B-->>C: ACK with value
end
The repository is organized around a small set of modules:
.
├── DHT.py # Starts a local Chord ring
├── DHTClient.py # Client helper for PUT and GET operations
├── DHTNode.py # Node, stabilization, routing, and finger tables
├── utils.py # Hashing and ring interval helpers
├── requirements.txt # Test dependencies
└── tests/ # Automated tests
The main implementation lives in DHTNode.py. It includes:
FingerTable, which stores successor shortcuts for each finger entry;DHTNode.node_join, which routes join requests to the correct predecessor;DHTNode.stabilize, which refreshes successor and finger table state;DHTNode.put, which stores or forwards key-value pairs; andDHTNode.get, which retrieves or forwards lookups.
DHTClient.py provides the client-facing put(key, value) and get(key)
methods. Clients can contact any node in the ring; the DHT routes the request to
the responsible node.
Create a virtual environment and install the test dependencies:
python3 -m venv .venv
source .venv/bin/activate
python -m pip install -r requirements.txtRun the tests from the repository root:
python -m pytest -vSome tests start real UDP nodes on localhost and wait for the ring to stabilize. If a test run fails because a port is still in use, stop the previous process and run the suite again.
Start the DHT in one terminal:
python3 DHT.pyBy default this starts five nodes beginning at localhost:5000. You can change
the number of nodes and stabilization timeout:
python3 DHT.py --nodes 8 --timeout 2In another terminal, use DHTClient to store and retrieve values:
python3 - <<'PY'
from DHTClient import DHTClient
client = DHTClient(("localhost", 5000))
client.put("A", [0, 1, 2])
print(client.get("A"))
PY- Ring interval checks handle both normal and wrap-around ranges.
PUTstores values only on the responsible node.GETretrieves values from the responsible node.- Forwarded requests preserve the original client address for the response.
- New nodes receive the correct successor during join.
- Stabilization updates successor and predecessor pointers.
- Finger tables refresh periodically.
- Remote lookups use finger table routing.
- All protocol messages are Pickle-encoded dictionaries over UDP.
- The automated test suite passes before submission.