Skip to content

Instantly share code, notes, and snippets.

@khalidx
Last active August 3, 2022 00:14
Show Gist options
  • Save khalidx/33fda24994a4f4e5fda41c0da2a9236d to your computer and use it in GitHub Desktop.
Save khalidx/33fda24994a4f4e5fda41c0da2a9236d to your computer and use it in GitHub Desktop.
An implementation of a "hash ring" for partitioning with consistent hashes.

Usage

const hashRing = createHashRing({
  clients: [
    { address: 'localhost:5001' },
    { address: 'localhost:5002' },
    { address: 'localhost:5003' }
  ]
})

const client = hashRing.getClientForKey('some-key')
// returns => { address: 'localhost:5001' }

Implementation

import { createHash } from 'crypto'

export type Client = {
  address: string
}

/**
 * A HashRing for ConsistentHashingPartitioning.
 * 
 * Inspired by:
 * 
 * - https://blog.hltbra.net/2019/02/18/consistent-hashing.html
 * - https://github.com/redis-essentials/book/blob/master/chapter%208/consistenthashing.js
 * - https://michaelnielsen.org/blog/consistent-hashing/
 */
export function createHashRing (params: { clients: Client[], vnodes?: number }) {
  const vnodes = params.vnodes || 256
  const ring: { [key: string]: Client } = {}
  const ringHashes: string[] = []
  const md5 = (str: string) => {
    return createHash('md5').update(str).digest('hex')
  }
  const addClient = (client: Client) => {
    for (let i = 0; i < vnodes; i++) {
      const hash = md5(client.address + ':' + i)
      ring[hash] = client
    }
    ringHashes.splice(0, ringHashes.length, ...Object.keys(ring).sort())
  }
  const removeClient = (client: Client) => {
    for (let i = 0; i < vnodes; i++) {
      const hash = md5(client.address + ':' + i)
      delete ring[hash]
    }
    ringHashes.splice(0, ringHashes.length, ...Object.keys(ring).sort())
  }
  const getClientForKey = (key: string) => {
    const keyHash = md5(key)
    for (const ringHash of ringHashes) {
      if (ringHash >= keyHash) return ring[ringHash]
    }
    return ring[ringHashes[0]]
  }
  for (const client of params.clients) {
    addClient(client)
  }
  return {
    addClient,
    removeClient,
    getClientForKey
  }
}

Alternate implementation

This presents a simplified implementation of consistent hashing, but without the "hash ring".

export function consistentHash (key: string, buckets: number) {
  const hash = createHash('md5').update(key).digest('hex')
  return parseInt(hash, 16) % buckets
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment