Skip to content

Commit

Permalink
basic hash ring visualization
Browse files Browse the repository at this point in the history
  • Loading branch information
loganzartman committed Dec 3, 2023
1 parent 7cd0e44 commit 5588181
Show file tree
Hide file tree
Showing 4 changed files with 241 additions and 0 deletions.
13 changes: 13 additions & 0 deletions src/app/blog/posts/consistent-hashing.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
title: Consistent hashing
description: an interactive introduction to consistent hashing
date: 2023-12-02T12:00:00
---

import FlattenRing from './consistent-hashing/FlattenRing';

# Consistent hashing

Consistent hashing is a way to hash a key to one of `N` values, such that if we change `N`, most keys still map to the same value, and each `i in N` has roughly the same number of keys.

<FlattenRing />
61 changes: 61 additions & 0 deletions src/app/blog/posts/consistent-hashing/FlattenRing.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
'use client';

import {MouseEventHandler, useState} from 'react';

import Ring from '@/app/blog/posts/consistent-hashing/Ring';

function Button({
onClick,
children,
}: {
onClick?: MouseEventHandler<HTMLButtonElement>;
children?: React.ReactNode;
}) {
return (
<button
className="px-2 py-1 ring-1 ring-brand-200 rounded-full"
onClick={onClick}
>
{children}
</button>
);
}

export default function FlattenRing() {
const [seed, setSeed] = useState(42);
const [circle, setCircle] = useState(false);
const [serverCount, setServerCount] = useState(3);
const servers = Array.from({length: serverCount}).map((_, i) => `S${i}`);
return (
<div className="flex flex-col">
<Ring
servers={servers}
keys={['K1', 'K2', 'K3']}
circle={circle}
seed={seed}
/>
<label className="flex flex-row gap-2">
<input
type="checkbox"
checked={!circle}
onChange={(e) => setCircle(!e.target.checked)}
/>
Flatten
</label>
<label className="flex flex-row gap-2">
Servers
<input
type="range"
min="1"
max="6"
step="1"
value={serverCount}
onChange={(e) => setServerCount(e.target.valueAsNumber)}
/>
</label>
<Button onClick={() => setSeed(Math.random() * 1000)}>
Randomize seed
</Button>
</div>
);
}
98 changes: 98 additions & 0 deletions src/app/blog/posts/consistent-hashing/Ring.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
'use client';

import {AnimatePresence, motion} from 'framer-motion';

import {hash} from '@/app/blog/posts/consistent-hashing/hash-string';

type Props = {
servers: string[];
keys: string[];
circle: boolean;
seed: number;
};

function pointsToD(points: number[][]) {
console.log(points);
return points
.map(
(p, i) => `${i === 0 ? 'M' : 'L'}${p[0].toFixed(3)},${p[1].toFixed(3)}`,
)
.join(' ');
}

export default function Ring({servers, keys, circle, seed}: Props) {
const n = 64;
const mod = 2 ** 32 - 1;

const variants = {
line: {
d: pointsToD(Array.from({length: n}).map((_, i) => [(i / n) * 2 - 1, 0])),
},
circle: {
d: pointsToD(
Array.from({length: n}).map((_, i) => {
const f = (i / (n - 1) + 0.25) * 2 * Math.PI;
return [Math.cos(f), Math.sin(f)];
}),
),
},
};

const serverIcons = servers.map((server, i) => {
const h = hash(server, seed);
const pos = h / mod;
const circleProps = {
translateX: Math.cos((pos + 0.25) * Math.PI * 2),
translateY: Math.sin((pos + 0.25) * Math.PI * 2),
scale: 1,
fill: `hsl(${(i / servers.length) * 0.9 * 360}, 80%, 80%)`,
};
const lineProps = {
translateX: pos * 2 - 1,
translateY: 0,
scale: 1,
fill: `hsl(${(i / servers.length) * 0.9 * 360}, 80%, 80%)`,
};

return (
<motion.g
key={server}
custom={i}
initial={{scale: 0, translateX: 0, translateY: 0}}
exit={{scale: 0, translateX: 0, translateY: 0}}
animate={circle ? circleProps : lineProps}
transition={{type: 'spring', stiffness: 200, damping: 40}}
>
<circle cx={0} cy={0} r={0.05} />
<text
fill="black"
fontSize="0.06"
alignmentBaseline="middle"
textAnchor="middle"
fontWeight="bold"
x={0}
y={0}
>
{server}
</text>
</motion.g>
);
});

return (
<motion.svg
className="w-full"
viewBox="-1.2 -1.2 2.4 2.4"
animate={circle ? 'circle' : 'line'}
>
<motion.path
variants={variants}
className="stroke-brand-200"
strokeWidth="2"
fill="none"
transition={{type: 'spring', stiffness: 200, damping: 40}}
/>
<AnimatePresence>{serverIcons}</AnimatePresence>
</motion.svg>
);
}
69 changes: 69 additions & 0 deletions src/app/blog/posts/consistent-hashing/hash-string.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
export function hash(key: string, seed: number): number {
// murmurhash 3 32 gc
// written by chatgpt, works well enough for me
let remainder, bytes, h1, h1b, c1, c2, k1, i;

remainder = key.length & 3; // key.length % 4
bytes = key.length - remainder;
h1 = seed;
c1 = 0xcc9e2d51;
c2 = 0x1b873593;
i = 0;

while (i < bytes) {
k1 =
(key.charCodeAt(i) & 0xff) |
((key.charCodeAt(++i) & 0xff) << 8) |
((key.charCodeAt(++i) & 0xff) << 16) |
((key.charCodeAt(++i) & 0xff) << 24);
++i;

k1 =
((k1 & 0xffff) * c1 + ((((k1 >>> 16) * c1) & 0xffff) << 16)) & 0xffffffff;
k1 = (k1 << 15) | (k1 >>> 17);
k1 =
((k1 & 0xffff) * c2 + ((((k1 >>> 16) * c2) & 0xffff) << 16)) & 0xffffffff;

h1 ^= k1;
h1 = (h1 << 13) | (h1 >>> 19);
h1b =
((h1 & 0xffff) * 5 + ((((h1 >>> 16) * 5) & 0xffff) << 16)) & 0xffffffff;
h1 = (h1b & 0xffff) + 0x6b64 + ((((h1b >>> 16) + 0xe654) & 0xffff) << 16);
}

k1 = 0;

switch (remainder) {
case 3:
k1 ^= (key.charCodeAt(i + 2) & 0xff) << 16;
case 2:
k1 ^= (key.charCodeAt(i + 1) & 0xff) << 8;
case 1:
k1 ^= key.charCodeAt(i) & 0xff;

k1 =
((k1 & 0xffff) * c1 + ((((k1 >>> 16) * c1) & 0xffff) << 16)) &
0xffffffff;
k1 = (k1 << 15) | (k1 >>> 17);
k1 =
((k1 & 0xffff) * c2 + ((((k1 >>> 16) * c2) & 0xffff) << 16)) &
0xffffffff;
h1 ^= k1;
}

h1 ^= key.length;

h1 ^= h1 >>> 16;
h1 =
((h1 & 0xffff) * 0x85ebca6b +
((((h1 >>> 16) * 0x85ebca6b) & 0xffff) << 16)) &
0xffffffff;
h1 ^= h1 >>> 13;
h1 =
((h1 & 0xffff) * 0xc2b2ae35 +
((((h1 >>> 16) * 0xc2b2ae35) & 0xffff) << 16)) &
0xffffffff;
h1 ^= h1 >>> 16;

return h1 >>> 0;
}

0 comments on commit 5588181

Please sign in to comment.