Spin up 3 postgres instances with identical schema
5434, 5435, 5436 (host ports; must match the ports used in index.js)
Write
Read
1. Create init.sql
CREATE TABLE URL_TABLE( id serial NOT NULL PRIMARY KEY, URL text, URL_ID character(5) UNIQUE);
2. Dockerfile
FROM postgresCOPY init.sql /docker-entrypoint-initdb.d
3. Docker Image
docker build -t pgshard .
4. Docker containers
# Each container needs a unique name, a unique host port and a password# (the official postgres image won't start without POSTGRES_PASSWORD or an auth method)docker run --name pgshard1 -e POSTGRES_PASSWORD=postgres -p 5434:5432 -d pgsharddocker run --name pgshard2 -e POSTGRES_PASSWORD=postgres -p 5435:5432 -d pgsharddocker run --name pgshard3 -e POSTGRES_PASSWORD=postgres -p 5436:5432 -d pgshard
Node server
// index.jsconst express = require("express");const { Client } = require("pg");// const ConsistentHash = require("consistent-hash");const HashRing = require("hashring");const crypto = require("crypto");const app = express();app.use(express.json());// const hr = new ConsistentHash();const hr = new HashRing();hr.add("5436");hr.add("5435");hr.add("5434");const clients = { 5436: new Client({ host: "localhost", port: "5436", user: "postgres", password: "postgres", database: "postgres", }), 5435: new Client({ host: "localhost", port: "5435", user: "postgres", password: "postgres", database: "postgres", }), 5434: new Client({ host: "localhost", port: "5434", user: "postgres", password: "postgres", database: "postgres", }),};// Connect to all PostgreSQL instancesasync function connectAllClients() { for (const port in clients) { try { await clients[port].connect(); console.log(`Connected to Postgres on port ${port}`); } catch (err) { console.error(`Failed to connect to Postgres on port ${port}:`, err); } }}connectAllClients();app.get("/:urlId", async (req, res) => { const urlId = req.params.urlId; const server = hr.get(urlId); const results = await clients[server].query( "SELECT * FROM URL_TABLE WHERE URL_ID = $1", [urlId], ); if (results.rowCount > 0) { originalUrl = results.rows[0].url; res.send({ originalUrl: originalUrl, }); } else { res.sendStatus(404); }});app.post("/", async (req, res) => { const url = req.query.url; const hash = crypto.createHash("sha256").update(url).digest("base64url"); // base64url: no "/" or "+" in the URL path const urlId = hash.substr(0, 5); const server = hr.get(urlId); await clients[server].query( "INSERT INTO URL_TABLE (URL, URL_ID) VALUES ($1, $2)", [url, urlId], ); res.send({ hash: hash, urlId: urlId, server: server, });});app.listen(8081, () => console.log("Listening on port 8081"));
Collisions: 5 base64url chars = 64⁵ ≈ 1 billion ids, so different URLs can collide. The UNIQUE on URL_ID makes a collision fail loudly instead of silently storing two URLs under one id (each id always routes to the same shard, so a per-shard UNIQUE is enough). Re-posting the same URL also hits it; handle the error or look the id up first