{
  "id": "P165",
  "slug": "shard-planner",
  "key": "P165-shard-planner",
  "title": "Shard planner",
  "summary": "Order ids allocated 8 apart, placed on buckets and nodes: id % 16 uses 2 of 16 buckets, a ring moves only the keys a new node takes, and one customer's key makes one node serve more than five times the even share.",
  "entry": "main.eml",
  "ui": "terminal",
  "readme": "# P165 - Shard planner\n\n1,024 order ids placed on buckets and on nodes: what comes out even, and\nwhat does not.\n\n- `main.eml` - the menu, the keys and their traffic, buckets by id % n and\n  by a mixing hash, a 13th node, the load on each node, and a bucket count\n  of your own\n- `shards.eml` - the ids and their request rates, a multiplicative mixing\n  hash, a ring with 16 points a node, and finding a key's owner on it\n\nHow each part works:\n\n- The allocator hands out ids in blocks of 8: 1000, 1008, ... 9184. Every\n  key gets 20 requests a second except customer 5000's, which gets 9,200,\n  31.0% of all 29,660.\n- As in the corpus case `the-hash-was-uniform-and-the-buckets-were-not`,\n  id % n reaches only n / gcd(n, 8) buckets: 2 of 16, 4 of 32, 8 of 64,\n  and all 17 of 17. The ids are not skewed; they share a factor with a\n  power-of-two bucket count. The top bits of a mixing hash use all 16, 32\n  and 64.\n- As in `the-hash-was-stable-and-the-ring-was-not`, adding a 13th node moves\n  946 of the 1,024 keys when they are placed by id % nodes, and 59 on the\n  ring, every one of them to the new node. For the 120 seconds both node\n  lists are in use, the half of the clients still on the old one send the\n  moved keys' 1,180 requests a second to the old owners: 70,800 requests\n  to the wrong node, from the same stable hash.\n- As in `the-shard-key-was-uniform-and-the-traffic-was-not`, keys per node\n  run from 58 to 133, while the node holding customer 5000 serves 11,800\n  requests a second, 5.2 times the even share: no placement of keys can\n  split one key.\n\nWhat is checked: menu choices 1 to 6; a bucket count from 2 to 1024. An\nempty answer cancels.\n\nSessions: `sessions/basic.in` gives:\n- the keys, the buckets, the 13th node and the load per node;\n- bucket counts of 24, 128 and 17.\n\n`sessions/bad-input.in` gives:\n- menu choices 0 and x;\n- bucket counts: a cancel, 1, 1025 and x;\n- bucket counts of 2 and 1024.\n\nBuilt on the verified corpus cases `the-hash-was-uniform-and-the-buckets-were-not`\n(ids allocated 8 apart in a power-of-two table),\n`the-hash-was-stable-and-the-ring-was-not` (a node added to a consistent\nhashing ring while clients hold different member lists) and\n`the-shard-key-was-uniform-and-the-traffic-was-not` (keys even across 64\nshards, and one key with 31 percent of the requests).\n",
  "modules": [
    {
      "name": "main.eml",
      "eml": "# P165 shard planner: 1,024 order ids on buckets and nodes - the keys and\n# their traffic, buckets by id % n and by a mixing hash, a 13th node added,\n# the load on each node, and a bucket count of your own.\nimport shards\n\ndef plain(s):\n    0 => i\n    len(s) => j\n    while i < j and s[i] == \" \":\n        i + 1 => i\n    while j > i and s[j - 1] == \" \":\n        j - 1 => j\n    return s[i:j]\n\ndef pad(s, n):\n    s => out\n    while len(out) < n:\n        out + \" \" => out\n    return out\n\ndef lpad(s, n):\n    s => out\n    while len(out) < n:\n        \" \" + out => out\n    return out\n\ndef digits(t):\n    for c in t:\n        if c < \"0\" or c > \"9\":\n            return False\n    return len(t) > 0\n\ndef commas(n):\n    str(n) => s\n    \"\" => out\n    0 => k\n    len(s) => i\n    while i > 0:\n        if k == 3:\n            \",\" + out => out\n            0 => k\n        s[i - 1] + out => out\n        k + 1 => k\n        i - 1 => i\n    return out\n\ndef tenths(num, den):\n    # num / den as a percent to one place, rounded half-up\n    num * 2000 + den => y\n    int((y - y % (2 * den)) / (2 * den)) => t\n    return str(int((t - t % 10) / 10)) + \".\" + str(t % 10) + \"%\"\n\ndef total_rate():\n    0 => t\n    for x in shards.ids():\n        t + shards.rate(x) => t\n    return t\n\ndef spread(counts):\n    # [buckets used, the busiest]\n    0 => used\n    0 => most\n    for c in counts:\n        if c > 0:\n            used + 1 => used\n        if c > most:\n            c => most\n    return [used, most]\n\ndef by_mod(n):\n    [0] * n => c\n    for x in shards.ids():\n        c[x % n] + 1 => c[x % n]\n    return spread(c)\n\ndef by_top(b, n):\n    [0] * n => c\n    for x in shards.ids():\n        shards.top(x, b) => k\n        c[k] + 1 => c[k]\n    return spread(c)\n\ndef log2(n):\n    # b with 2^b == n, or -1 if n is not a power of two\n    0 => b\n    1 => p\n    while p < n:\n        p * 2 => p\n        b + 1 => b\n    if p == n:\n        return b\n    return 0 - 1\n\ndef show_keys():\n    \"1,024 order ids from the allocator, which hands them out in blocks of 8: 1000, 1008, 1016, ... 9184.\" ^0\n    (\"Every key gets 20 requests a second, except customer \" + str(shards.hot_id()) + \"'s, which gets \" + commas(shards.rate(shards.hot_id())) + \": \" + tenths(shards.rate(shards.hot_id()), total_rate()) + \" of all \" + commas(total_rate()) + \".\") ^0\n\ndef show_buckets():\n    \"Where id % n puts the 1,024 ids, and where the top bits of a mixing hash put them:\" ^0\n    (\"  \" + pad(\"buckets\", 9) + lpad(\"id % n: used\", 13) + lpad(\"busiest\", 9) + lpad(\"mixed: used\", 14) + lpad(\"busiest\", 9)) ^0\n    for n in [16, 17, 32, 64]:\n        by_mod(n) => a\n        \"  \" + pad(str(n), 9) + lpad(str(a[0]), 13) + lpad(str(a[1]), 9) => line\n        if log2(n) > 0:\n            by_top(log2(n), n) => m\n            line + lpad(str(m[0]), 14) + lpad(str(m[1]), 9) => line\n        else:\n            line + lpad(\"-\", 14) + lpad(\"-\", 9) => line\n        line ^0\n    \"The ids are 8 apart, so id % n reaches only n / gcd(n, 8) of the buckets: a power of two shares the factor 8, and 17 shares none. The ids are not skewed; the bucket count and the allocator share a divisor.\" ^0\n\ndef show_join():\n    shards.ring(12) => r12\n    shards.ring(13) => r13\n    0 => by_mod\n    0 => by_ring\n    0 => moved_rate\n    for x in shards.ids():\n        if x % 12 != x % 13:\n            by_mod + 1 => by_mod\n        if shards.owner(r12, x) != shards.owner(r13, x):\n            by_ring + 1 => by_ring\n            moved_rate + shards.rate(x) => moved_rate\n    \"12 nodes, then a 13th:\" ^0\n    (\"  placed by id % nodes: \" + str(by_mod) + \" of the 1,024 keys change node.\") ^0\n    (\"  placed on a ring, 16 points a node: \" + str(by_ring) + \" keys change node, every one of them to the new node.\") ^0\n    \"Clients read the node list from a registry every 60 seconds, with jitter: for 120 seconds both rings are in use, half the clients on each on average.\" ^0\n    (\"The \" + str(by_ring) + \" moved keys get \" + commas(moved_rate) + \" requests a second, and the half still on the old ring send theirs to the old owners: \" + commas(moved_rate * 60) + \" requests to the wrong node, from the same stable hash.\") ^0\n\ndef show_load():\n    shards.ring(13) => r\n    [0] * 13 => keys\n    [0] * 13 => reqs\n    for x in shards.ids():\n        shards.owner(r, x) => n\n        keys[n] + 1 => keys[n]\n        reqs[n] + shards.rate(x) => reqs[n]\n    \"Keys and requests on each node of the 13-node ring:\" ^0\n    (\"  \" + pad(\"node\", 6) + lpad(\"keys\", 5) + lpad(\"requests a second\", 19)) ^0\n    0 => k\n    while k < 13:\n        (\"  \" + pad(\"n\" + str(k), 6) + lpad(str(keys[k]), 5) + lpad(commas(reqs[k]), 19)) ^0\n        k + 1 => k\n    spread(keys) => s\n    1024 => least\n    0 => hot\n    0 => k\n    while k < 13:\n        if keys[k] < least:\n            keys[k] => least\n        if reqs[k] > reqs[hot]:\n            k => hot\n        k + 1 => k\n    total_rate() * 2 + 13 => y\n    int((y - y % 26) / 26) => even\n    reqs[hot] * 130 * 2 + total_rate() => z\n    int((z - z % (2 * total_rate())) / (2 * total_rate())) => times\n    (\"Keys per node: \" + str(least) + \" to \" + str(s[1]) + \". Requests: n\" + str(hot) + \", which holds customer \" + str(shards.hot_id()) + \", serves \" + commas(reqs[hot]) + \" a second, \" + str(int((times - times % 10) / 10)) + \".\" + str(times % 10) + \" times the even share of \" + commas(even) + \".\") ^0\n    (\"Spreading keys does not spread requests: one key is \" + tenths(shards.rate(shards.hot_id()), total_rate()) + \" of the traffic, and no placement of keys can split it.\") ^0\n\ndef own():\n    plain(input(\"buckets (2 to 1024)> \")) => t\n    if t == \"\":\n        \"Cancelled.\" ^0\n        return\n    if len(t) > 4 or not digits(t) or int(t) < 2 or int(t) > 1024:\n        \"Type a whole number from 2 to 1024.\" ^0\n        return\n    int(t) => n\n    by_mod(n) => a\n    (\"id % \" + str(n) + \": \" + str(a[0]) + \" of \" + str(n) + \" buckets used, the busiest with \" + str(a[1]) + \" ids; gcd(\" + str(n) + \", 8) is \" + str(shards.gcd(n, 8)) + \", and \" + str(n) + \" / \" + str(shards.gcd(n, 8)) + \" is \" + str(int(n / shards.gcd(n, 8))) + \".\") ^0\n    if log2(n) > 0:\n        by_top(log2(n), n) => m\n        \"The top \" + str(log2(n)) + \" bits\" => unit\n        if log2(n) == 1:\n            \"The top bit\" => unit\n        (unit + \" of the mixed hash: \" + str(m[0]) + \" of \" + str(n) + \" used, the busiest with \" + str(m[1]) + \".\") ^0\n\n\"== Shard planner ==\" ^0\n\"1,024 order ids placed on buckets and on nodes: what is even, and what is not.\" ^0\nTrue => running\nwhile running:\n    \"\" ^0\n    \"1) the keys  2) buckets  3) a 13th node  4) load per node  5) a bucket count of your own  6) quit\" ^0\n    plain(input(\"choice> \")) => choice\n    if choice == \"1\":\n        show_keys()\n    elif choice == \"2\":\n        show_buckets()\n    elif choice == \"3\":\n        show_join()\n    elif choice == \"4\":\n        show_load()\n    elif choice == \"5\":\n        own()\n    elif choice == \"6\":\n        False => running\n    else:\n        \"Pick a number from 1 to 6.\" ^0\n\"Bye.\" ^0\n",
      "python": "import shards\n\ndef plain(s):\n    i = 0\n    j = len(s)\n    while i < j and s[i] == \" \":\n        i = i + 1\n    while j > i and s[j - 1] == \" \":\n        j = j - 1\n    return s[i:j]\n\ndef pad(s, n):\n    out = s\n    while len(out) < n:\n        out = out + \" \"\n    return out\n\ndef lpad(s, n):\n    out = s\n    while len(out) < n:\n        out = \" \" + out\n    return out\n\ndef digits(t):\n    for c in t:\n        if c < \"0\" or c > \"9\":\n            return False\n    return len(t) > 0\n\ndef commas(n):\n    s = str(n)\n    out = \"\"\n    k = 0\n    i = len(s)\n    while i > 0:\n        if k == 3:\n            out = \",\" + out\n            k = 0\n        out = s[i - 1] + out\n        k = k + 1\n        i = i - 1\n    return out\n\ndef tenths(num, den):\n    y = num * 2000 + den\n    t = int((y - y % (2 * den)) / (2 * den))\n    return str(int((t - t % 10) / 10)) + \".\" + str(t % 10) + \"%\"\n\ndef total_rate():\n    t = 0\n    for x in shards.ids():\n        t = t + shards.rate(x)\n    return t\n\ndef spread(counts):\n    used = 0\n    most = 0\n    for c in counts:\n        if c > 0:\n            used = used + 1\n        if c > most:\n            most = c\n    return [used, most]\n\ndef by_mod(n):\n    c = [0] * n\n    for x in shards.ids():\n        c[x % n] = c[x % n] + 1\n    return spread(c)\n\ndef by_top(b, n):\n    c = [0] * n\n    for x in shards.ids():\n        k = shards.top(x, b)\n        c[k] = c[k] + 1\n    return spread(c)\n\ndef log2(n):\n    b = 0\n    p = 1\n    while p < n:\n        p = p * 2\n        b = b + 1\n    if p == n:\n        return b\n    return 0 - 1\n\ndef show_keys():\n    print(\"1,024 order ids from the allocator, which hands them out in blocks of 8: 1000, 1008, 1016, ... 9184.\")\n    print(\"Every key gets 20 requests a second, except customer \" + str(shards.hot_id()) + \"'s, which gets \" + commas(shards.rate(shards.hot_id())) + \": \" + tenths(shards.rate(shards.hot_id()), total_rate()) + \" of all \" + commas(total_rate()) + \".\")\n\ndef show_buckets():\n    print(\"Where id % n puts the 1,024 ids, and where the top bits of a mixing hash put them:\")\n    print(\"  \" + pad(\"buckets\", 9) + lpad(\"id % n: used\", 13) + lpad(\"busiest\", 9) + lpad(\"mixed: used\", 14) + lpad(\"busiest\", 9))\n    for n in [16, 17, 32, 64]:\n        a = by_mod(n)\n        line = \"  \" + pad(str(n), 9) + lpad(str(a[0]), 13) + lpad(str(a[1]), 9)\n        if log2(n) > 0:\n            m = by_top(log2(n), n)\n            line = line + lpad(str(m[0]), 14) + lpad(str(m[1]), 9)\n        else:\n            line = line + lpad(\"-\", 14) + lpad(\"-\", 9)\n        print(line)\n    print(\"The ids are 8 apart, so id % n reaches only n / gcd(n, 8) of the buckets: a power of two shares the factor 8, and 17 shares none. The ids are not skewed; the bucket count and the allocator share a divisor.\")\n\ndef show_join():\n    r12 = shards.ring(12)\n    r13 = shards.ring(13)\n    by_mod = 0\n    by_ring = 0\n    moved_rate = 0\n    for x in shards.ids():\n        if x % 12 != x % 13:\n            by_mod = by_mod + 1\n        if shards.owner(r12, x) != shards.owner(r13, x):\n            by_ring = by_ring + 1\n            moved_rate = moved_rate + shards.rate(x)\n    print(\"12 nodes, then a 13th:\")\n    print(\"  placed by id % nodes: \" + str(by_mod) + \" of the 1,024 keys change node.\")\n    print(\"  placed on a ring, 16 points a node: \" + str(by_ring) + \" keys change node, every one of them to the new node.\")\n    print(\"Clients read the node list from a registry every 60 seconds, with jitter: for 120 seconds both rings are in use, half the clients on each on average.\")\n    print(\"The \" + str(by_ring) + \" moved keys get \" + commas(moved_rate) + \" requests a second, and the half still on the old ring send theirs to the old owners: \" + commas(moved_rate * 60) + \" requests to the wrong node, from the same stable hash.\")\n\ndef show_load():\n    r = shards.ring(13)\n    keys = [0] * 13\n    reqs = [0] * 13\n    for x in shards.ids():\n        n = shards.owner(r, x)\n        keys[n] = keys[n] + 1\n        reqs[n] = reqs[n] + shards.rate(x)\n    print(\"Keys and requests on each node of the 13-node ring:\")\n    print(\"  \" + pad(\"node\", 6) + lpad(\"keys\", 5) + lpad(\"requests a second\", 19))\n    k = 0\n    while k < 13:\n        print(\"  \" + pad(\"n\" + str(k), 6) + lpad(str(keys[k]), 5) + lpad(commas(reqs[k]), 19))\n        k = k + 1\n    s = spread(keys)\n    least = 1024\n    hot = 0\n    k = 0\n    while k < 13:\n        if keys[k] < least:\n            least = keys[k]\n        if reqs[k] > reqs[hot]:\n            hot = k\n        k = k + 1\n    y = total_rate() * 2 + 13\n    even = int((y - y % 26) / 26)\n    z = reqs[hot] * 130 * 2 + total_rate()\n    times = int((z - z % (2 * total_rate())) / (2 * total_rate()))\n    print(\"Keys per node: \" + str(least) + \" to \" + str(s[1]) + \". Requests: n\" + str(hot) + \", which holds customer \" + str(shards.hot_id()) + \", serves \" + commas(reqs[hot]) + \" a second, \" + str(int((times - times % 10) / 10)) + \".\" + str(times % 10) + \" times the even share of \" + commas(even) + \".\")\n    print(\"Spreading keys does not spread requests: one key is \" + tenths(shards.rate(shards.hot_id()), total_rate()) + \" of the traffic, and no placement of keys can split it.\")\n\ndef own():\n    t = plain(input(\"buckets (2 to 1024)> \"))\n    if t == \"\":\n        print(\"Cancelled.\")\n        return\n    if len(t) > 4 or not digits(t) or int(t) < 2 or int(t) > 1024:\n        print(\"Type a whole number from 2 to 1024.\")\n        return\n    n = int(t)\n    a = by_mod(n)\n    print(\"id % \" + str(n) + \": \" + str(a[0]) + \" of \" + str(n) + \" buckets used, the busiest with \" + str(a[1]) + \" ids; gcd(\" + str(n) + \", 8) is \" + str(shards.gcd(n, 8)) + \", and \" + str(n) + \" / \" + str(shards.gcd(n, 8)) + \" is \" + str(int(n / shards.gcd(n, 8))) + \".\")\n    if log2(n) > 0:\n        m = by_top(log2(n), n)\n        unit = \"The top \" + str(log2(n)) + \" bits\"\n        if log2(n) == 1:\n            unit = \"The top bit\"\n        print(unit + \" of the mixed hash: \" + str(m[0]) + \" of \" + str(n) + \" used, the busiest with \" + str(m[1]) + \".\")\n\nprint(\"== Shard planner ==\")\nprint(\"1,024 order ids placed on buckets and on nodes: what is even, and what is not.\")\nrunning = True\nwhile running:\n    print(\"\")\n    print(\"1) the keys  2) buckets  3) a 13th node  4) load per node  5) a bucket count of your own  6) quit\")\n    choice = plain(input(\"choice> \"))\n    if choice == \"1\":\n        show_keys()\n    elif choice == \"2\":\n        show_buckets()\n    elif choice == \"3\":\n        show_join()\n    elif choice == \"4\":\n        show_load()\n    elif choice == \"5\":\n        own()\n    elif choice == \"6\":\n        running = False\n    else:\n        print(\"Pick a number from 1 to 6.\")\nprint(\"Bye.\")\n"
    },
    {
      "name": "shards.eml",
      "eml": "# P165 shard planner - 1,024 order ids placed on buckets and nodes. As in\n# the corpus case the-hash-was-uniform-and-the-buckets-were-not, the ids\n# come from an allocator that hands them out 8 apart, and id % n reaches\n# only the buckets that 8 can: a power of two shares its factor. As in\n# the-hash-was-stable-and-the-ring-was-not, a ring moves only the keys the\n# new node takes, and while clients hold two node lists, the same stable\n# hash names two owners. And as in\n# the-shard-key-was-uniform-and-the-traffic-was-not, keys spread evenly and\n# requests do not.\n\ndef ids():\n    # 1000, 1008, 1016, ... - 1,024 of them\n    [0] * 1024 => out\n    0 => k\n    while k < 1024:\n        1000 + 8 * k => out[k]\n        k + 1 => k\n    return out\n\ndef hot_id():\n    return 5000\n\ndef rate(x):\n    # requests a second for key x\n    if x == hot_id():\n        return 9200\n    return 20\n\ndef mix(x):\n    # a multiplicative hash onto 0 .. 2^32 - 1 (x below 3,000,000)\n    return (x * 2654435761) % 4294967296\n\ndef top(x, b):\n    # bucket of x among 2^b: the top b bits of its mixed hash\n    1 => d\n    0 => k\n    while k < 32 - b:\n        d * 2 => d\n        k + 1 => k\n    mix(x) => h\n    return int((h - h % d) / d)\n\ndef ring(nodes):\n    # [position, node] for 16 points a node, sorted by position\n    [] => pts\n    0 => n\n    while n < nodes:\n        0 => v\n        while v < 16:\n            pts + [[mix(100000 + n * 100 + v), n]] => pts\n            v + 1 => v\n        n + 1 => n\n    1 => i\n    while i < len(pts):\n        pts[i] => p\n        i - 1 => j\n        while j >= 0 and pts[j][0] > p[0]:\n            pts[j] => pts[j + 1]\n            j - 1 => j\n        p => pts[j + 1]\n        i + 1 => i\n    return pts\n\ndef owner(pts, x):\n    # the node of the first point at or after x's position, round the ring\n    mix(x) => h\n    0 => lo\n    len(pts) => hi\n    while lo < hi:\n        int((lo + hi - (lo + hi) % 2) / 2) => mid\n        if pts[mid][0] < h:\n            mid + 1 => lo\n        else:\n            mid => hi\n    if lo == len(pts):\n        return pts[0][1]\n    return pts[lo][1]\n\ndef gcd(a, b):\n    while b > 0:\n        a % b => r\n        b => a\n        r => b\n    return a\n",
      "python": "def ids():\n    out = [0] * 1024\n    k = 0\n    while k < 1024:\n        out[k] = 1000 + 8 * k\n        k = k + 1\n    return out\n\ndef hot_id():\n    return 5000\n\ndef rate(x):\n    if x == hot_id():\n        return 9200\n    return 20\n\ndef mix(x):\n    return x * 2654435761 % 4294967296\n\ndef top(x, b):\n    d = 1\n    k = 0\n    while k < 32 - b:\n        d = d * 2\n        k = k + 1\n    h = mix(x)\n    return int((h - h % d) / d)\n\ndef ring(nodes):\n    pts = []\n    n = 0\n    while n < nodes:\n        v = 0\n        while v < 16:\n            pts = pts + [[mix(100000 + n * 100 + v), n]]\n            v = v + 1\n        n = n + 1\n    i = 1\n    while i < len(pts):\n        p = pts[i]\n        j = i - 1\n        while j >= 0 and pts[j][0] > p[0]:\n            pts[j + 1] = pts[j]\n            j = j - 1\n        pts[j + 1] = p\n        i = i + 1\n    return pts\n\ndef owner(pts, x):\n    h = mix(x)\n    lo = 0\n    hi = len(pts)\n    while lo < hi:\n        mid = int((lo + hi - (lo + hi) % 2) / 2)\n        if pts[mid][0] < h:\n            lo = mid + 1\n        else:\n            hi = mid\n    if lo == len(pts):\n        return pts[0][1]\n    return pts[lo][1]\n\ndef gcd(a, b):\n    while b > 0:\n        r = a % b\n        a = b\n        b = r\n    return a\n"
    }
  ],
  "sessions": [
    {
      "name": "bad-input",
      "input": "0\nx\n5\n\n5\n1\n5\n1025\n5\nx\n5\n2\n5\n1024\n6\n",
      "screen": "== Shard planner ==\n1,024 order ids placed on buckets and on nodes: what is even, and what is not.\n\n1) the keys  2) buckets  3) a 13th node  4) load per node  5) a bucket count of your own  6) quit\nchoice> 0\nPick a number from 1 to 6.\n\n1) the keys  2) buckets  3) a 13th node  4) load per node  5) a bucket count of your own  6) quit\nchoice> x\nPick a number from 1 to 6.\n\n1) the keys  2) buckets  3) a 13th node  4) load per node  5) a bucket count of your own  6) quit\nchoice> 5\nbuckets (2 to 1024)> \nCancelled.\n\n1) the keys  2) buckets  3) a 13th node  4) load per node  5) a bucket count of your own  6) quit\nchoice> 5\nbuckets (2 to 1024)> 1\nType a whole number from 2 to 1024.\n\n1) the keys  2) buckets  3) a 13th node  4) load per node  5) a bucket count of your own  6) quit\nchoice> 5\nbuckets (2 to 1024)> 1025\nType a whole number from 2 to 1024.\n\n1) the keys  2) buckets  3) a 13th node  4) load per node  5) a bucket count of your own  6) quit\nchoice> 5\nbuckets (2 to 1024)> x\nType a whole number from 2 to 1024.\n\n1) the keys  2) buckets  3) a 13th node  4) load per node  5) a bucket count of your own  6) quit\nchoice> 5\nbuckets (2 to 1024)> 2\nid % 2: 1 of 2 buckets used, the busiest with 1024 ids; gcd(2, 8) is 2, and 2 / 2 is 1.\nThe top bit of the mixed hash: 2 of 2 used, the busiest with 513.\n\n1) the keys  2) buckets  3) a 13th node  4) load per node  5) a bucket count of your own  6) quit\nchoice> 5\nbuckets (2 to 1024)> 1024\nid % 1024: 128 of 1024 buckets used, the busiest with 8 ids; gcd(1024, 8) is 8, and 1024 / 8 is 128.\nThe top 10 bits of the mixed hash: 450 of 1024 used, the busiest with 4.\n\n1) the keys  2) buckets  3) a 13th node  4) load per node  5) a bucket count of your own  6) quit\nchoice> 6\nBye.\n",
      "interpreter": "equal"
    },
    {
      "name": "basic",
      "input": "1\n2\n3\n4\n5\n24\n5\n128\n5\n17\n6\n",
      "screen": "== Shard planner ==\n1,024 order ids placed on buckets and on nodes: what is even, and what is not.\n\n1) the keys  2) buckets  3) a 13th node  4) load per node  5) a bucket count of your own  6) quit\nchoice> 1\n1,024 order ids from the allocator, which hands them out in blocks of 8: 1000, 1008, 1016, ... 9184.\nEvery key gets 20 requests a second, except customer 5000's, which gets 9,200: 31.0% of all 29,660.\n\n1) the keys  2) buckets  3) a 13th node  4) load per node  5) a bucket count of your own  6) quit\nchoice> 2\nWhere id % n puts the 1,024 ids, and where the top bits of a mixing hash put them:\n  buckets   id % n: used  busiest   mixed: used  busiest\n  16                   2      512            16       68\n  17                  17       61             -        -\n  32                   4      256            32       35\n  64                   8      128            64       20\nThe ids are 8 apart, so id % n reaches only n / gcd(n, 8) of the buckets: a power of two shares the factor 8, and 17 shares none. The ids are not skewed; the bucket count and the allocator share a divisor.\n\n1) the keys  2) buckets  3) a 13th node  4) load per node  5) a bucket count of your own  6) quit\nchoice> 3\n12 nodes, then a 13th:\n  placed by id % nodes: 946 of the 1,024 keys change node.\n  placed on a ring, 16 points a node: 59 keys change node, every one of them to the new node.\nClients read the node list from a registry every 60 seconds, with jitter: for 120 seconds both rings are in use, half the clients on each on average.\nThe 59 moved keys get 1,180 requests a second, and the half still on the old ring send theirs to the old owners: 70,800 requests to the wrong node, from the same stable hash.\n\n1) the keys  2) buckets  3) a 13th node  4) load per node  5) a bucket count of your own  6) quit\nchoice> 4\nKeys and requests on each node of the 13-node ring:\n  node   keys  requests a second\n  n0       84              1,680\n  n1       72              1,440\n  n2       73              1,460\n  n3       88              1,760\n  n4      131             11,800\n  n5      133              2,660\n  n6       89              1,780\n  n7       60              1,200\n  n8       59              1,180\n  n9       59              1,180\n  n10      58              1,160\n  n11      59              1,180\n  n12      59              1,180\nKeys per node: 58 to 133. Requests: n4, which holds customer 5000, serves 11,800 a second, 5.2 times the even share of 2,282.\nSpreading keys does not spread requests: one key is 31.0% of the traffic, and no placement of keys can split it.\n\n1) the keys  2) buckets  3) a 13th node  4) load per node  5) a bucket count of your own  6) quit\nchoice> 5\nbuckets (2 to 1024)> 24\nid % 24: 3 of 24 buckets used, the busiest with 342 ids; gcd(24, 8) is 8, and 24 / 8 is 3.\n\n1) the keys  2) buckets  3) a 13th node  4) load per node  5) a bucket count of your own  6) quit\nchoice> 5\nbuckets (2 to 1024)> 128\nid % 128: 16 of 128 buckets used, the busiest with 64 ids; gcd(128, 8) is 8, and 128 / 8 is 16.\nThe top 7 bits of the mixed hash: 128 of 128 used, the busiest with 12.\n\n1) the keys  2) buckets  3) a 13th node  4) load per node  5) a bucket count of your own  6) quit\nchoice> 5\nbuckets (2 to 1024)> 17\nid % 17: 17 of 17 buckets used, the busiest with 61 ids; gcd(17, 8) is 1, and 17 / 1 is 17.\n\n1) the keys  2) buckets  3) a 13th node  4) load per node  5) a bucket count of your own  6) quit\nchoice> 6\nBye.\n",
      "interpreter": "equal"
    }
  ],
  "builtOn": [
    {
      "slug": "the-hash-was-uniform-and-the-buckets-were-not",
      "caseId": "545-the-hash-was-uniform-and-the-buckets-were-not",
      "title": "The hash was uniform and the buckets were not"
    },
    {
      "slug": "the-hash-was-stable-and-the-ring-was-not",
      "caseId": "635-the-hash-was-stable-and-the-ring-was-not",
      "title": "The hash was stable and the ring was not"
    },
    {
      "slug": "the-shard-key-was-uniform-and-the-traffic-was-not",
      "caseId": "669-the-shard-key-was-uniform-and-the-traffic-was-not",
      "title": "The shard key was uniform and the traffic was not"
    }
  ],
  "updated": "2026-10-19"
}
