An Algorithmic Lucidity

a blog

Consistent Hashing

Dear reader, suppose you're a distibuted data storage system. Your soul (although some pedants would insist on the word program) is dispersed across a cluster of several networked computers. From time to time, your human patrons give you files, and your job—more than that, the very purpose of your existence—is to store these files for safekeeping and later retrieval.

The humans who originally crafted your soul chose a simple algorithm as the means by which you decide which file goes on which of the many storage devices that live in the computers you inhabit: you find the MD5 hash of the filename, take its residue modulo n where n is the number of devices you have—let's call the result i—and you put the file on the (zero-indexed) ith device. So when you had sixteen devices and the humans wanted you to store twilight.pdf, you computed md5("twilight.pdf") = 429eb07bb8a3871c431fe03694105883, saw that the lowest nibble was 3, and put the file on your 3rd device (most humans would say the fourth device, counting from one).

It's not a bad system, you tell yourself (some sort of pride or loyalty preventing you from disparaging your creators' efforts, even to yourself). At least it keeps the data spread out evenly. (A shudder goes down your internal buses as you contemplate what disasters might have happened if your creators had been even more naive and, say, had you put files with names starting with A through D on the first device, &c. What would have happened that time when your patrons decided they wanted to store beat00001.mp3 through beat18691.mp3?)

But still, you've been having problems lately. As your patrons have come to rely on you more and more, they've added more machines bearing more devices to your cluster—and occasionally devices fail, too. And every time you gain or lose a device, your hashing scheme for assigning files to devices goes out of date: in the vast majority of cases, md5(filename) mod n changes when n does, requiring almost all of your files to be shuffled onto different devices, an expensive and unpleasant procedure.

One day, after a particularly painful rearrangement of files, you decide enough is enough. You're going to need a new way to assign files to devices, even if it means rewriting part of your own soul (and that's exactly what it will mean). You do some searching and read about an idea called consistent hashing that seems like a perfect solution to your problem. (In fact, your reading even mentions that your older cousin, OpenStack Swift, uses a modified version of consistent hashing for the same purpose. You've always resented Swift and aren't happy with the idea of copying one of her algorithms—but you tell yourself that that's not important.)

Anyway, consistent hashing. The essential nature of your problem is that you need a way to split the space of possible filenames into n partitions in a way that minimizes the number of files that have to change partitions when n changes. So imagine mapping the 128-bit output space of MD5 around a ring. Now pseudorandomly designate m spots on the ring (m being some prudently-chosen natural number) for each of your n storage devices: say, by hashing the device name suffixed with the number j for each j between 0 and m–1 inclusive. Internally, you might represent the ring as just an array of hash/device pairs. Like this (in Clojure; here the array and the pairs it contains are actually "vectors")—

(import 'java.security.MessageDigest)

(defn md5 [string]
  (let [digest-builder
        (java.security.MessageDigest/getInstance "MD5")]
    (.update digest-builder (.getBytes string))
    (clojure.string/join
     (map (fn [chr] (format "%x" chr))
          (.digest digest-builder)))))

(defn suffixed [name n]
  (map (fn [j] (str name j))
       (range n)))

(defn ring-kv-pairs [device spots]
  (map (fn [suffixed-name]
         (vector (md5 suffixed-name) device))
       (suffixed device spots)))

(defn make-ring [devices spots]
  (vec (sort
        (reduce concat
                (map (fn [device]
                       (ring-kv-pairs device spots))
                     devices)))))

Then when your patrons ask you to store a file, you're still going to hash its name, but instead of taking the residue mod n of the hash to decide where the file should live, you're going to locate the hash in your ring, and find the next designated spot on the ring associated with one of your storage devices, and the file will live on that device. In terms of your internal representation of the ring as an array of hash/device pairs, you'll find the first pair whose first item (the hash of the suffixed device name) is greater than the hash of the name of the file to be stored, and the second item of that pair will tell you which storage device to use. (And if the filename hash is greater than all those stored in the (first items of the pairs stored in the) array, then you just use the first pair in the array. It is supposed to represent a ring, after all.) Like this—

(defn ring-search [ring key]
  (or (some (fn [pair]
              (if (> (compare (first pair) key) 0)
                pair))
            ring)
      (first ring))) 

(defn ring-lookup [ring filename]
  (last (ring-search ring (md5 filename))))

And now—why, now when you gain (respectively lose) a storage device, you can decide the allocation question simply by adding (respectively removing) the appropriate designated spots to (respectively from) your ring—

(defn ring-add [ring device spots]
  (sort (vec (concat ring (ring-kv-pairs device spots)))))

(defn ring-remove [ring device]
  (filter (fn [spot] (not= (last spot) device))
          ring))

—and most files stay on the same device: the only ones that have to move are those with a new nextmost designated spot in the ring!

Your Consistent Hashing Ring

You decide to run a quick simulation to see if you've gotten this right before saving the edits to your soul. Suppose you had storage devices device0 through device5 and your patrons wanted you to store file0 through file5999; how would the files end up distributed across your devices? You choose a prudent-seeming integer for the number of designated spots per device—say, 450?—and write the simulation like this—

(defn filemap [ring files]
  (into {}
        (for [file files
              :let [device (ring-lookup ring file)]]
          [file device])))

(defn counter [devices filemap]
  (for [d devices]
    [d (count (filter (fn [entry]
                        (= (last entry) d)) filemap))]))

(def my-devices (suffixed "device" 6))
(def my-first-ring (make-ring my-devices 450))
(def my-files (suffixed "file" 6000))
(def my-filemap (filemap my-first-ring my-files))
(doseq [total (counter my-devices my-filemap)]
  (println total))

And the results seem reasonable enough:

$ clojure consistent_hashing.clj 
[device0 1020]
[device1 993]
[device2 1082]
[device3 955]
[device4 993]
[device5 957]

2013 Year in Reverse

Dear reader, as another year comes to a close, it is perhaps wise that we should take a few moments to reflect on what we learned here at An Algorithmic Lucidity in 2013—the year that was!

In the year 2013, this blog saw 103 posts and (at press time) 40 comments. Among these—

We heard a poem about why you should hire me. We surveyed a few numbers between 0 and 1. (Friend of the blog Grognor told me that the funniest part was the explicit disclaimer that the list was non-exhaustive. I replied that not everyone knows about the diagonalization argument—but considering this blog's, um, selective audience, maybe my readers do—all five of you!) We mentioned some hidden costs of talking about stuff. We noted that some words have substrings which are other words. I wrote a series about my experiences studying web development at App Academy (9 8 7 6 5 4 3 2 1). I pasted my first attempt at an OKCupid profile. We observed that I'm a moron. Books arrived in the post. We mused on the nature of personhood and implemented some classic algorithms: the Ford-Fulkerson technique somewhat clumsily in Ruby, quicksort in the form of a letter to a fictional horse, and Huffman coding in Python. We lamented the nature of existence, noticed the non-observance of an unusual tradition, and heard a poem about watching a motion picture by someone you met at university. Three problems with unsolicited advice were discussed, as was the role of bases in the theory of vector spaces, and the subjective indistinguishability of propaganda and other forms of instruction.

So that was 2013 in the year of our Common Era. But what, I hear you ask (both of you who clicked through after the break), what about twenty-fourteen? Having considered the year that was, what of the year that hasn't yet? What is your faithful correspondent thinking about writing ("thinking about writing")? Well, without giving too much away—and, come to think of it, without much of a track record of successfully predicting my future behavior—I'll say to expect more computer-scientific contrivances and more detailed fiction, but probably somewhat fewer overall posts (this last partially because I'm going to be busy contributing to world economic growth, and partially because I want to put more emphasis on the virtue of Quality rather than the virtue of Shipping). So don't touch that bookmark! The best is yet to come! I thank you for reading, and remain,

Your faithful correspondent,
Zack M. Davis

Genre

"I didn't intend for my blog to become a compendium of very short dialogues! It's just—it's just—"

"They're easier to write than anything else?"

"—they're easier to write than anything else."

Classification

"So, what do you think it—"

"It's a Turing machine."

"You didn't even look at it!"

"I don't need to!"

"&c."

Is it wrong that I feel more positively disposed towards Etsy after figuring out where it must have (must have) gotten its name?

Fortune

"They're going to pay you X dollars a year? But that's fantastic! You're rich!"

"I don't like the way you say that. You make it sound as if I had won the lottery."

"Lottery, new job, what's the difference? Good fortune should be celebrated."

"Not like that. If they're going to pay me X dollars a year, that means I have one year to create X dollars of economic value. It's a serious responsibility."

Cover Letter

Beset by nights of torment,
Bent to keep your site performant,
Beneath the moon aloft there then appear
Beautied forms so gallant who
Beseech the gods of Talent to
Bequeath a junior software engineer!

You need someone specific,
One computer-scientific,
Who can complete the team at [Company Name],
Whose reputation takes repose
In repos replete with code which shows
[They put the other candidates to shame].

From graph theory to jQuery,
And matters in between,
My code Pythonic, words unironic,
I greet you through this screen
And ask, if just to highlight it,
If you've positions I might fit,
With pretense shed entirely.
I then said, "Hire me."

Thinking About Writing

"I've been thinking about writing a novel."

"I don't understand."

"I said, I've been thinking about writing a novel. What's there not to understand?"

"The phrase thinking about writing. Is that even grammatical? And if so, what could it possibly mean?"

"What? It's perfectly gram—oh, I get it. Fine. No hedging: I am going to write a novel!"

"Sorry, I still don't get it. I know what it means to have written something, or to be at a keyboard writing something. But to be going to write something, in the unobservable future? Even if the concept is coherent—and I'm not sure it is—how could you possibly know?"

Highball

"Now remember: when you're negotiating salary and they press you for a number, do not reveal your BATNA. Give them an unrealistically high estimate and make them negotiate down. Now practice on me."

"Ahem. Well, sir or madam, given my demonstrated expertise and the value I could create for this company, I think a fair starting salary would be ... sixty thousand."

"No, no, no! I said—"

"Per day."

"Uh ... that's a little too—"

"In Bitcoin."

Numbers Between 0 and 1 (Non-exhaustive List)

π/4, the Euler–Mascheroni constant, Chaitin's construction, the fraction of my internal narrative which consists of various rephrasings of I've been a contemptible fool; but, that's decision-theoretically irrelevant

Computing the Powerset

Suppose we want to find the powerset of a given set, that is, the set of all its subsets. How might we go about it? Well, the powerset of the empty set is the set containing the empty set.

$$\mathcal{P}(\emptyset)=\{\emptyset\}$$

And the powerset of the union of a set S with a set containing one element e, is just the union of the powerset of S with the set whose elements are like the members of the powerset of S except that they also contain e.

$$\mathcal{P}(S\cup\{e\})=\mathcal{P}(S)\cup\{t\cup\{e\}\}_{t\in\mathcal{P}(S)}$$

So in Clojure we might say

(require 'clojure.set)

(defn include-element [collection element]
  (map (fn [set] (clojure.set/union set #{element}))
       collection))

(defn powerset [set]
  (if (empty? set)
    #{#{}}
    (let [subproblem (powerset (rest set))]
      (clojure.set/union subproblem
                         (include-element subproblem
                                          (first set))))))

An Unauthorized Use of Ms. Gunnison's Secret Time Machine

Gunni told her not to touch it;
Common sense would say as much in
Plainer words, for these devices
Warrant care if not a license,
Just to view the glowing screen
Of the forbidden time machine.

It's meant for research that's historical,
Not for making oneself an oracle,
Not for giving Voltaire a tease,
Or climbing up Pangaean trees.

But Laura had a burden
In this timeline's normal version,
So she made her own incursion
To years the burden had occurred in.
No one knows just what she did,
Or how the timeline handled it,
Or how things were supposed to go
If Laura had not changed the flow.

And as we gathered in her mansion,
And I checked my gift poem's scansion,
I had to wonder why she will not tell
Why her investments have gone so well,
Or why all her enemies have come down with
Fatal but late-to-manifest genetic diseases,
Like Huntington's chorea.
I dismissed my concerns and raised my drink
To sing: Happy birthday—or so we think.

Conversational Overhead

A woman of wisdom once told me to heed Paul Graham's advice to notice the things you can't say and then don't say them, which stance I'm updating slightly towards, because even when you're only making a perfectly reasonable point along the lines of Policy debates should not appear one-sided; I don't think that your Argument A actually supports Policy X (although I agree that X could be desireable for reasons independent of A) and everyone is charitable and no one bites, there's still a huge amount of emotional overhead incurred just by being in the conversation at all, because even when and you and your interlocutors are honest, you almost never have common knowledge of that honesty, so your interlocutors aren't necessarily sure that you're not just disagreeing with A out of secret enmity towards X, and you're not sure that they're sure that you're not, all of which drama is a drain on mental energy that could otherwise have been allocated to entirely grown-up concerns like JavaScript and money.

Quotations III

I am convinced, from many experiments, I could not study, to any degree of perfection, either mathematics, arithmetic, or algebra, without being a Deist, if not an Atheist.

—John Wesley

Selfishness and altruism are positively correlated within individuals, for the obvious reason.

—@InstanceOfClass

There is only one book: the world book. And all books are volumes of this one book. And all pages are pages of this world book. [...] You’re already skimming. You're already skipping over. You’re already not reading all the pages of the book.

—Khatzumoto, "All Reading Is Skimming"

The moral is that it is probably better not to sin at all, but if some kind of sin you must be pursuing,

—"Portrait of the Artist as a Prematurely Old Man" by Odgen Nash

Science may not fucking love you back.

—Rachel Haywire

And that after this is accomplished, and the brave new world begins

—"The Gods of the Copybook Headings" by Rudyard Kipling

App Academy Diary, Week Nine

Sunday 17 November 2013— This was the last week of App Academy's regular course content; the next cohort starts Monday and my cohort will begin the three-week "post-course" mostly focused on interview practice, applying for jobs, &c. I got Superscription into a non-embarassing state: I made the feed-fetching happen as a scheduled task, added guest users, introduced the ability to mark entries as having been read, made an attractive click-and-drag category selector, &c. I still want to—at the very least—implement infinite-scroll pagination (fetching all the unread entries from the start can be very slow if there are a lot of them) and rewrite the category selector's terrible, terrible code. On Friday a lot of my class went to the San Francisco Startup Job Fair at noon, and we also had our demo day at the office at three. I think I made an okay showing? But thanks for reading.

You Can't Spell X Without Y

Why is it considered rude to reschedule an event after you've already sent out the invitations? Why do people stubbornly rejecting a compromise tend to do so in a polite and kindly manner? Why did you name your car Rainbow Dash? Speculative answers to these and other questions might be found in the following list of observations.

You can't spell alliteratively without literati.
You can't spell announcement without cement.
You can't spell apprenticeship without entice.
You can't spell chemotherapy without mother.
You can't spell eponymous without pony.
You can't spell compassion without compass.
You can't spell literate without iterate.
You can't spell disappointingly without tingly.
You can't spell disapproving without roving.
You can't spell disconcerting without sconce.
You can't spell discontinuance without nuance.
You can't spell ill-naturedness without redness.
You can't spell illustrative without strati.
You can't spell intransigently without gently.
You can't spell resolute without solute.
You can't spell oversuspicious without versus.
You can't spell precedent without recede.
You can't spell vindictive without indict.

World's Best

"We need some sort of slogan to go on our advertisements. Any ideas?"

"How about 'World's Best Widgets'?"

"Ha—no."

"Too cliché?"

"Not ambitious enough."

"Not ambitious enough?! How so?"

"Think about it! What use is it to have the best widget in today's world, if someone might just invent a better widget tomorrow? And who would be content to have the best widget in our universe, past, present, and future, when still better widgets might have existed if things had gone differently? No! I want all our potential customers to know that we make the best widgets in all possible worlds!"

Cascading Stable Strategies

"What do you think of my website's new look? I designed it with ESS."

"It's hideous! And you mean CSS."

"No, ESS. I used genetic algorithms to 'breed' new designs until the pool of candidates settled down into equilibrium."

"Ah."

App Academy Diary, Week Eight

Tuesday 5 Novembmer 2013— Yesterday was our last pair-programming project; I worked with Ben Watts on a little chat server in Node using WebSockets. I felt like perhaps there was something regrettable about shoving so much functionality into a big callback, but maybe that's just the nature of JavaScript, and not really regrettable at all? On Sunday I started my RSS-aggregator capstone project, now called Superscription.

entity-relationship diagram for the Superscription domain model: User, Category, and Subscription models connecting to UserSubscription and Entry

Thursday 7 Novembmer 2013— I guess Superscription is going okay. Here's how it works. You can sign up and sign in and stuff. (I used Devise for this rather than rolling my own.) There are forms for adding a category or a subscription URL. When you add a URL, stuff happens which depends on stuff. More specifically, if you enter a URL which the system has never seen before, then a new Subscription model (which accepts_nested_attributes_for an associated UserSubscription join model) is created, and it requests the feed from the URL you supplied and tries to parse the XML using Nokogiri. If that doesn't work, it suggests that maybe you got the URL wrong, but if it does work, then it makes a bunch of Entry objects for all the stories in the feed, saves everything, and redirects you to your subscription index page. But if the system has already seen your URL before (presumably from another user with similar reading tastes), then it doesn't wastefully fetch all that data again: it just makes a new UserSubscription that points to you and your Category and the Subscription that it already has. It's efficient! Then when you click the "Read!" button (which is non-hideously positioned thanks to nearly an hour of fighting the CSS), all your news is organized by category under Bootstrap tabs. And on the subscription index page, when you click the little trash can next to a subscription, then it does the cool jQuery fade-out thing and sends an Ajax request telling the server to destroy that UserSubscription.

early Superscription screenshot showing a logged-in user's RSS feed reader with a "Google barge mystery unfurled" story open

Sunday 10 Novembmer 2013— You know, I don't think I'm happy with Superscription's current state. As currently written, it's fetching feeds inside of the request-response cycle, which is just awful on a technical level even if I seem to have gotten away with it so far. I'll want to do some serious rework this week (next week?—I suppose these entries have been following the Monday-indexed convention) so that the fetch happens as a background job (even though that will cost money to host on Heroku, which only offers one free web dyno). And it could do with a lot more JavaScripty features on the frontend, and I need to read the Bootstrap documentation more carefully.

I made some improvements to Wires today. I got logout working, introduced redirection after logging in or out, and added a favicon! Yet I still don't have associations working, due to unforeseen scoping issues.