« | » Main « | »

Goodbye Tabblo

Tuesday 31 January 2012

A short recap of my history with Tabblo, a photo-sharing, storytelling site: I joined the startup in January 2006, we were acquired by Hewlett-Packard in May 2007, and I left HP in December of 2010.

I mention this because another milestone in my relationship with Tabblo was reached this month: not only are all of the original startup employees gone from HP (I was the last), but now all of the employees I hired to work on the server code are also gone. Now I literally don't know the people responsible for the site. In this case, "responsible" doesn't mean, "updates the code for the site," because nothing has been changed on the site in years. In this case, "responsible" means "will fix the servers if they fail."

As I figure it, there's only one milestone left to go: tabblo.com will eventually stop working, and no one will know how to fix it, and tabblo.com will be gone for good. Computers don't run indefinitely. Left alone, servers will go for a long time, but eventually something will break. I don't think anyone at Hewlett-Packard will miss tabblo.com, and I don't think anyone there would know how to fix it if it broke.

I loved Tabblo, both as a job and as a product, and I have a message for the current Tabblo users: leave Tabblo. I know there aren't similar alternatives, but Tabblo will not last forever. You should leave while it is still your choice.

Sad Tabblo

If the past is any guide, some Tabblo users will want to do something to make HP care, to make them pay attention and take care of Tabblo. This is futile, HP won't care, not because HP is bad, but because Tabblo is not a viable business. True, that's partly due to HP's neglect of it over the years, but even when we were acquired, the site was not an interesting business proposition for HP.

When HP bought us, they already had a photo site, Snapfish. It isn't a community the way Tabblo is, and it doesn't allow for the same range of self-expression as Tabblo does. But none of that mattered. Whatever you think of Tabblo vs Snapfish, the fact remains: HP was never interested in Tabblo as a web site. Snapfish already had millions of customers, and generated revenue for HP. HP wasn't about to confuse people by running two photo sites, and there weren't enough paying customers on Tabblo to make merging them a priority. Tabblo was never a money-maker at the scale a company like HP needs.

HP didn't acquire Tabblo to get tabblo.com. They acquired Tabblo so that we could build other web sites that used Tabblo-like technology to make web content printable. Tabblo.com was left running because it was easier to let it run than to shut it down. As time went on, other web sites were run with the same code on the same servers, so shutting down Tabblo was tricky logistically. Now those other web sites are gone, and Tabblo just keeps on running. With the latest employee departures, no one at HP even knows how to shut it down, other than to simply pull the plug.

The Tabblo site is still running, but it won't indefinitely. When it fails, it will be gone. I'm not putting any more pictures on it, and I don't think anyone else should either.

The only reason I don't feel bad about saying this is that Tabblo stopped being a viable site a long time ago, and we confronted the possibility of it disappearing a long time ago too. There were various rumblings over the years of someone at HP finally deciding to shut down the site, but it never happened. Ironically, the reason the site will be allowed to run until it simply dies is because HP knows they shouldn't just shut off the servers. They know that the users deserve some advance notice, that a plan should be put in place for an orderly shut down. But that takes time and attention and focus, and Tabblo isn't important enough to HP to get time or attention or focus. So it will simply run until it dies.

Keep in mind: I don't work at HP, and I don't have any direct knowledge about anything happening there. Maybe things are better than I imagine. But I saw these forces at work while I was there, and I've been in touch with the last ones out the door to know that nothing had gotten visibly better.

My last Tabblo work was writing Tabblo Lifeboat, a tool you can use to download all of your tabblos along with their photos. If you have stuff on Tabblo, give it a try.

To the current users of Tabblo: find something else. Goodbye Tabblo, I love you in lots of ways, I wish it had turned out differently. It was fun. I did the best I could.

Slim comparisons

Thursday 26 January 2012

Hanging out in the #python IRC channel today, I learned something new about Python comparisons. It isn't so much a new detail of the language, as a way to make use of a detail, a clever technique that I hadn't seen before.

When defining a class, it's often useful to define an equality comparison so that instances of your class can be considered equal. For example, in an object with three attributes, the typical way to define __eq__ is like this:

class Thing(object):
    def __init__(self, a, b, c):
        self.a = a
        self.b = b
        self.c = c

    def __eq__(self, other):
        print "Comparing %r and %r" % (self, other) 
        return (
            self.a == other.a and
            self.b == other.b and
            self.c == other.c
            )

When run, it shows what happens:

>>> x = Thing(1, 2, 3)
>>> y = Thing(1, 2, 3)
>>> print x == y
Comparing <Thing 37088896> and <Thing 37088952>
True

Here the __eq__ method compares the three attributes directly on the self and other objects, and returns a boolean, a simple direct comparison.

But on IRC, a different technique was proposed:

class Thing(object):
    def __init__(self, a, b, c):
        self.a = a
        self.b = b
        self.c = c

    def __eq__(self, other):
        print "Comparing %r and %r" % (self, other) 
        return (self.a, self.b, self.c) == other

Now when we run it, something unusual happens:

>>> x = Thing(1, 2, 3)
>>> y = Thing(1, 2, 3)
>>> print x == y
Comparing <Thing 37219968> and <Thing 37220024>
Comparing <Thing 37220024> and (1, 2, 3)
True

Our __eq__ is being called twice! The first time, it's called with two Thing objects, and it tries to compare a tuple of (1, 2, 3) to other, which is y, which is a Thing. Tuples don't support comparison to Thing's, so it returns NotImplemented. The == operator handles that case, and relying on the commutative nature of ==, tries swapping the two arguments. That means comparing y to (1, 2, 3), which calls our __eq__ again. Now it compares (1, 2, 3) to (1, 2, 3), which succeeds, producing the final True result.

This is an interesting technique, but I'm not sure I like it. For one thing, the code doesn't read clearly. It's comparing a tuple to an object, which isn't supported. It only makes sense when you keep in mind the argument-swapping dance.

For another, it make operations work that maybe shouldn't:

x == (1, 2, 3)
(1, 2, 3) == x

I don't know that I want these comparisons to succeed. It exposes internals that should be hidden. Of course, why would a caller who didn't know the internals try a comparison like this? But things like this have a way of creeping out to bite you.

I'm glad to have a better understanding of the workings of comparisons, but I'm not sure I'll write them like this.

Decorated fabric over the edge

Tuesday 10 January 2012

Like many, I use Fabric to write deploy procedures, but I feel like I'm doing it wrong. Fabric is fundamentally based on the ideas of "hosts" and "tasks". You write a Python file whose functions are tasks, and from the command line you can ask that a list of tasks be performed on a list of hosts.

Tasks can be decorated to affect their execution, for example, the @runs_once decorator will mean the function is only executed once, no matter how many hosts are specified. This can be useful for performing initial work, such as preparing a tarball to be copied to many hosts. So for example, I can write something like this:

@task
def deploy():
    make_tarball()
    copy_tarball()

@runs_once
def make_tarball()
    # .. create a .tgz ..

def copy_tarball()
    put('the_tar_ball.tgz', '/tmp/')

Fabric will run this by running the deploy task for each host, which will call both make_tarball and copy_tarball, but the runs_once decorator on make_tarball means that it will only be executed for the first host, while copy_tarball will be executed for all of them.

This is great, and building on it for a multi-server deploy, I wanted to have functions that would be run on a subset of the hosts. I have servers divided into roles: app server vs. static content server, for example. Fabric provides a role system, and includes a @roles decorator to control what gets run where:

env.roledefs.update({
    'app': ['www1', 'www2'],
    'static': ['stat1']
})

@roles('app')
def my_func():
    pass

But we run into a problem: @roles only works on top-level tasks invoked from the command line. If I decorate my copy_tarball function with it, it will be ignored. This is because of how the decorator has been written: it annotates the function with role information, and the Fabric main loop knows how to read that annotation to decide what tasks to run on which hosts.

I wanted a deploy script that looked something like this:

@task
def deploy():
    copy_to_apps()
    copy_to_static()

@only_roles('app')
def copy_to_apps():
    #.. copy stuff ..

@only_roles('static')
def copy_to_static():
    #.. copy stuff ..

So I wrote my own decorator to do roles the way I wanted:

def only_roles(*roles):
    """Make a function run only on hosts that have certain roles."""
    def _dec(fn):
        @functools.wraps(fn)
        def _wrapped(*args, **kwargs):
            for role in roles:
                if env.host_string in env.roledefs.get(role, ()):
                    return fn(*args, **kwargs)
        return _wrapped
    return _dec

But I felt funny about this: I saw something in the Fabric docs that sounded like just what I wanted, but it didn't work as I thought it would, so I had to write my own. This makes me think I'm using Fabric wrong.

The runs_once decorator is great for doing one-time initial work, and I found I wanted a book-end for it: a way to do one-time cleanup work. Fabric provided nothing, and I could see why: there's no global knowledge about all the hosts and tasks, and no way to specify work to be done after they are through. For that matter, there's no way to specify work to be done before they start, but @runs_once provides that effect.

So I wrote another decorator, this one more devious and risky:

def runs_last(func):
    """A decorator to run the function only on the *last* host.

    This only works if you don't apply any other restrictions
    on the function.

    """
    func.times_invoked = 0
    @functools.wraps(func)
    def decorated(*args, **kwargs):
        func.times_invoked += 1
        all_hosts = set()
        for hosts in env.roledefs.values():
            all_hosts.update(hosts)
        if func.times_invoked == len(all_hosts):
            func(*args, **kwargs)
    return decorated

Here we count the number of invocations, and guess at the number of the last one based on the hosts we know about. There are a variety of ways this could not work, but it was fine in my environment.

Since I'm sharing useful Fabric decorators, here's one that prevents repetitive work being done that won't have any extra effect:

def idempotent(func):
    """Don't invoke `func` more than once for host and arguments."""
    func.past_results = {}
    @functools.wraps(func)
    def decorated(*args, **kwargs):
        key = (env.host_string, args, tuple(kwargs.items()))
        if key not in func.past_results:
            func.past_results[key] = func(*args, **kwargs)
        return func.past_results[key]
    return decorated

Am I using Fabric wrong? It seems like maybe I'm expecting it to do too much, like the right way is to have my deploy() function in a larger script somehow. Or is Fabric fine like this, and I've just missed the right path?

Safari buys Threepress

Monday 9 January 2012

For the last year, I've been doing a lot of freelance work for Threepress on Ibis Reader, their HTML5 e-book platform. The good news out this morning is that they have been acquired by Safari Books Online.

This is a great fit, because of their already-shared business, technology, and culture. Congratulations to Liza, Keith, Jonathan, and Chuck, who have all done great work, and will now be Safari employees. Xconomy has some sound bites from Liza and her new boss about the acquisition.

The down-side for me is that I will not be working as much with them now, but there may be some smaller projects, and I'm really glad to have had the year with them that I did.

PS: it isn't mentioned in the new blurbs, but Ibis Reader is a Django project, so this is a big plus for Django and Python also.

Where can I walk to in 30 minutes?

Saturday 7 January 2012

I like to walk to explore, even in my home neighborhood. So I like to find new places to go. I wanted to print map and draw a 1½-mile radius circle on it to find a good range.

But the set of points you can reach with a mile and a half of walking is not a circle, it will depend on where the streets are. Anyone know of a map app that can show me the actual range?

Mapnificent is a similar idea: it shows how far you can get in a given amount of time, using public transportation, walking, and maybe biking. But it also just draws circles.

Is what I want out there?

Happy and quiet

Monday 2 January 2012

Happy New Year everyone, I hope your 2011 was good, and that 2012 will be even better. This year I hear people talking about "intentions" rather than "resolutions," is that a reflection of reality, or an early cop-out so that failure doesn't feel like "failure"?

When I think about the year past and the year ahead, my "intention" is to spend my time more mindfully. That doesn't dictate how I will spend it, or even that I should spend it purposefully, but that I should decide how to spend it, rather than falling into habits and ruts.

In Pico Iyer's relevant and recent widely-circulated piece, The Joy of Quiet, he notes our "progress:"

In barely one generation we’ve moved from exulting in the time-saving devices that have so expanded our lives to trying to get away from them — often in order to make more time. The more ways we have to connect, the more many of us seem desperate to unplug. Like teenagers, we appear to have gone from knowing nothing about the world to knowing too much all but overnight.

Luckily, the bulk of the essay is not on this same, "things are so different now" theme. In fact, he quotes Blaise Pascal:

Distraction is the only thing that consoles us for our miseries, and yet it is itself the greatest of our miseries.

Later, he calls on Thoreau:

We have more and more ways to communicate, as Thoreau noted, but less and less to say. Partly because we’re so busy communicating.

Here's the full Thoreau paragraph:

As with our colleges, so with a hundred "modern improvements"; there is an illusion about them; there is not always a positive advance. The devil goes on exacting compound interest to the last for his early share and numerous succeeding investments in them. Our inventions are wont to be pretty toys, which distract our attention from serious things. They are but improved means to an unimproved end, an end which it was already but too easy to arrive at; as railroads lead to Boston or New York. We are in great haste to construct a magnetic telegraph from Maine to Texas; but Maine and Texas, it may be, have nothing important to communicate. Either is in such a predicament as the man who was earnest to be introduced to a distinguished deaf woman, but when he was presented, and one end of her ear trumpet was put into his hand, had nothing to say. As if the main object were to talk fast and not to talk sensibly. We are eager to tunnel under the Atlantic and bring the Old World some weeks nearer to the New; but perchance the first news that will leak through into the broad, flapping American ear will be that the Princess Adelaide has the whooping cough. After all, the man whose horse trots a mile in a minute does not carry the most important messages; he is not an evangelist, nor does he come round eating locusts and wild honey. I doubt if Flying Childers [an English racehorse] ever carried a peck of corn to mill.

So which is it? Are we in a bad state that has developed in the last generation, or are we merely in the same predicament as Pascal and Thoreau? I think it's easy for people to lament their situation and think their condition is a recent malady. Even Thoreau complains of "modern improvements," but if Thoreau complained in 1854, and Pascal in 1658, then how can we honestly blame 21st-century developments for our situation?

People are omnivores, and along with meat and vegetables, we consume information, attention and distraction, craving as much as we can get. Too much food will make us fat, too much distraction will make us, well, distracted. It's true now, and it was true in Thoreau's time.

The new technologies certainly contribute to the problem, but they didn't create it. Thoreau's quip about the "broad, flapping American ear" could have been written about tabloids, TV, and Twitter, but it wasn't. Iyer's solution is quiet, a respite from connection, but I think we can also use the tools we have in better ways.

I liked Alex Soojung-Kim Pang's conclusion:

Connectivity and silence, collaboration and contemplation, sociability and solitude, are not opposites, and we shouldn't think that we should choose between one and the other. Rather, they're like food and water, or parents and children. Each is essential; they're different but not mutually exclusive. The great challenge is to find places for them all, and to know how to use them.

So, along with the usual resolutions to eat more healthfully, here's to spending time more mindfully.

« | » Main « | »