The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.

NAME

AnyEvent::Redis::Federated - Full-featured Async Perl Redis client

SYNOPSIS

  use AnyEvent::Redis::Federated;

  my $r = AnyEvent::Redis::Federated->new(%opts);

  # batch up requests and explicity wait for completion
  $redis->set("foo$_", "bar$_") for 1..20;
  $redis->poll;

  # send a request with a callback
  $redis->get("foo1", sub {
    my $val = shift;
    print "cb got: $val\n"; # should print "cb got: 1"
  });
  $redis->poll;

DESCRIPTION

This is a wrapper around AnyEvent::Redis which adds timeouts, connection retries, multi-machine cluster configuration (including consistent hashing), node groups, and other magic bits.

HASHING AND SCALING

Keys are run through a consistent hashing algorithm to map them to "nodes" which ultimately map to instances defined by back-end host:port entries. For example, the redis_1 node may map to the host and port redis1.example.com:63791, but that'll all be transparent to the user.

However, there are features in Redis that are handy if you know a given set of keys lives on a single insance (a wildcard fetch like KEYS gmail*, for example). To facilitate that, you can specify a "key group" that will be hashed insead of hashing the key.

For example:

  key group: gmail
  key      : foo@gmail.com

  key group: gmail
  key      : bar@gmail.com

Put another way, the key group defaults to the key for the named operation, but if specified, is used instead as the input to the consistent hashing function.

Using the same key group means that multiple keys end up on the same Redis instance. To do so, simply change any key in a call to an arrayref where item 0 is the key group and item 1 is the key.

  $r->set(['gmail', 'foo@gmail.com'], 'spammer', $cb);
  $r->set(['gmail', 'bar@gmail.com'], 'spammer', $cb);

Anytime a key is an arrayref, AnyEvent::Redis::Federated will assume you're using a key group.

PERSISTENT CONNECTIONS

By default, AnyEvent::Redis::Federated will use a new connection for each command. You can enable persistent connections by passing a persistent agrument (with a true value) in new(). You will likely also want to set a idle_timeout value as well. The idle_timeout defaults to 0 (which means no timeout). But if set to a posistive value, that's the number of seconds that a connection is allowed to remain idle before it is re-established. A number up to 60 seconds is probably reasonable.

SHARED CONNECTIONS

Because creating AnyEvent::Redis::Federated objects isn't cheap (due mainly to initializing the consistent hashing ring), there is a mechanism for sharing a connection object among modules without prior knowledge of each other. If you specify a tag in the new() constructor and another module in the same process tries to create an object with the same tag, it will get a reference to the one you created.

For example, in your code:

  my $redis = AnyEvent::Redis::Federated->new(tag => 'rate-limiter');

Then in another module:

  my $r = AnyEvent::Redis::Federated->new(tag => 'rate-limiter');

Both $redis and $r will be references to the same object.

Since the first module to create an object with a given tag gets to define the various retry parameters (as described in the next section), it's worth thinking about whether or not you really want this behavior. In many cases, you may--but not in all cases.

Tag names are used as a hash key internally and compared using Perl's normal stringification mechanism, so you could use a full-blown object as your tag if you wanted to do such a thing.

CONNECTION RETRIES

If a connection to a server goes down, AnyEvent::Redis::Federated will notice and retry on subsequent calls. If the server remains down after a configured number of tries, it will go into back-off mode, retrying occasionally and increasing the time between retries until the server is back on-line or the retry interval time has reached the maximum configured vaue.

The module has some hopefully sane defaults built in, but you can override any or all of them in your code when creating an AnyEvent::Redis::Federated object. The following keys contol this behvaior (defaults listed in parens for each):

   * max_host_retries (3) is the number of times a server will be
     re-tried before starting the back-off logic

   * base_retry_interval (10) is the number of seconds between retries
     when entering back-off mode

   * retry_interval_mult (2) is the number we'll multiply
     base_retry_interval by on each subsequent failure in back-off
     mode

   * max_retry_interval (600) is the number of seconds that the retry
     interval will not exceed

When a server first goes down, this module will warn() a message that says "redis server $server seems down\n" where $server is the $host:$port pair that represents the connection to the server. If this is the first time that server has been seen down, it will additionally warn() "redis server $server down, first time\n".

If a server remainds down on subsequent retries beyond max_host_retries, the module will warn() "redis server $server still down, backing off" to let you know that the back-off logic is about to kick in. Each time the retry_interval is increased, it will warn() "redis server $server retry_interval now $retry_interval".

If a down server does come back up, the module will warn() "redis server $server back up (down since $down_since)\n" where $down_since is human readable timestamp. It will also clear all internal state about the down server.

TIMEOUTS

This module provides support for command timeouts.

The command timeout controls how long we're willing to wait for a response to a given request made to a Redis server. Redis usually responds VERY quickly to most requests. But if there's a temporary network problem or something tying up the server, you may wish to fail quickly and move on.

NOTE: these timeouts are implemented using alarm(), so be careful of also using alarm() calls in your own code that could interfere.

MULTI-KEY OPERATIONS

Some operations can operate on many keys and might cross server boundries. They are currently supported provided that you remember to specify a hash key to ensure the all live on the same node. Example operations are:

  * mget
  * sinter
  * sinterstore
  * sdiff
  * sdiffstore
  * zunionstore

Previous versions of this module listed these as unsupported commands, but that's rather limiting. So they're supported now, provided you know what you're doing.

METHODS

AnyEvent::Redis::Federated inherits all of the normal Redis methods. However, you can supply a callback or AnyEvent condvar as the final argument and it'll do the right thing:

  $redis->get("foo", sub { print shift,"\n"; });

You can also use call chaining:

  $redis->set("foo", 1)->set("bar", 2)->get("foo", sub {
    my $val = shift;
    print "foo: $val\n";
  });

CONFIGURATION

AnyEvent::Redis::Federated requires a configuration hash be passed to it at instantiation time. The constructor will die() unless a unless a 'config' option is passed to it. The configuration structure looks like:

  my $config = {
    nodes => {
      redis_1 => { address => 'db1:63790' },
      redis_2 => { address => 'db1:63791' },
      redis_3 => { address => 'db2:63790' },
      redis_4 => { address => 'db2:63791' },
    },
  };

The "nodes" and "master_of" hashes are described below.

NODES

The "nodes" configuation maps an arbitrary node name to a host:port pair. (The hostname can be replaced with an IP address.)

Node names (redis_N in the example above) are VERY important since they are the keys used to build the consistent hashing ring. It's generally the wrong idea to change a node name. Since node names are mapped to a host:port pair, we can move a node from one host to another without rehashing a bunch of keys.

There is unlikely to be a need to remove a node.

Adding nodes to a cluster is currently not well-supported, but is an area of active development.

EVENT LOOP

Since this module wraps AnyEvent::Redis, there are two main ways you can integrate it into your code. First, if you're using AnyEvent, it should "just work." However, if you're not otherwise using AnyEvent, you can still take advantage of batching up requests and waiting for them in parallel by calling the poll() method as illustrated in the synopsis.

Calling poll() asks the module to issue any pending requests and wait for all of them to return before returning control back to your code.

EXPORT

None.

SEE ALSO

The normal AnyEvent::Redis perl client perldoc AnyEvent::Redis.

The Redis API documentation:

  http://redis.io/commands

Jeremy Zawodny's blog describing craigslist's use of redis sharding:

  http://blog.zawodny.com/2011/02/26/redis-sharding-at-craigslist/

That posting described an implementation which was based on the regular (non-async) Redis client from CPAN. This code is a port of that to AnyEvent.

BUGS

Please report bugs as issues on github:

  https://github.com/craigslist/perl-AnyEvent-Redis-Federated/issues

AUTHOR

Jeremy Zawodny, <jzawodn@craigslist.org>

Joshua Thayer, <joshua@craigslist.org>

Tyle Phelps, <tyler@craigslist.org>

COPYRIGHT AND LICENSE

Copyright (C) 2009-2011 by craigslist.

This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself, either Perl version 5.10.0 or, at your option, any later version of Perl 5 you may have available.