-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
7cd0e44
commit 5588181
Showing
4 changed files
with
241 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 /> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |