Mojo-Redis

 view release on metacpan or  search on metacpan

.perltidyrc  view on Meta::CPAN

-pbp     # Start with Perl Best Practices
-w       # Show all warnings
-iob     # Ignore old breakpoints
-l=120   # 120 characters per line
-mbl=2   # No more than 2 blank lines
-i=2     # Indentation is 2 columns
-ci=2    # Continuation indentation is 2 columns
-vt=0    # Less vertical tightness
-pt=2    # High parenthesis tightness
-bt=2    # High brace tightness
-sbt=2   # High square bracket tightness
-isbc    # Don't indent comments without leading space
-wn      # Opening and closing containers to be "welded" together

Changes  view on Meta::CPAN

Revision history for perl distribution Mojo-Redis

3.29 2022-02-23T14:56:18+0900
 - Fix use of "defined" in unit test

3.28 2022-02-21T15:40:56+0900
 - Add channel to Mojo::Redis::PubSub::listen() callback

3.27 2021-11-20T10:51:49+0900
 - Add experimental "subscribe" and "psubscribe" events to Mojo::Redis::PubSub
 - Fix examples for set and expire #62
 - Fix handling "psubscribe" response from Redis #63
 - Fix sending database requests after connecting to sentinel server #64
 - Fix only passing on (p)message messages to listen handlers #67
 - Remove experimental write_q() method, and replaced it with write()
 - Remove the ->multi_p(@promises) syntax #68 #70
   Contributor: Jan "Yenya" Kasprzak

3.26 2021-03-01T09:01:51+0900
 - Avoid circular reference in redis response parser
   Contributor: Dan Book

3.25 2020-10-02T10:21:30+0900
 - Fix handling undef() in _process_...() methods #56
 - Fix some leaks in Mojo::Redis::PubSub
 - Add Mojo::Redis::PubSub->notify_p()

3.24 2019-05-07T22:25:50+0700
 - Fix PubSub->keyspace_listen() #42

3.23 2019-05-04T21:12:25+0700
 - Fix compatibility with Mojolicious 8.15 #46

3.22 2019-04-24T12:32:18+0700
 - Forgot to update protocol parser for Mojo::Redis::Cache after 3.21 #43
 - Fix broken link in Mojo::Redis::Connection #44
   Contributor: Mohammad S Anwar

3.21 2019-04-16T09:58:44+0700
 - Changed default protocol parser to Protocol::Redis::XS #43

3.20 2019-04-04T10:31:03+0700
 - Use Protocol::Redis::Faster instead of Protocol::Redis #38
 - Only decode data from bulk string responses #40
 - Fix allowing custom URL object with userinfo in constructor #41

3.19 2019-01-31T13:03:11+0900
 - Add support for encoding and decoding of JSON messages in Mojo::Redis::PubSub

3.18 2019-01-31T12:39:46+0900
 - Add reconnect logic for Mojo::Redis::PubSub #37
 - Add CAVEATS for Protocol::Redis::XS #38
 - Changed default protocol to Protocol::Redis #38
 - Updated documentation to use nicer variable names

3.17 2018-12-17T19:03:43+0900
 - Made connection-lost.t more robust
 - Add xread_structured() method
 - Add failing test for xread and Protocol::Redis::XS

3.16 2018-12-14T19:39:18+0900
 - Fix $db object from reconnecting #33

3.15 2018-12-13T08:24:10+0900
 - Fix connection-lost.t in other languages #30
 - Bumped Mojolicious version for Mojo::Promise support #32

3.14 2018-12-12T23:10:30+0900
 - Fix fork-safity for the blocking connection #28
 - Fix connection-lost.t in other languages #29

3.13 2018-12-11T14:44:45+0900
 - Fix rejecting promises when connection is lost #24
 - Fix connection.t when using a remote server #25
 - Fix cursor.t to use TEST_ONLINE #26 #27
   Contributor: Alexander Karelas

3.12 2018-12-07T12:04:19+0900
 - Add support for negative cache expire for serving stale data
 - Add destructor to Connection object to clean up connections
 - Add "close" event to Connection object

3.11 2018-08-17T00:01:33+0200
 - Fix invalid Makefile.PL

3.10 2018-08-16T23:41:10+0200
 - Add cluster commands #5
 - Add basic support for sentinel #13

3.09 2018-08-09T15:32:24+0200
 - Improved documentation for Cache

3.08 2018-08-02T15:37:32+0800
 - Add benchmark test #3
 - Fix cache() need to use Protocol::Redis because of binary data
 - Fix GEOPOS return value
 - Fix only decode response data if defined #19
 - Fix do not create new connection objects during global destruction
 - Improved return values in documentation

3.07 2018-07-12T09:59:05+0800
 - Add support for sending custom commands #18
 - Fix documentation issues
 - Will not enqueue connections if url() has changed

3.06 2018-07-11T11:00:01+0800
 - Fix processing exec() result
 - Improved example applications and add references from the POD

3.05 2018-07-11T10:24:59+0800
 - Fix holding $db in memory when issuing commands and returning promises
 - Add info_structured() method to Mojo::Redis::Database
 - Add example of Mojolicious application using Mojo::Redis::Cache

3.04 2018-07-11T09:24:09+0800
 - Add server commands #4
 - Add stream commands #6
 - Add support for keyspace notifications #10
 - Documented how pipelining works #7

3.03 2018-07-05T11:45:01+0800
 - Add eval(), evalsha() and script() #8
 - Cannot have a custom class for transactions #14

3.02 2018-07-01T18:24:37+0900
 - Add Mojo::Redis::Cache->memomize_p()
 - Add examples/twitter.pl
 - Add UTF-8 encoding as default and allow encoding to be changed #1
 - Add documentation for events #2
 - Add support for connecting to unix socket #12
 - Add support for offline cache #15
 - Add support for refreshing cache #16

3.01 2018-06-28T15:22:47+0900
 - Add Mojo::Redis::Cache
 - Add examples/chat.pl

3.00 2018-06-28T10:11:24+0900
 - Started on a new version to replace Mojo::Redis2 and the old Mojo::Redis
 - Mojo::Redis as EXPERIMENTAL
 - Add connection pool
 - Add Mojo::Redis::Cursor
 - Add Mojo::Redis::Database
 - Add Mojo::Redis::PubSub
 - Add Mojo::Redis::Transaction

MANIFEST  view on Meta::CPAN

.perltidyrc
Changes
cpanfile
examples/cache.pl
examples/chat.pl
examples/twitter.pl
lib/Mojo/Redis.pm
lib/Mojo/Redis/Cache.pm
lib/Mojo/Redis/Connection.pm
lib/Mojo/Redis/Cursor.pm
lib/Mojo/Redis/Database.pm
lib/Mojo/Redis/PubSub.pm
Makefile.PL
MANIFEST			This list of files
README.md
t/00-basic.t
t/benchmark.t
t/cache-offline.t
t/cache.t
t/connection-auth.t
t/connection-lost.t
t/connection-sentinel.t
t/connection-unix.t
t/connection.t
t/cursor.t
t/db.t
t/geo.t
t/keyspace-listen.t
t/method-coverage.t
t/pipelining.t
t/pubsub-reconnect.t
t/pubsub.t
t/redis.t
t/scripting.t
t/txn.t
t/xread.t
META.yml                                 Module YAML meta-data (added by MakeMaker)
META.json                                Module JSON meta-data (added by MakeMaker)

META.json  view on Meta::CPAN

{
   "abstract" : "Redis driver based on Mojo::IOLoop",
   "author" : [
      "Jan Henning Thorsen <jhthorsen@cpan.org>"
   ],
   "dynamic_config" : 0,
   "generated_by" : "ExtUtils::MakeMaker version 7.62, CPAN::Meta::Converter version 2.150010",
   "license" : [
      "artistic_2"
   ],
   "meta-spec" : {
      "url" : "http://search.cpan.org/perldoc?CPAN::Meta::Spec",
      "version" : 2
   },
   "name" : "Mojo-Redis",
   "no_index" : {
      "directory" : [
         "t",
         "inc"
      ]
   },
   "prereqs" : {
      "build" : {
         "requires" : {}
      },
      "configure" : {
         "requires" : {
            "ExtUtils::MakeMaker" : "0"
         }
      },
      "runtime" : {
         "requires" : {
            "Mojolicious" : "8.50",
            "Protocol::Redis::Faster" : "0.002",
            "perl" : "5.016"
         }
      },
      "test" : {
         "requires" : {
            "Test::More" : "0.88"
         }
      }
   },
   "release_status" : "stable",
   "resources" : {
      "bugtracker" : {
         "web" : "https://github.com/jhthorsen/mojo-redis/issues"
      },
      "homepage" : "https://github.com/jhthorsen/mojo-redis",
      "repository" : {
         "type" : "git",
         "url" : "https://github.com/jhthorsen/mojo-redis.git",
         "web" : "https://github.com/jhthorsen/mojo-redis"
      }
   },
   "version" : "3.29",
   "x_contributors" : [
      "Jan Henning Thorsen <jhthorsen@cpan.org>",
      "Dan Book <grinnz@grinnz.com>"
   ],
   "x_serialization_backend" : "JSON::PP version 4.06"
}

META.yml  view on Meta::CPAN

---
abstract: 'Redis driver based on Mojo::IOLoop'
author:
  - 'Jan Henning Thorsen <jhthorsen@cpan.org>'
build_requires:
  Test::More: '0.88'
configure_requires:
  ExtUtils::MakeMaker: '0'
dynamic_config: 0
generated_by: 'ExtUtils::MakeMaker version 7.62, CPAN::Meta::Converter version 2.150010'
license: artistic_2
meta-spec:
  url: http://module-build.sourceforge.net/META-spec-v1.4.html
  version: '1.4'
name: Mojo-Redis
no_index:
  directory:
    - t
    - inc
requires:
  Mojolicious: '8.50'
  Protocol::Redis::Faster: '0.002'
  perl: '5.016'
resources:
  bugtracker: https://github.com/jhthorsen/mojo-redis/issues
  homepage: https://github.com/jhthorsen/mojo-redis
  repository: https://github.com/jhthorsen/mojo-redis.git
version: '3.29'
x_contributors:
  - 'Jan Henning Thorsen <jhthorsen@cpan.org>'
  - 'Dan Book <grinnz@grinnz.com>'
x_serialization_backend: 'CPAN::Meta::YAML version 0.018'

Makefile.PL  view on Meta::CPAN

# Generated by git-ship. See 'git-ship --man' for help or https://github.com/jhthorsen/app-git-ship
use utf8;
use ExtUtils::MakeMaker;
my %WriteMakefileArgs = (
  NAME           => 'Mojo::Redis',
  AUTHOR         => 'Jan Henning Thorsen <jhthorsen@cpan.org>',
  LICENSE        => 'artistic_2',
  ABSTRACT_FROM  => 'lib/Mojo/Redis.pm',
  VERSION_FROM   => 'lib/Mojo/Redis.pm',
  EXE_FILES      => [qw()],
  OBJECT         => '',
  BUILD_REQUIRES => {}
,
  TEST_REQUIRES  => {
  'Test::More' => '0.88'
}
,
  PREREQ_PM      => {
  'Mojolicious' => '8.50',
  'Protocol::Redis::Faster' => '0.002',
  'perl' => '5.016'
}
,
  META_MERGE     => {
    'dynamic_config' => 0,
    'meta-spec'      => {version => 2},
    'resources'      => {
      bugtracker => {web => 'https://github.com/jhthorsen/mojo-redis/issues'},
      homepage   => 'https://github.com/jhthorsen/mojo-redis',
      repository => {
        type => 'git',
        url  => 'https://github.com/jhthorsen/mojo-redis.git',
        web  => 'https://github.com/jhthorsen/mojo-redis',
      },
    },
    'x_contributors' => [
  'Jan Henning Thorsen <jhthorsen@cpan.org>',
  'Dan Book <grinnz@grinnz.com>'
]
,
  },
  test => {TESTS => (-e 'META.yml' ? 't/*.t' : 't/*.t xt/*.t')},
);

unless (eval { ExtUtils::MakeMaker->VERSION('6.63_03') }) {
  my $test_requires = delete $WriteMakefileArgs{TEST_REQUIRES};
  @{$WriteMakefileArgs{PREREQ_PM}}{keys %$test_requires} = values %$test_requires;
}

WriteMakefile(%WriteMakefileArgs);

README.md  view on Meta::CPAN

# NAME

Mojo::Redis - Redis driver based on Mojo::IOLoop

# SYNOPSIS

## Blocking

    use Mojo::Redis;
    my $redis = Mojo::Redis->new;
    $redis->db->set(foo => 42);
    $redis->db->expire(foo => 600);
    warn $redis->db->get('foo');

## Promises

    $redis->db->get_p("mykey")->then(sub {
      print "mykey=$_[0]\n";
    })->catch(sub {
      warn "Could not fetch mykey: $_[0]";
    })->wait;

## Pipelining

Pipelining is built into the API by sending a lot of commands and then use
["all" in Mojo::Promise](https://metacpan.org/pod/Mojo%3A%3APromise#all) to wait for all the responses.

    Mojo::Promise->all(
      $db->set_p($key, 10),
      $db->incrby_p($key, 9),
      $db->incr_p($key),
      $db->get_p($key),
      $db->incr_p($key),
      $db->get_p($key),
    )->then(sub {
      @res = map {@$_} @_;
    })->wait;

# DESCRIPTION

[Mojo::Redis](https://metacpan.org/pod/Mojo%3A%3ARedis) is a Redis driver that use the [Mojo::IOLoop](https://metacpan.org/pod/Mojo%3A%3AIOLoop), which makes it
integrate easily with the [Mojolicious](https://metacpan.org/pod/Mojolicious) framework.

It tries to mimic the same interface as [Mojo::Pg](https://metacpan.org/pod/Mojo%3A%3APg), [Mojo::mysql](https://metacpan.org/pod/Mojo%3A%3Amysql) and
[Mojo::SQLite](https://metacpan.org/pod/Mojo%3A%3ASQLite), but the methods for talking to the database vary.

This module is in no way compatible with the 1.xx version of `Mojo::Redis`
and this version also tries to fix a lot of the confusing methods in
`Mojo::Redis2` related to pubsub.

This module is currently EXPERIMENTAL, and bad design decisions will be fixed
without warning. Please report at
[https://github.com/jhthorsen/mojo-redis/issues](https://github.com/jhthorsen/mojo-redis/issues) if you find this module
useful, annoying or if you simply find bugs. Feedback can also be sent to
`jhthorsen@cpan.org`.

# EVENTS

## connection

    $cb = $redis->on(connection => sub { my ($redis, $connection) = @_; });

Emitted when [Mojo::Redis::Connection](https://metacpan.org/pod/Mojo%3A%3ARedis%3A%3AConnection) connects to the Redis.

# ATTRIBUTES

## encoding

    $str   = $redis->encoding;
    $redis = $redis->encoding("UTF-8");

The value of this attribute will be passed on to
["encoding" in Mojo::Redis::Connection](https://metacpan.org/pod/Mojo%3A%3ARedis%3A%3AConnection#encoding) when a new connection is created. This
means that updating this attribute will not change any connection that is
in use.

Default value is "UTF-8".

## max\_connections

    $int   = $redis->max_connections;
    $redis = $redis->max_connections(5);

Maximum number of idle database handles to cache for future use, defaults to
5\. (Default is subject to change)

## protocol\_class

    $str   = $redis->protocol_class;
    $redis = $redis->protocol_class("Protocol::Redis::XS");

Default to [Protocol::Redis::XS](https://metacpan.org/pod/Protocol%3A%3ARedis%3A%3AXS) if the optional module is available and at
least version 0.06, or falls back to [Protocol::Redis::Faster](https://metacpan.org/pod/Protocol%3A%3ARedis%3A%3AFaster).

## pubsub

    $pubsub = $redis->pubsub;

Lazy builds an instance of [Mojo::Redis::PubSub](https://metacpan.org/pod/Mojo%3A%3ARedis%3A%3APubSub) for this object, instead of
returning a new instance like ["db"](#db) does.

## url

    $url   = $redis->url;
    $redis = $redis->url(Mojo::URL->new("redis://localhost/3"));

Holds an instance of [Mojo::URL](https://metacpan.org/pod/Mojo%3A%3AURL) that describes how to connect to the Redis server.

# METHODS

## db

    $db = $redis->db;

Returns an instance of [Mojo::Redis::Database](https://metacpan.org/pod/Mojo%3A%3ARedis%3A%3ADatabase).

## cache

    $cache = $redis->cache(%attrs);

Returns an instance of [Mojo::Redis::Cache](https://metacpan.org/pod/Mojo%3A%3ARedis%3A%3ACache).

## cursor

    $cursor = $redis->cursor(@command);

Returns an instance of [Mojo::Redis::Cursor](https://metacpan.org/pod/Mojo%3A%3ARedis%3A%3ACursor) with
["command" in Mojo::Redis::Cursor](https://metacpan.org/pod/Mojo%3A%3ARedis%3A%3ACursor#command) set to the arguments passed. See
["new" in Mojo::Redis::Cursor](https://metacpan.org/pod/Mojo%3A%3ARedis%3A%3ACursor#new). for possible commands.

## new

    $redis = Mojo::Redis->new("redis://localhost:6379/1");
    $redis = Mojo::Redis->new(Mojo::URL->new->host("/tmp/redis.sock"));
    $redis = Mojo::Redis->new(\%attrs);
    $redis = Mojo::Redis->new(%attrs);

Object constructor. Can coerce a string into a [Mojo::URL](https://metacpan.org/pod/Mojo%3A%3AURL) and set ["url"](#url)
if present.

# AUTHORS

Jan Henning Thorsen - `jhthorsen@cpan.org`

Dan Book - `grinnz@grinnz.com`

# COPYRIGHT AND LICENSE

Copyright (C) 2018, Jan Henning Thorsen.

This program is free software, you can redistribute it and/or modify it under
the terms of the Artistic License version 2.0.

cpanfile  view on Meta::CPAN

# You can install this projct with curl -L http://cpanmin.us | perl - https://github.com/jhthorsen/mojo-redis/archive/master.tar.gz
requires "perl"                    => "5.016";
requires "Mojolicious"             => "8.50";
requires "Protocol::Redis::Faster" => "0.002";

test_requires "Test::More" => "0.88";

examples/cache.pl  view on Meta::CPAN

#/usr/bin/env perl
use Mojolicious::Lite -signatures;

use Mojo::Redis;

helper redis => sub { state $r = Mojo::Redis->new };

helper cache => sub {
  my $c = shift;
  return $c->stash->{'redis.cache'} ||= $c->redis->cache->refresh($c->param('_refresh'));
};

helper get_redis_stats_p => sub {
  my ($c, $section) = @_;
  return $c->redis->db->info_structured_p($section ? ($section) : ());
};

get '/stats' => sub {
  my $c = shift->render_later;

  $c->cache->memoize_p($c, get_redis_stats_p => [$c->param('section')])->then(sub {
    $c->render(json => shift);
  })->catch(sub {
    $c->reply_exception(shift);
  });
};

app->start;

examples/chat.pl  view on Meta::CPAN

#!/usr/bin/env perl
use Mojolicious::Lite -signatures;

use lib 'lib';
use Mojo::Redis;

helper redis => sub { state $r = Mojo::Redis->new };

get '/' => 'chat';

websocket '/socket' => sub ($c) {
  my $pubsub = $c->redis->pubsub;
  my $cb = $pubsub->listen('chat:example' => sub ($pubsub, $msg) { $c->send($msg) });

  $c->inactivity_timeout(3600);
  $c->on(finish => sub { $pubsub->unlisten('chat:example' => $cb) });
  $c->on(message => sub ($c, $msg) { $pubsub->notify('chat:example' => $msg) });
};

app->start;

__DATA__
@@ chat.html.ep
<!DOCTYPE html>
<html>
  <head>
    <title>Mojo::Redis Chat Example</title>
    <link rel="stylesheet" href="//fonts.googleapis.com/css?family=Roboto:300,300italic,700,700italic">
    <link rel="stylesheet" href="//cdn.rawgit.com/necolas/normalize.css/master/normalize.css">
    <link rel="stylesheet" href="//cdn.rawgit.com/milligram/milligram/master/dist/milligram.min.css">
    <style>
      body {
        margin: 3rem 1rem;
      }
      pre {
        padding: 0.2rem 0.5rem;
      }
      .wrapper {
        max-width: 35em;
        margin: 0 auto;
      }
    </style>
  </head>
  <body>
    <div class="wrapper">
      <h1>Mojo::Redis Chat Example</h1>
      <form>
        <label>
          <span>Message:</span>
          <input type="search" name="message" value="Some message" placeholder="Write a message" autocomplete="off" disabled>
        </label>
        <button class="button" disabled>Send message</button>
      </form>
      <h2>Messages</h2>
      <pre id="messages">Connecting...</pre>
    </div>
    %= javascript begin
      var formEl = document.getElementsByTagName("form")[0];
      var inputEl = formEl.message;
      var messagesEl = document.getElementById("messages");
      var ws = new WebSocket("<%= url_for('socket')->to_abs %>");
      var id = Math.round(Math.random() * 1000);

      var hms = function() {
        var d = new Date();
        return [d.getHours(), d.getMinutes(), d.getSeconds()].map(function(v) {
          return v < 10 ? "0" + v : v;
        }).join(":");
      };

      formEl.addEventListener("submit", function(e) {
        e.preventDefault();
        if (inputEl.value.length) ws.send(hms() + " <" + id + "> " + inputEl.value);
        inputEl.value = "";
      });

      ws.onopen = function(e) {
        inputEl.disabled = false;
        document.getElementsByTagName("button")[0].disabled = false;
        messagesEl.innerHTML = hms() + " &lt;server> Connected.";
      };

      ws.onmessage = function(e) {
        messagesEl.innerHTML = e.data.replace(/</g, "&lt;") + "<br>" + messagesEl.innerHTML;
      };
    % end
  </body>
</html>

examples/twitter.pl  view on Meta::CPAN

#!/usr/bin/env perl
use Mojolicious::Lite -signatures;

use lib 'lib';
use Mojo::Redis;

helper redis => sub { state $r = Mojo::Redis->new };

get '/' => sub ($c) {
  return $c->render('login') unless my $username = $c->session('username');
  return $c->redirect_to(profile => {username => $username});
  },
  'index';

get '/logout' => sub ($c) {
  delete $c->session->{$_} for qw(uid username);
  $c->redirect_to('index');
};

get '/:username', sub ($c) {
  my $db        = $c->redis->db;
  my $username  = $c->stash('username');
  my $logged_in = my $uid;

  $c->stash(logged_in => $username eq $c->session('username') // '');
  $c->render_later;
  $db->hget_p('twitter_clone:users' => $username)->then(sub {
    $uid = shift or die $c->reply->not_found;
  })->then(sub {
    my $page           = $c->param('page') || 1;
    my $items_per_page = 20;
    my $start          = ($page - 1) * $items_per_page;
    $db->lrange_p("twitter_clone:posts:$uid", $start, $start + $items_per_page);
  })->then(sub {
    my $post_ids = shift;
    Mojo::Promise->all(map { $db->hgetall_p("twitter_clone:post:$_") } @$post_ids);
  })->then(sub {
    $c->render(posts => [map { $_->[0] } @_]);
  })->catch(sub {
    $c->reply->exception(shift) unless $c->stash('status');
  });
}, 'profile';

post '/:username/add-post', sub ($c) {
  my $v = $c->validation;
  my $uid = $c->session('uid') or return $c->redirect_to('index');

  $v->required('message');
  return $c->render('profile', status => 400, posts => [], error => 'Missing input.', logged_in => 1)
    unless $v->is_valid;

  $c->render_later;
  my $db = $c->redis->db;
  my $post_id;
  $db->incr_p('twitter_clone:next_post_id')->then(sub {
    $post_id = shift;
    $db->hmset_p("twitter_clone:post:$post_id", uid => $uid, time => time, body => $v->param('message'));
  })->then(sub {
    Mojo::Promise->all(
      $db->lpush_p("twitter_clone:posts:$uid", $post_id),
      $db->lpush_p("twitter_clone:timeline",   $post_id),
      $db->ltrim_p("twitter_clone:timeline", 0, 1000),
    );
  })->then(sub {
    $c->redirect_to('profile');
  })->catch(sub {
    $c->reply->exception(shift) unless $c->stash('status');
  });
}, 'add_post';

post '/login', sub ($c) {
  my $v = $c->validation;

  $v->csrf_protect;
  $v->required('password');
  $v->required('username');
  return $c->render(status => 400, error => 'Missing input.') unless $v->is_valid;

  $c->render_later;
  my $db = $c->redis->db;
  $db->hget_p('twitter_clone:users' => $v->param('username'))->then(sub {
    my $uid = shift;
    die $c->render(status => 400, error => 'Invalid username or password.') unless $uid;
    $c->session(uid => $uid, username => $v->param('username'));
    return $db->hget_p("twitter_clone:user:$uid", 'password');
  })->then(sub {
    my $password = shift;
    die $c->render(status => 400, error => 'Invalid username or password.')
      if !$password
      or $password ne $v->param('password');
    $c->redirect_to(profile => {username => $v->param('username')});
  })->catch(sub {
    $c->reply->exception(shift) unless $c->stash('status');
  });
}, 'login';

Mojo::IOLoop->next_tick(\&add_dummy_user);
app->defaults(layout => 'default');
app->secrets([$ENV{MOJO_TWITTER_CLONE_SECRET} || rand(1000)]);
app->start;

sub add_dummy_user {
  my $db = app->redis->db;
  my $uid;

  $db->hget_p('twitter_clone:users' => 'batgirl')->then(sub {
    die "--> User batgirl already added.\n" if $uid = shift;
  })->then(sub {
    $db->incr_p('twitter_clone:next_user_id');
  })->then(sub {
    $uid = shift;

    # Password should not be in plain text!
    Mojo::Promise->all(
      $db->hmset_p("twitter_clone:user:$uid", username => 'batgirl', password => 's3cret'),
      $db->hset_p('twitter_clone:users', batgirl => $uid),
    );
  })->then(sub {
    warn "--> User batgirl added.\n";
  })->catch(sub {
    warn $_[0];
  });
}

__DATA__
@@ login.html.ep
<h1>Login</h1>
<p>A dummy user has been added, so no need to change the form inputs.</p>
%= form_for 'login', begin
  <label>
    <span>Username</span>
     %= text_field 'username', 'batgirl'
  </label>
  <label>
    <span>Password</span>
     %= password_field 'password', value => 's3cret'
  </label>
  % if (my $error = stash 'error') {
    <p class="alert"><%= $error %></p>
  % }
  <button class="button">Login</button>
% end
@@ profile.html.ep
<h1><%= $username %></h1>
% if ($logged_in) {
  %= form_for 'add_post', begin
    <label>
      <span>Message</span>
      %= text_field 'message', placeholder => "What's on your mind?"
    </label>
    % if (my $error = stash 'error') {
      <p class="alert"><%= $error %></p>
    % }
    <button class="button">Post</button>
  % end
% }
<ul class="posts">
  % for my $post (@$posts) {
    <li>
      <small class="posts_time"><%= scalar localtime $post->{time} %></small>
      <div class="posts_body"><%= $post->{body} %></div>
    </li>
  % }
</ul>
@@ layouts/default.html.ep
<!DOCTYPE html>
<html>
  <head>
    <title>Design and implementation of a simple Twitter clone using Perl and the Redis key-value store</title>
    <link rel="stylesheet" href="//fonts.googleapis.com/css?family=Roboto:300,300italic,700,700italic">
    <link rel="stylesheet" href="//cdn.rawgit.com/necolas/normalize.css/master/normalize.css">
    <link rel="stylesheet" href="//cdn.rawgit.com/milligram/milligram/master/dist/milligram.min.css">
    <style>
      body {
        margin: 3rem 1rem;
      }
      pre {
        padding: 0.2rem 0.5rem;
      }
      .wrapper {
        max-width: 35em;
        margin: 0 auto;
      }
      .posts {
        list-style: none;
      }
      .posts li {
        border-bottom: 1px solid #bbb;
        margin-bottom: 2rem;
      }
    </style>
  </head>
  <body>
    <div class="wrapper"><%= content %></div>
  </body>
</html>

lib/Mojo/Redis.pm  view on Meta::CPAN

package Mojo::Redis;
use Mojo::Base 'Mojo::EventEmitter';

use Mojo::URL;
use Mojo::Redis::Connection;
use Mojo::Redis::Cache;
use Mojo::Redis::Cursor;
use Mojo::Redis::Database;
use Mojo::Redis::PubSub;
use Scalar::Util 'blessed';

our $VERSION = '3.29';

$ENV{MOJO_REDIS_URL} ||= 'redis://localhost:6379';

has encoding        => 'UTF-8';
has max_connections => 5;

has protocol_class => do {
  my $class = $ENV{MOJO_REDIS_PROTOCOL};
  $class ||= eval { require Protocol::Redis::XS; Protocol::Redis::XS->VERSION('0.06'); 'Protocol::Redis::XS' };
  $class ||= 'Protocol::Redis::Faster';
  eval "require $class; 1" or die $@;
  $class;
};

has pubsub => sub {
  my $self   = shift;
  my $pubsub = Mojo::Redis::PubSub->new(redis => $self);
  Scalar::Util::weaken($pubsub->{redis});
  return $pubsub;
};

has url => sub { Mojo::URL->new($ENV{MOJO_REDIS_URL}) };

sub cache  { Mojo::Redis::Cache->new(redis => shift, @_) }
sub cursor { Mojo::Redis::Cursor->new(redis => shift, command => [@_ ? @_ : (scan => 0)]) }
sub db     { Mojo::Redis::Database->new(redis => shift) }

sub new {
  my $class = shift;
  return $class->SUPER::new(@_) unless @_ % 2 and ref $_[0] ne 'HASH';
  my $url = shift;
  $url = Mojo::URL->new($url) unless blessed $url and $url->isa('Mojo::URL');
  return $class->SUPER::new(url => $url, @_);
}

sub _connection {
  my ($self, %args) = @_;

  $args{ioloop} ||= Mojo::IOLoop->singleton;
  my $conn = Mojo::Redis::Connection->new(
    encoding => $self->encoding,
    protocol => $self->protocol_class->new(api => 1),
    url      => $self->url->clone,
    %args
  );

  Scalar::Util::weaken($self);
  $conn->on(connect => sub { $self->emit(connection => $_[0]) });
  $conn;
}

sub _blocking_connection {
  my $self = shift->_fork_safety;

  # Existing connection
  my $conn = $self->{blocking_connection};
  return $conn->encoding($self->encoding) if $conn and $conn->is_connected;

  # New connection
  return $self->{blocking_connection} = $self->_connection(ioloop => $conn ? $conn->ioloop : Mojo::IOLoop->new);
}

sub _dequeue {
  my $self = shift->_fork_safety;

  # Exsting connection
  while (my $conn = shift @{$self->{queue} || []}) { return $conn->encoding($self->encoding) if $conn->is_connected }

  # New connection
  return $self->_connection;
}

sub _enqueue {
  my ($self, $conn) = @_;
  my $queue = $self->{queue} ||= [];
  push @$queue, $conn if $conn->is_connected and $conn->url eq $self->url and $conn->ioloop eq Mojo::IOLoop->singleton;
  shift @$queue while @$queue > $self->max_connections;
}

sub _fork_safety {
  my $self = shift;
  delete @$self{qw(blocking_connection pid queue)} unless ($self->{pid} //= $$) eq $$;    # Fork-safety
  $self;
}

1;

=encoding utf8

=head1 NAME

Mojo::Redis - Redis driver based on Mojo::IOLoop

=head1 SYNOPSIS

=head2 Blocking

  use Mojo::Redis;
  my $redis = Mojo::Redis->new;
  $redis->db->set(foo => 42);
  $redis->db->expire(foo => 600);
  warn $redis->db->get('foo');

=head2 Promises

  $redis->db->get_p("mykey")->then(sub {
    print "mykey=$_[0]\n";
  })->catch(sub {
    warn "Could not fetch mykey: $_[0]";
  })->wait;

=head2 Pipelining

Pipelining is built into the API by sending a lot of commands and then use
L<Mojo::Promise/all> to wait for all the responses.

  Mojo::Promise->all(
    $db->set_p($key, 10),
    $db->incrby_p($key, 9),
    $db->incr_p($key),
    $db->get_p($key),
    $db->incr_p($key),
    $db->get_p($key),
  )->then(sub {
    @res = map {@$_} @_;
  })->wait;

=head1 DESCRIPTION

L<Mojo::Redis> is a Redis driver that use the L<Mojo::IOLoop>, which makes it
integrate easily with the L<Mojolicious> framework.

It tries to mimic the same interface as L<Mojo::Pg>, L<Mojo::mysql> and
L<Mojo::SQLite>, but the methods for talking to the database vary.

This module is in no way compatible with the 1.xx version of C<Mojo::Redis>
and this version also tries to fix a lot of the confusing methods in
C<Mojo::Redis2> related to pubsub.

This module is currently EXPERIMENTAL, and bad design decisions will be fixed
without warning. Please report at
L<https://github.com/jhthorsen/mojo-redis/issues> if you find this module
useful, annoying or if you simply find bugs. Feedback can also be sent to
C<jhthorsen@cpan.org>.

=head1 EVENTS

=head2 connection

  $cb = $redis->on(connection => sub { my ($redis, $connection) = @_; });

Emitted when L<Mojo::Redis::Connection> connects to the Redis.

=head1 ATTRIBUTES

=head2 encoding

  $str   = $redis->encoding;
  $redis = $redis->encoding("UTF-8");

The value of this attribute will be passed on to
L<Mojo::Redis::Connection/encoding> when a new connection is created. This
means that updating this attribute will not change any connection that is
in use.

Default value is "UTF-8".

=head2 max_connections

  $int   = $redis->max_connections;
  $redis = $redis->max_connections(5);

Maximum number of idle database handles to cache for future use, defaults to
5. (Default is subject to change)

=head2 protocol_class

  $str   = $redis->protocol_class;
  $redis = $redis->protocol_class("Protocol::Redis::XS");

Default to L<Protocol::Redis::XS> if the optional module is available and at
least version 0.06, or falls back to L<Protocol::Redis::Faster>.

=head2 pubsub

  $pubsub = $redis->pubsub;

Lazy builds an instance of L<Mojo::Redis::PubSub> for this object, instead of
returning a new instance like L</db> does.

=head2 url

  $url   = $redis->url;
  $redis = $redis->url(Mojo::URL->new("redis://localhost/3"));

Holds an instance of L<Mojo::URL> that describes how to connect to the Redis server.

=head1 METHODS

=head2 db

  $db = $redis->db;

Returns an instance of L<Mojo::Redis::Database>.

=head2 cache

  $cache = $redis->cache(%attrs);

Returns an instance of L<Mojo::Redis::Cache>.

=head2 cursor

  $cursor = $redis->cursor(@command);

Returns an instance of L<Mojo::Redis::Cursor> with
L<Mojo::Redis::Cursor/command> set to the arguments passed. See
L<Mojo::Redis::Cursor/new>. for possible commands.

=head2 new

  $redis = Mojo::Redis->new("redis://localhost:6379/1");
  $redis = Mojo::Redis->new(Mojo::URL->new->host("/tmp/redis.sock"));
  $redis = Mojo::Redis->new(\%attrs);
  $redis = Mojo::Redis->new(%attrs);

Object constructor. Can coerce a string into a L<Mojo::URL> and set L</url>
if present.

=head1 AUTHORS

Jan Henning Thorsen - C<jhthorsen@cpan.org>

Dan Book - C<grinnz@grinnz.com>

=head1 COPYRIGHT AND LICENSE

Copyright (C) 2018, Jan Henning Thorsen.

This program is free software, you can redistribute it and/or modify it under
the terms of the Artistic License version 2.0.

=cut

lib/Mojo/Redis/Cache.pm  view on Meta::CPAN

package Mojo::Redis::Cache;
use Mojo::Base -base;

use Mojo::JSON;
use Scalar::Util 'blessed';
use Storable    ();
use Time::HiRes ();

use constant OFFLINE => $ENV{MOJO_REDIS_CACHE_OFFLINE};

has connection => sub {
  OFFLINE ? shift->_offline_connection : shift->redis->_dequeue->encoding(undef);
};
has deserialize    => sub { \&Storable::thaw };
has default_expire => 600;
has namespace      => 'cache:mojo:redis';
has refresh        => 0;
has redis          => sub { Carp::confess('redis is required in constructor') };
has serialize      => sub { \&Storable::freeze };

sub compute_p {
  my $compute = pop;
  my $self    = shift;
  my $key     = join ':', $self->namespace, shift;
  my $expire  = shift || $self->default_expire;

  my $p = $self->refresh ? Mojo::Promise->new->resolve : $self->connection->write_p(GET => $key);
  return $p->then(sub {
    my $data = $_[0] ? $self->deserialize->(shift) : undef;
    return $self->_maybe_compute_p($key, $expire, $compute, $data) if $expire < 0;
    return $self->_compute_p($key, $expire, $compute) unless $data;
    return $data->[0];
  });
}

sub memoize_p {
  my ($self, $obj, $method) = (shift, shift, shift);
  my $args = ref $_[0] eq 'ARRAY' ? shift : [];
  my $expire = shift || $self->default_expire;
  my $key = join ':', '@M' => (ref($obj) || $obj), $method, Mojo::JSON::encode_json($args);

  return $self->compute_p($key, $expire, sub { $obj->$method(@$args) });
}

sub _compute_p {
  my ($self, $key, $expire, $compute) = @_;

  my $set = sub {
    my $data = shift;
    my @set
      = $expire < 0
      ? $self->serialize->([$data, _time() + -$expire])
      : ($self->serialize->([$data]), PX => 1000 * $expire);
    $self->connection->write_p(SET => $key => @set)->then(sub {$data});
  };

  my $data = $compute->();
  return (blessed $data and $data->can('then')) ? $data->then(sub { $set->(@_) }) : $set->($data);
}

sub _maybe_compute_p {
  my ($self, $key, $expire, $compute, $data) = @_;

  # Nothing in cache
  return $self->_compute_p($key => $expire, $compute)->then(sub { ($_[0], {computed => 1}) }) unless $data;

  # No need to refresh cache
  return ($data->[0], {expired => 0}) if $data->[1] and _time() < $data->[1];

  # Try to refresh, but use old data on error
  my $p = Mojo::Promise->new;
  eval {
    $self->_compute_p($key => $expire, $compute)->then(
      sub { $p->resolve(shift,      {computed => 1,     expired => 1}) },
      sub { $p->resolve($data->[0], {error    => $_[0], expired => 1}) },
    );
  } or do {
    $p->resolve($data->[0], {error => $@, expired => 1});
  };

  return $p;
}

sub _offline_connection {
  state $c = eval <<'HERE' or die $@;
package Mojo::Redis::Connection::Offline;
use Mojo::Base 'Mojo::Redis::Connection';
our $STORE = {}; # Meant for internal use only

sub write_p {
  my ($conn, $op, $key) = (shift, shift, shift);

  if ($op eq 'SET') {
    $STORE->{$conn->url}{$key} = [$_[0], defined $_[2] ? $_[2] + Mojo::Redis::Cache::_time() * 1000 : undef];
    return Mojo::Promise->new->resolve('OK');
  }
  else {
    my $val     = $STORE->{$conn->url}{$key} || [];
    my $expired = $val->[1] && $val->[1] < Mojo::Redis::Cache::_time() * 1000;
    delete $STORE->{$conn->url}{$key} if $expired;
    return Mojo::Promise->new->resolve($expired ? undef : $val->[0]);
  }
}

'Mojo::Redis::Connection::Offline';
HERE
  my $redis = shift->redis;
  return $c->new(url => $redis->url);
}

sub _time { Time::HiRes::time() }

1;

=encoding utf8

=head1 NAME

Mojo::Redis::Cache - Simple cache interface using Redis

=head1 SYNOPSIS

  use Mojo::Redis;

  my $redis = Mojo::Redis->new;
  my $cache = $redis->cache;

  # Cache and expire the data after 60.7 seconds
  $cache->compute_p("some:key", 60.7, sub {
    my $p = Mojo::Promise->new;
    Mojo::IOLoop->timer(0.1 => sub { $p->resolve("some data") });
    return $p;
  })->then(sub {
    my $some_key = shift;
  });

  # Cache and expire the data after default_expire() seconds
  $cache->compute_p("some:key", sub {
    return {some => "data"};
  })->then(sub {
    my $some_key = shift;
  });

  # Call $obj->get_some_slow_data() and cache the return value
  $cache->memoize_p($obj, "get_some_slow_data")->then(sub {
    my $data = shift;
  });

  # Call $obj->get_some_data_by_id({id => 42}) and cache the return value
  $cache->memoize_p($obj, "get_some_data_by_id", [{id => 42}])->then(sub {
    my $data = shift;
  });

See L<https://github.com/jhthorsen/mojo-redis/blob/master/examples/cache.pl>
for example L<Mojolicious> application.

=head1 DESCRIPTION

L<Mojo::Redis::Cache> provides a simple interface for caching data in the
Redis database. There is no "check if exists", "get" or "set" methods in this
class. Instead, both L</compute_p> and L</memoize_p> will fetch the value
from Redis, if the given compute function / method has been called once, and
the cached data is not expired.

If you need to check if the value exists, then you can manually look up the
the key using L<Mojo::Redis::Database/exists>.

=head1 ENVIRONMENT VARIABLES

=head2 MOJO_REDIS_CACHE_OFFLINE

Set C<MOJO_REDIS_CACHE_OFFLINE> to 1 if you want to use this cache without a
real Redis backend. This can be useful in unit tests.

=head1 ATTRIBUTES

=head2 connection

  $conn  = $cache->connection;
  $cache = $cache->connection(Mojo::Redis::Connection->new);

Holds a L<Mojo::Redis::Connection> object.

=head2 default_expire

  $num  = $cache->default_expire;
  $cache = $cache->default_expire(600);

Holds the default expire time for cached data.

=head2 deserialize

  $cb   = $cache->deserialize;
  $cache = $cache->deserialize(\&Mojo::JSON::decode_json);

Holds a callback used to deserialize data from Redis.

=head2 namespace

  $str  = $cache->namespace;
  $cache = $cache->namespace("cache:mojo:redis");

Prefix for the cache key.

=head2 redis

  $conn = $cache->redis;
  $cache = $cache->redis(Mojo::Redis->new);

Holds a L<Mojo::Redis> object used to create the connection to talk with Redis.

=head2 refresh

  $bool = $cache->refresh;
  $cache = $cache->refresh(1);

Will force the cache to be computed again if set to a true value.

=head2 serialize

  $cb   = $cache->serialize;
  $cache = $cache->serialize(\&Mojo::JSON::encode_json);

Holds a callback used to serialize before storing the data in Redis.

=head1 METHODS

=head2 compute_p

  $promise = $cache->compute_p($key => $expire => $compute_function);
  $promise = $cache->compute_p($key => $expire => sub { return "data" });
  $promise = $cache->compute_p($key => $expire => sub { return Mojo::Promise->new });

This method will store the return value from the C<$compute_function> the
first time it is called and pass the same value to L<Mojo::Promise/then>.
C<$compute_function> will not be called the next time, if the C<$key> is
still present in Redis, but instead the cached value will be passed on to
L<Mojo::Promise/then>.

C<$key> will be prefixed by L</namespace> resulting in "namespace:some-key".

C<$expire> is the number of seconds before the cache should expire, and will
default to L</default_expire> unless passed in. The last argument is a
callback used to calculate cached value.

C<$expire> can also be a negative number. This will result in serving old cache
in the case where the C<$compute_function> fails. An example usecase would be
if you are fetching Twitter updates for your website, but instead of throwing
an exception if Twitter is down, you will serve old data instead. Note that the
fulfilled promise will get two variables passed in:

  $promise->then(sub { my ($data, $info) = @_ });

C<$info> is a hash and can have these keys:

=over 2

=item * computed

Will be true if the C<$compute_function> was called successfully and C<$data>
is fresh.

=item * expired

Will be true if C<$data> is expired. If this key is present and false, it will
indicate that the C<$data> is within the expiration period. The C<expired> key
can be found together with both L</computed> and L</error>.

=item * error

Will hold a string if the C<$compute_function> failed.

=back

Negative C<$expire> is currently EXPERIMENTAL, but unlikely to go away.

=head2 memoize_p

  $promise = $cache->memoize_p($obj, $method_name, \@args, $expire);
  $promise = $cache->memoize_p($class, $method_name, \@args, $expire);

L</memoize_p> behaves the same way as L</compute_p>, but has a convenient
interface for calling methods on an object. One of the benefits is that you
do not have to come up with your own cache key. This method is pretty much
the same as:

  $promise = $cache->compute_p(
    join(":", $cache->namespace, "@M", ref($obj), $method_name, serialize(\@args)),
    $expire,
    sub { return $obj->$method_name(@args) }
  );

See L</compute_p> regarding C<$expire>.

=head1 SEE ALSO

L<Mojo::Redis>

=cut

lib/Mojo/Redis/Connection.pm  view on Meta::CPAN

package Mojo::Redis::Connection;
use Mojo::Base 'Mojo::EventEmitter';

use File::Spec::Functions 'file_name_is_absolute';
use Mojo::IOLoop;
use Mojo::Promise;

use constant DEBUG                     => $ENV{MOJO_REDIS_DEBUG};
use constant CONNECT_TIMEOUT           => $ENV{MOJO_REDIS_CONNECT_TIMEOUT}           || 10;
use constant SENTINELS_CONNECT_TIMEOUT => $ENV{MOJO_REDIS_SENTINELS_CONNECT_TIMEOUT} || CONNECT_TIMEOUT;

has encoding => sub { Carp::confess('encoding is required in constructor') };
has ioloop   => sub { Carp::confess('ioloop is required in constructor') };
has protocol => sub { Carp::confess('protocol is required in constructor') };
has url      => sub { Carp::confess('url is required in constructor') };

sub DESTROY {
  my $self = shift;
  $self->disconnect if defined $self->{pid} and $self->{pid} == $$;
}

sub disconnect {
  my $self = shift;
  $self->_reject_queue;
  $self->{stream}->close if $self->{stream};
  $self->{gone_away} = 1;
  return $self;
}

sub is_connected { $_[0]->{stream} && !$_[0]->{gone_away} ? 1 : 0 }

sub write {
  my $self = shift;
  push @{$self->{write}}, [$self->_encode(@_)];
  $self->is_connected ? $self->_write : $self->_connect;
  return $self;
}

sub write_p {
  my $self = shift;
  my $p    = Mojo::Promise->new->ioloop($self->ioloop);
  push @{$self->{write}}, [$self->_encode(@_), $p];
  $self->is_connected ? $self->_write : $self->_connect;
  return $p;
}

sub _connect {
  my $self = shift;
  return $self if $self->{id};    # Connecting

  # Cannot reuse a connection because of transaction state and other state
  return $self->_reject_queue('Redis server has gone away') if $self->{gone_away};

  my $url = $self->{master_url} || $self->url;
  return $self->_discover_master if !$self->{master_url} and $url->query->param('sentinel');

  Scalar::Util::weaken($self);
  delete $self->{master_url};     # Make sure we forget master_url so we can reconnect
  $self->protocol->on_message($self->_parse_message_cb);
  $self->{id} = $self->ioloop->client(
    $self->_connect_args($url, {port => 6379, timeout => CONNECT_TIMEOUT}),
    sub {
      return unless $self;
      my ($loop, $err, $stream) = @_;
      my $close_cb = $self->_on_close_cb;
      return $self->$close_cb($err) if $err;

      $stream->timeout(0);
      $stream->on(close => $close_cb);
      $stream->on(error => $close_cb);
      $stream->on(read  => $self->_on_read_cb);

      unshift @{$self->{write}}, [$self->_encode(SELECT => $url->path->[0])] if length $url->path->[0];
      unshift @{$self->{write}}, [$self->_encode(AUTH   => $url->password)]  if length $url->password;
      $self->{pid}    = $$;
      $self->{stream} = $stream;
      $self->emit('connect');
      $self->_write;
    }
  );

  warn "[@{[$self->_id]}] CONNECTING $url (blocking=@{[$self->_is_blocking]})\n" if DEBUG;
  return $self;
}

sub _connect_args {
  my ($self, $url, $defaults) = @_;
  my %args = (address => $url->host || 'localhost');

  if (file_name_is_absolute $args{address}) {
    $args{path} = delete $args{address};
  }
  else {
    $args{port} = $url->port || $defaults->{port};
  }

  $args{timeout} = $defaults->{timeout} || CONNECT_TIMEOUT;
  return \%args;
}

sub _discover_master {
  my $self      = shift;
  my $url       = $self->url->clone;
  my $sentinels = $url->query->every_param('sentinel');
  my $timeout   = $url->query->param('sentinel_connect_timeout') || SENTINELS_CONNECT_TIMEOUT;

  $url->host_port(shift @$sentinels);
  $self->url->query->param(sentinel => [@$sentinels, $url->host_port]);    # Round-robin sentinel list
  $self->protocol->on_message($self->_parse_message_cb);
  $self->{id} = $self->ioloop->client(
    $self->_connect_args($url, {port => 16379, timeout => $timeout}),
    sub {
      my ($loop, $err, $stream) = @_;
      return unless $self;
      return $self->_discover_master if $err;

      $stream->timeout(0);
      $stream->on(close => sub { $self->_discover_master unless $self->{master_url} });
      $stream->on(error => sub { $self->_discover_master });
      $stream->on(read  => $self->_on_read_cb);

      $self->{stream} = $stream;
      my $p = Mojo::Promise->new;
      unshift @{$self->{write}}, undef;    # prevent _write() from writing commands
      unshift @{$self->{write}}, [$self->_encode(SENTINEL => 'get-master-addr-by-name', $self->url->host), $p];
      unshift @{$self->{write}}, [$self->_encode(AUTH     => $url->password)] if length $url->password;

      $self->{write_lock} = 1;
      $p->then(
        sub {
          my $host_port = shift;
          delete $self->{id};
          delete $self->{write_lock};
          return $self->_discover_master unless ref $host_port and @$host_port == 2;
          $self->{master_url} = $self->url->clone->host($host_port->[0])->port($host_port->[1]);
          $self->{stream}->close;
          $self->_connect;
        },
        sub { $self->_discover_master },
      );

      $self->_write;
    }
  );

  warn "[@{[$self->_id]}] SENTINEL DISCOVERY $url (blocking=@{[$self->_is_blocking]})\n" if DEBUG;
  return $self;
}

sub _encode {
  my $self     = shift;
  my $encoding = $self->encoding;
  return $self->protocol->encode({
    type => '*', data => [map { +{type => '$', data => $encoding ? Mojo::Util::encode($encoding, $_) : $_} } @_]
  });
}

sub _id { $_[0]->{id} || '0' }

sub _is_blocking { shift->ioloop eq Mojo::IOLoop->singleton ? 0 : 1 }

sub _on_close_cb {
  my $self = shift;

  Scalar::Util::weaken($self);
  return sub {
    return unless $self;
    my ($stream, $err) = @_;
    delete $self->{$_} for qw(id stream);
    $self->{gone_away} = 1;
    $self->_reject_queue($err);
    $self->emit('close')                                             if @_ == 1;
    warn qq([@{[$self->_id]}] @{[$err ? "ERROR $err" : "CLOSED"]}\n) if $self and DEBUG;
  };
}

sub _on_read_cb {
  my $self = shift;

  Scalar::Util::weaken($self);
  return sub {
    return unless $self;
    my ($stream, $chunk) = @_;
    do { local $_ = $chunk; s!\r\n!\\r\\n!g; warn "[@{[$self->_id]}] >>> ($_)\n" } if DEBUG;
    $self->protocol->parse($chunk);
  };
}

sub _parse_message_cb {
  my $self = shift;

  Scalar::Util::weaken($self);
  return sub {
    my ($protocol, @messages) = @_;
    my $encoding = $self->encoding;
    $self->_write unless $self->{write_lock};

    my $unpack = sub {
      my @res;

      while (my $m = shift @_) {
        if ($m->{type} eq '-') {
          return $m->{data}, undef;
        }
        elsif ($m->{type} eq ':') {
          push @res, 0 + $m->{data};
        }
        elsif ($m->{type} eq '*' and ref $m->{data} eq 'ARRAY') {
          my ($err, $res) = __SUB__->(@{$m->{data}});
          return $err if defined $err;
          push @res, $res;
        }

        # Only bulk string replies can contain binary-safe encoded data
        elsif ($m->{type} eq '$' and $encoding and defined $m->{data}) {
          push @res, Mojo::Util::decode($encoding, $m->{data});
        }
        else {
          push @res, $m->{data};
        }
      }

      return undef, \@res;
    };

    my ($err, $res) = $unpack->(@messages);
    my $p = shift @{$self->{waiting} || []};
    return $p ? $p->reject($err)       : $self->emit(error    => $err) unless $res;
    return $p ? $p->resolve($res->[0]) : $self->emit(response => $res->[0]);
  };
}

sub _reject_queue {
  my ($self, $err) = @_;
  state $default = 'Premature connection close';
  for my $p (@{delete $self->{waiting} || []}) { $p      and $p->reject($err      || $default) }
  for my $i (@{delete $self->{write}   || []}) { $i->[1] and $i->[1]->reject($err || $default) }
  return $self;
}

sub _write {
  my $self = shift;

  while (my $op = shift @{$self->{write}}) {
    my $loop = $self->ioloop;
    do { local $_ = $op->[0]; s!\r\n!\\r\\n!g; warn "[@{[$self->_id]}] <<< ($_)\n" } if DEBUG;
    push @{$self->{waiting}}, $op->[1];
    $self->{stream}->write($op->[0]);
  }
}

1;

=encoding utf8

=head1 NAME

Mojo::Redis::Connection - Low level connection class for talking to Redis

=head1 SYNOPSIS

  use Mojo::Redis::Connection;

  my $conn = Mojo::Redis::Connection->new(
               ioloop   => Mojo::IOLoop->singleton,
               protocol => Protocol::Redis::Faster->new(api => 1),
               url      => Mojo::URL->new("redis://localhost"),
             );

  $conn->write_p("GET some_key")->then(sub { print "some_key=$_[0]" })->wait;

=head1 DESCRIPTION

L<Mojo::Redis::Connection> is a low level driver for writing and reading data
from a Redis server.

You probably want to use L<Mojo::Redis> instead of this class.

=head1 EVENTS

=head2 close

  $cb = $conn->on(close => sub { my ($conn) = @_; });

Emitted when the connection to the redis server gets closed.

=head2 connect

  $cb = $conn->on(connect => sub { my ($conn) = @_; });

Emitted right after a connection is established to the Redis server, but
after the AUTH and SELECT commands are queued.

=head2 error

  $cb = $conn->on(error => sub { my ($conn, $error) = @_; });

Emitted if there's a connection error or the Redis server emits an error, and
there's not a promise to handle the message.

=head2 response

  $cb = $conn->on(response => sub { my ($conn, $res) = @_; });

Emitted when receiving a message from the Redis server.

=head1 ATTRIBUTES

=head2 encoding

  $str  = $conn->encoding;
  $conn = $conn->encoding("UTF-8");

Holds the character encoding to use for data from/to Redis. Set to C<undef>
to disable encoding/decoding data. Without an encoding set, Redis expects and
returns bytes. See also L<Mojo::Redis/encoding>.

=head2 ioloop

  $loop = $conn->ioloop;
  $conn = $conn->ioloop(Mojo::IOLoop->new);

Holds an instance of L<Mojo::IOLoop>.

=head2 protocol

  $protocol = $conn->protocol;
  $conn     = $conn->protocol(Protocol::Redis::XS->new(api => 1));

Holds a protocol object, such as L<Protocol::Redis::Faster> that is used to
generate and parse Redis messages.

=head2 url

  $url  = $conn->url;
  $conn = $conn->url(Mojo::URL->new->host("/tmp/redis.sock")->path("/5"));
  $conn = $conn->url("redis://localhost:6379/1");

=head1 METHODS

=head2 disconnect

  $conn = $conn->disconnect;

Used to disconnect from the Redis server.

=head2 is_connected

  $bool = $conn->is_connected;

True if a connection to the Redis server is established.

=head2 write

  $conn = $conn->write(@command_and_args);

Used to write a message to the redis server. Calling this method should result
in either a L</error> or L</response> event.

This is useful in the a

=head2 write_p

  $promise = $conn->write_p(@command_and_args);

Will write a command to the Redis server and establish a connection if not
already connected and returns a L<Mojo::Promise>.

=head1 SEE ALSO

L<Mojo::Redis>

=cut

lib/Mojo/Redis/Cursor.pm  view on Meta::CPAN

package Mojo::Redis::Cursor;
use Mojo::Base 'Mojo::EventEmitter';

use Carp qw(confess croak);

has connection => sub { shift->redis->_dequeue };
sub command  { $_[0]->{command} }
sub finished { !!$_[0]->{finished} }
has redis => sub { confess 'redis is required in constructor' };

sub again {
  my $self = shift;
  $self->{finished} = 0;
  $self->command->[$self->{cursor_pos_in_command}] = 0;
  return $self;
}

sub all {
  my $cb   = ref $_[-1] eq 'CODE' ? pop : undef;
  my $self = shift->again;                                                   # Reset cursor
  my $conn = $cb ? $self->connection : $self->redis->_blocking_connection;
  my @res;

  # Blocking
  unless ($cb) {
    my $err;
    while (my $p = $self->_next_p($conn)) {
      $p->then(sub { push @res, @{$_[0] || []} })->catch(sub { $err = shift })->wait;
      croak $err if $err;
    }
    return $self->{process}->($self, \@res);
  }

  # Non-blocking
  $self->_next_p($conn)->then(sub {
    push @res, @{$_[0]};
    return $self->$cb('', $self->{process}->($self, \@res)) if $self->{finished};
    return $self->_next_p($conn)->then(__SUB__);
  })->catch(sub { $self->$cb($_[0], []) });

  return $self;
}

sub all_p {
  my $self = shift->again;        # Reset cursor
  my $conn = $self->connection;
  my @res;

  return $self->_next_p($conn)->then(sub {
    push @res, @{$_[0]};
    return $self->{process}->($self, \@res) if $self->{finished};
    return $self->_next_p($conn)->then(__SUB__);
  });
}

sub next {
  my $cb   = ref $_[-1] eq 'CODE' ? pop : undef;
  my $self = shift;

  # Cursor is exhausted
  return $cb ? $self->tap($cb, '', undef) : undef
    unless my $p = $self->_next_p($cb ? $self->connection : $self->redis->_blocking_connection);

  # Blocking
  unless ($cb) {
    my ($err, $res);
    $p->then(sub { $res = $self->{process}->($self, shift) })->catch(sub { $err = shift })->wait;
    croak $err if $err;
    return $res;
  }

  # Non-blocking
  $p->then(sub { $self->$cb('', $self->{process}->($self, shift)) })->catch(sub { $self->$cb(shift, undef) });
  return $self;
}

sub next_p {
  my $self = shift;
  return $self->_next_p($self->connection)->then(sub { $self->{process}->($self, shift) });
}

sub new {
  my $self = shift->SUPER::new(@_);
  my $cmd  = $self->command;

  $cmd->[0] ||= 'unknown';
  $self->{process} = __PACKAGE__->can(lc "_process_$cmd->[0]") or confess "Unknown cursor command: @$cmd";

  if ($cmd->[0] eq 'keys') {
    @$cmd = (scan => 0, $cmd->[1] ? (match => $cmd->[1]) : ());
  }
  elsif ($cmd->[0] eq 'smembers') {
    @$cmd = (sscan => $cmd->[1], 0);
  }
  elsif ($cmd->[0] =~ /^(hgetall|hkeys)/) {
    @$cmd = (hscan => $cmd->[1], 0);
  }

  $self->{cursor_pos_in_command} = $cmd->[0] =~ /^scan$/i ? 1 : 2;
  return $self;
}

sub _next_p {
  my ($self, $conn) = @_;
  return undef if $self->{finished};

  my $cmd = $self->command;
  return $conn->write_p(@$cmd)->then(sub {
    my $res = shift;
    $cmd->[$self->{cursor_pos_in_command}] = $res->[0] // 0;
    $self->{finished} = 1 unless $res->[0];
    return $res->[1];
  });
}

sub _process_hgetall  { +{@{$_[1]}} }
sub _process_hkeys    { my %h = @{$_[1]}; return [keys %h]; }
sub _process_hscan    { $_[1] }
sub _process_keys     { $_[1] }
sub _process_scan     { $_[1] }
sub _process_smembers { $_[1] }
sub _process_sscan    { $_[1] }
sub _process_zscan    { $_[1] }

sub DESTROY {
  my $self = shift;
  return unless (my $redis = $self->{redis}) && (my $conn = $self->{connection});
  $redis->_enqueue($conn);
}

1;

=encoding utf8

=head1 NAME

Mojo::Redis::Cursor - Iterate the results from SCAN, SSCAN, HSCAN and ZSCAN

=head1 SYNOPSIS

  use Mojo::Redis;
  my $redis  = Mojo::Redis->new;
  my $cursor = $redis->cursor(hkeys => 'redis:scan_test:hash');
  my $keys   = $cursor->all;

=head1 DESCRIPTION

L<Mojo::Redis::Cursor> provides methods for iterating over the result from
the Redis commands SCAN, SSCAN, HSCAN and ZSCAN.

See L<https://redis.io/commands/scan> for more information.

=head1 ATTRIBUTES

=head2 command

  $array_ref = $cursor->command;

The current command used to get data from Redis. This need to be set in the
constructor, but reading it out might not reflect the value put in. Examples:

  $r->new(command => [hgetall => "foo*"]);
  # $r->command == [hscan => "foo*", 0]

  $r->new(command => [SSCAN => "foo*"])
  # $r->command == [SSCAN => "foo*", 0]

Also, calling L</next> will change the value of L</command>. Example:

  $r->new(command => ["keys"]);
  # $r->command == [scan => 0]
  $r->next;
  # $r->command == [scan => 42]

=head2 connection

  $conn   = $cursor->connection;
  $cursor = $cursor->connection(Mojo::Redis::Connection->new);

Holds a L<Mojo::Redis::Connection> object.

=head2 finished

  $bool = $cursor->finished;

True after calling L</all> or if L</next> has iterated the whole list of members.

=head2 redis

  $conn   = $cursor->connection;
  $cursor = $cursor->connection(Mojo::Redis::Connection->new);

Holds a L<Mojo::Redis> object used to create the connections to talk with Redis.

=head1 METHODS

=head2 again

  $cursor->again;

Used to reset the cursor and make L</next> start over.

=head2 all

  $res    = $cursor->all;
  $cursor = $cursor->all(sub { my ($cursor, $res) = @_ });

Used to return all members. C<$res> is an array ref of strings, except when
using the command "hgetall".

=head2 all_p

  $promise = $cursor->all_p->then(sub { my $res = shift });

Same as L</all> but returns a L<Mojo::Promise>.

=head2 new

  $cursor = Mojo::Redis::Cursor->new(command => [...], redis => Mojo::Redis->new);

Used to construct a new object. L</command> and L</redis> is required as input.

Here are some examples of the differnet commands that are supported:

  # Custom cursor commands
  $cursor = $cursor->cursor(hscan => 0, match => '*', count => 100);
  $cursor = $cursor->cursor(scan  => 0, match => '*', count => 100);
  $cursor = $cursor->cursor(sscan => 0, match => '*', count => 100);
  $cursor = $cursor->cursor(zscan => 0, match => '*', count => 100);

  # Convenient cursor commands
  $cursor = $cursor->cursor(hgetall  => "some:hash:key");
  $cursor = $cursor->cursor(hkeys    => "some:hash:key");
  $cursor = $cursor->cursor(keys     => "some:key:pattern*");
  $cursor = $cursor->cursor(smembers => "some:set:key");

The convenient commands are alternatives to L<Mojo::Redis::Database/hgetall>,
L<Mojo::Redis::Database/hkeys>, L<Mojo::Redis::Database/keys> and
L<Mojo::Redis::Database/smembers>.

=head2 next

  $res    = $cursor->next;
  $cursor = $cursor->next(sub { my ($cursor, $err, $res) = @_ });

Used to return a chunk of members. C<$res> is an array ref of strings, except
when using the command "hgetall". C<$res> will also be C<undef()> when the
cursor is exhausted and L</finished> will be true.

=head2 next_p

  $promise = $cursor->next_p;

Same as L</next> but returns a L<Mojo::Prmoise>.

=head1 SEE ALSO

L<Mojo::Redis>.

=cut

lib/Mojo/Redis/Database.pm  view on Meta::CPAN

package Mojo::Redis::Database;
use Mojo::Base -base;

use Scalar::Util 'blessed';

our @BASIC_COMMANDS = (
  'append',           'bgrewriteaof', 'bgsave',            'bitcount',
  'bitfield',         'bitop',        'bitpos',            'client',
  'cluster',          'config',       'command',           'dbsize',
  'debug',            'decr',         'decrby',            'del',
  'dump',             'echo',         'eval',              'evalsha',
  'exists',           'expire',       'expireat',          'flushall',
  'flushdb',          'geoadd',       'geohash',           'geopos',
  'geodist',          'georadius',    'georadiusbymember', 'get',
  'getbit',           'getrange',     'getset',            'hdel',
  'hexists',          'hget',         'hgetall',           'hincrby',
  'hincrbyfloat',     'hkeys',        'hlen',              'hmget',
  'hmset',            'hset',         'hsetnx',            'hstrlen',
  'hvals',            'info',         'incr',              'incrby',
  'incrbyfloat',      'keys',         'lastsave',          'lindex',
  'linsert',          'llen',         'lpop',              'lpush',
  'lpushx',           'lrange',       'lrem',              'lset',
  'ltrim',            'memory',       'mget',              'move',
  'mset',             'msetnx',       'object',            'persist',
  'pexpire',          'pexpireat',    'pttl',              'pfadd',
  'pfcount',          'pfmerge',      'ping',              'psetex',
  'publish',          'randomkey',    'readonly',          'readwrite',
  'rename',           'renamenx',     'role',              'rpop',
  'rpoplpush',        'rpush',        'rpushx',            'restore',
  'sadd',             'save',         'scard',             'script',
  'sdiff',            'sdiffstore',   'set',               'setbit',
  'setex',            'setnx',        'setrange',          'sinter',
  'sinterstore',      'sismember',    'slaveof',           'slowlog',
  'smembers',         'smove',        'sort',              'spop',
  'srandmember',      'srem',         'strlen',            'sunion',
  'sunionstore',      'time',         'touch',             'ttl',
  'type',             'unlink',       'xadd',              'xrange',
  'xrevrange',        'xlen',         'xread',             'xreadgroup',
  'xpending',         'zadd',         'zcard',             'zcount',
  'zincrby',          'zinterstore',  'zlexcount',         'zpopmax',
  'zpopmin',          'zrange',       'zrangebylex',       'zrangebyscore',
  'zrank',            'zrem',         'zremrangebylex',    'zremrangebyrank',
  'zremrangebyscore', 'zrevrange',    'zrevrangebylex',    'zrevrangebyscore',
  'zrevrank',         'zscore',       'zunionstore',
);

our @BLOCKING_COMMANDS = ('blpop', 'brpop', 'brpoplpush', 'bzpopmax', 'bzpopmin');

has redis => sub { Carp::confess('redis is required in constructor') };

__PACKAGE__->_add_method('bnb,p' => $_)                    for @BASIC_COMMANDS;
__PACKAGE__->_add_method('nb,p'  => $_)                    for @BLOCKING_COMMANDS;
__PACKAGE__->_add_method('bnb'   => qw(_exec EXEC));
__PACKAGE__->_add_method('bnb'   => qw(_discard DISCARD));
__PACKAGE__->_add_method('bnb'   => qw(_multi MULTI));
__PACKAGE__->_add_method('bnb,p' => "${_}_structured", $_) for qw(info xread);
__PACKAGE__->_add_method('bnb,p' => $_)                    for qw(unwatch watch);

sub call {
  my $cb   = ref $_[-1] eq 'CODE' ? pop : undef;
  my $self = shift;
  my $p    = $self->connection($cb ? 0 : 1)->write_p(@_);

  # Non-blocking
  if ($cb) {
    $p->then(sub { $self->$cb('', @_) })->catch(sub { $self->$cb(shift, undef) });
    return $self;
  }

  # Blocking
  my ($err, @res);
  $p->then(sub { @res = @_ })->catch(sub { $err = shift })->wait;
  die $err if $err;
  return @res;
}

sub call_p {
  my $self = shift;
  return $self->connection->write_p(@_)->then(sub { $self = undef; @_ });
}

sub exec { delete $_[0]->{txn}; shift->_exec(@_) }

sub exec_p {
  my $self = shift;
  delete $self->{txn};
  return $self->connection->write_p('EXEC');
}

sub discard { delete $_[0]->{txn}; shift->_discard(@_) }

sub discard_p {
  my $self = shift;
  delete $self->{txn};
  return $self->connection->write_p('DISCARD');
}

sub multi {
  $_[0]->{txn} = ref $_[-1] eq 'CODE' ? 'default' : 'blocking';
  return shift->_multi(@_);
}

sub multi_p {
  my ($self, @p) = @_;
  Carp::croak('multi_p(@promises) syntax is not supported anymore. Use promise chaining instead.')
    if @p;
  $self->{txn} = 'default';
  return $self->connection->write_p('MULTI');
}

sub _add_method {
  my ($class, $types, $method, $op) = @_;
  my $caller  = caller;
  my $process = $caller->can(lc "_process_$method");

  $op ||= uc $method;

  for my $type (split /,/, $types) {
    Mojo::Util::monkey_patch(
      $caller,
      $type eq 'p' ? "${method}_p" : $method,
      $class->can("_generate_${type}_method")->($class, $op, $process)
    );
  }
}

sub connection {
  my $self = shift;

  # Back compat: $self->connection(Mojo::Redis::Connection->new);
  $self->{_conn_dequeue} = shift if blessed $_[0] and $_[0]->isa('Mojo::Redis::Connection');

  my $method = $_[0] ? '_blocking_connection' : '_dequeue';
  return $self->{"_conn$method"} ||= $self->redis->$method;
}

sub _generate_bnb_method {
  my ($class, $op, $process) = @_;

  return sub {
    my $cb   = ref $_[-1] eq 'CODE' ? pop : undef;
    my $self = shift;

    my $p = $self->connection($cb ? 0 : 1)->write_p($op, @_);
    $p = $p->then(sub { $self->$process(@_) }) if $process;

    # Non-blocking
    if ($cb) {
      $p->then(sub { $self->$cb('', @_) })->catch(sub { $self->$cb(shift, undef) });
      return $self;
    }

    # Blocking
    my ($err, $res);
    $p->then(sub { $res = shift })->catch(sub { $err = shift })->wait;
    die $err if defined $err;
    return $res;
  };
}

sub _generate_nb_method {
  my ($class, $op, $process) = @_;

  return sub {
    my ($self, $cb) = (shift, pop);
    $self->connection->write_p(@_)->then(sub { $self->$cb('', $process ? $self->$process(@_) : @_) })
      ->catch(sub { $self->$cb(shift, undef) });
    return $self;
  };
}

sub _generate_p_method {
  my ($class, $op, $process) = @_;

  return sub {
    my $self = shift;
    $self->connection->write_p($op => @_)->then(sub {
      return $process ? $self->$process(@_) : @_;
    });
  };
}

sub _process_geopos {
  ref $_[1] eq 'ARRAY' ? [map { ref $_ ? +{lng => $_->[0], lat => $_->[1]} : undef } @{$_[1]}] : $_[1];
}
sub _process_blpop   { ref $_[1] eq 'ARRAY' ? reverse @{$_[1]} : $_[1] }
sub _process_brpop   { ref $_[1] eq 'ARRAY' ? reverse @{$_[1]} : $_[1] }
sub _process_hgetall { ref $_[1] eq 'ARRAY' ? +{@{$_[1]}}      : $_[1] }

sub _process_info_structured {
  my $self    = shift;
  my $section = {};
  my %res;

  for (split /\r\n/, $_[0]) {
    if (/^\#\s+(\S+)/) {
      $section = $res{lc $1} = {};
    }
    elsif (/(\S+):(\S+)/) {
      $section->{$1} = $2;
    }
  }

  return keys %res == 1 ? $section : \%res;
}

sub _process_xread_structured {
  return $_[1] unless ref $_[1] eq 'ARRAY';
  return {map { ($_->[0] => $_->[1]) } @{$_[1]}};
}

sub DESTROY {
  my $self = shift;

  if (my $txn = delete $self->{txn}) {
    $self->connection(1)->write_p('DISCARD')->wait if $txn eq 'blocking';
  }
  elsif (my $redis = $self->{redis} and my $conn = $self->{_conn_dequeue}) {
    $redis->_enqueue($conn);
  }
}

1;

=encoding utf8

=head1 NAME

Mojo::Redis::Database - Execute basic redis commands

=head1 SYNOPSIS

  use Mojo::Redis;

  my $redis = Mojo::Redis->new;
  my $db    = $redis->db;

  # Blocking
  say "foo=" .$db->get("foo");

  # Non-blocking
  $db->get(foo => sub { my ($db, $res) = @_; say "foo=$res" });

  # Promises
  $db->get_p("foo")->then(sub { my ($res) = @_; say "foo=$res" });

See L<https://github.com/jhthorsen/mojo-redis/blob/master/examples/twitter.pl>
for example L<Mojolicious> application.

=head1 DESCRIPTION

L<Mojo::Redis::Database> has methods for sending and receiving structured
data to the Redis server.

=head1 ATTRIBUTES

=head2 redis

  $conn = $db->redis;
  $db = $db->redis(Mojo::Redis->new);

Holds a L<Mojo::Redis> object used to create the connections to talk with Redis.

=head1 METHODS

=head2 append

  $int     = $db->append($key, $value);
  $db      = $db->append($key, $value, sub { my ($db, $err, $int) = @_ });
  $promise = $db->append_p($key, $value);

Append a value to a key.

See L<https://redis.io/commands/append> for more information.

=head2 bgrewriteaof

  $ok      = $db->bgrewriteaof;
  $db      = $db->bgrewriteaof(sub { my ($db, $err, $ok) = @_ });
  $promise = $db->bgrewriteaof_p;

Asynchronously rewrite the append-only file.

See L<https://redis.io/commands/bgrewriteaof> for more information.

=head2 bgsave

  $ok      = $db->bgsave;
  $db      = $db->bgsave(sub { my ($db, $err, $ok) = @_ });
  $promise = $db->bgsave_p;

Asynchronously save the dataset to disk.

See L<https://redis.io/commands/bgsave> for more information.

=head2 bitcount

  $int     = $db->bitcount($key, [start end]);
  $db      = $db->bitcount($key, [start end], sub { my ($db, $err, $int) = @_ });
  $promise = $db->bitcount_p($key, [start end]);

Count set bits in a string.

See L<https://redis.io/commands/bitcount> for more information.

=head2 bitfield

  $res     = $db->bitfield($key, [GET type offset], [SET type offset value], [INCRBY type offset increment], [OVERFLOW WRAP|SAT|FAIL]);
  $db      = $db->bitfield($key, [GET type offset], [SET type offset value], [INCRBY type offset increment], [OVERFLOW WRAP|SAT|FAIL], sub { my ($db, $err, $res) = @_ });
  $promise = $db->bitfield_p($key, [GET type offset], [SET type offset value], [INCRBY typeoffset increment], [OVERFLOW WRAP|SAT|FAIL]);

Perform arbitrary bitfield integer operations on strings.

See L<https://redis.io/commands/bitfield> for more information.

=head2 bitop

  $int     = $db->bitop($operation, $destkey, $key [key ...]);
  $db      = $db->bitop($operation, $destkey, $key [key ...], sub { my ($db, $err, $int) = @_ });
  $promise = $db->bitop_p($operation, $destkey, $key [key ...]);

Perform bitwise operations between strings.

See L<https://redis.io/commands/bitop> for more information.

=head2 bitpos

  $int     = $db->bitpos($key, $bit, [start], [end]);
  $db      = $db->bitpos($key, $bit, [start], [end], sub { my ($db, $err, $int) = @_ });
  $promise = $db->bitpos_p($key, $bit, [start], [end]);

Find first bit set or clear in a string.

See L<https://redis.io/commands/bitpos> for more information.

=head2 blpop

  $db      = $db->blpop($key [key ...], $timeout, sub { my ($db, $val, $key) = @_ });
  $promise = $db->blpop_p($key [key ...], $timeout);

Remove and get the first element in a list, or block until one is available.

See L<https://redis.io/commands/blpop> for more information.

=head2 brpop

  $db      = $db->brpop($key [key ...], $timeout, sub { my ($db, $val, $key) = @_ });
  $promise = $db->brpop_p($key [key ...], $timeout);

Remove and get the last element in a list, or block until one is available.

See L<https://redis.io/commands/brpop> for more information.

=head2 brpoplpush

  $db      = $db->brpoplpush($source, $destination, $timeout, sub { my ($db, $err, $array_ref) = @_ });
  $promise = $db->brpoplpush_p($source, $destination, $timeout);

Pop a value from a list, push it to another list and return it; or block until one is available.

See L<https://redis.io/commands/brpoplpush> for more information.

=head2 bzpopmax

  $db      = $db->bzpopmax($key [key ...], $timeout, sub { my ($db, $err, $array_ref) = @_ });
  $promise = $db->bzpopmax_p($key [key ...], $timeout);

Remove and return the member with the highest score from one or more sorted sets, or block until one is available.

See L<https://redis.io/commands/bzpopmax> for more information.

=head2 bzpopmin

  $db      = $db->bzpopmin($key [key ...], $timeout, sub { my ($db, $err, $array_ref) = @_ });
  $promise = $db->bzpopmin_p($key [key ...], $timeout);

Remove and return the member with the lowest score from one or more sorted sets, or block until one is available.

See L<https://redis.io/commands/bzpopmin> for more information.

=head2 call

  $res = $db->call($command => @args);
  $db  = $db->call($command => @args, sub { my ($db, $err, $res) = @_; });

Same as L</call_p>, but either blocks or passes the result into a callback.

=head2 call_p

  $promise = $db->call_p($command => @args);
  $promise = $db->call_p(GET => "some:key");

Used to send a custom command to the Redis server.

=head2 client

  $res     = $db->client(@args);
  $db      = $db->client(@args, sub { my ($db, $err, $res) = @_ });
  $promise = $db->client_p(@args);

Run a "CLIENT" command on the server. C<@args> can be:

=over 2

=item * KILL [ip:port] [ID client-id] [TYPE normal|master|slave|pubsub] [ADDR ip:port] [SKIPME yes/no]

=item * LIST

=item * GETNAME

=item * PAUSE timeout

=item * REPLY [ON|OFF|SKIP]

=item * SETNAME connection-name

=back

See L<https://redis.io/commands#server> for more information.

=head2 connection

  $non_blocking_connection = $db->connection(0);
  $blocking_connection     = $db->connection(1);

Returns a L<Mojo::Redis::Connection> object. The default is to return a
connection suitable for non-blocking methods, but passing in a true value will
return the connection used for blocking methods.

  # Blocking
  my $res = $db->get("some:key");

  # Non-blocking
  $db->get_p("some:key");
  $db->get("some:key", sub { ... });

=head2 cluster

  $res     = $db->cluster(@args);
  $db      = $db->cluster(@args, sub { my ($db, $err, $res) = @_ });
  $promise = $db->cluster_p(@args);

Used to execute cluster commands.

See L<https://redis.io/commands#cluster> for more information.

=head2 command

  $array_ref = $db->command(@args);
  $db        = $db->command(@args, sub { my ($db, $err, $array_ref) = @_ });
  $promise   = $db->command_p(@args);

Get array of Redis command details.

=over 2

=item * empty list

=item * COUNT

=item * GETKEYS

=item * INFO command-name [command-name]

=back

See L<https://redis.io/commands/command> for more information.

=head2 dbsize

  $int     = $db->dbsize;
  $db      = $db->dbsize(sub { my ($db, $err, $int) = @_ });
  $promise = $db->dbsize_p;

Return the number of keys in the selected database.

See L<https://redis.io/commands/dbsize> for more information.

=head2 decr

  $num     = $db->decr($key);
  $db      = $db->decr($key, sub { my ($db, $err, $num) = @_ });
  $promise = $db->decr_p($key);

Decrement the integer value of a key by one.

See L<https://redis.io/commands/decr> for more information.

=head2 decrby

  $num     = $db->decrby($key, $decrement);
  $db      = $db->decrby($key, $decrement, sub { my ($db, $err, $num) = @_ });
  $promise = $db->decrby_p($key, $decrement);

Decrement the integer value of a key by the given number.

See L<https://redis.io/commands/decrby> for more information.

=head2 del

  $ok      = $db->del($key [key ...]);
  $db      = $db->del($key [key ...], sub { my ($db, $err, $ok) = @_ });
  $promise = $db->del_p($key [key ...]);

Delete a key.

See L<https://redis.io/commands/del> for more information.

=head2 discard

See L</discard_p>.

=head2 discard_p

  $ok      = $db->discard;
  $db      = $db->discard(sub { my ($db, $err, $ok) = @_ });
  $promise = $db->discard_p;

Discard all commands issued after MULTI.

See L<https://redis.io/commands/discard> for more information.

=head2 dump

  $ok      = $db->dump($key);
  $db      = $db->dump($key, sub { my ($db, $err, $ok) = @_ });
  $promise = $db->dump_p($key);

Return a serialized version of the value stored at the specified key.

See L<https://redis.io/commands/dump> for more information.

=head2 echo

  $res     = $db->echo($message);
  $db      = $db->echo($message, sub { my ($db, $err, $res) = @_ });
  $promise = $db->echo_p($message);

Echo the given string.

See L<https://redis.io/commands/echo> for more information.

=head2 eval

  $res     = $db->eval($script, $numkeys, $key [key ...], $arg [arg ...]);
  $db      = $db->eval($script, $numkeys, $key [key ...], $arg [arg ...], sub { my ($db, $err, $res) = @_ });
  $promise = $db->eval_p($script, $numkeys, $key [key ...], $arg [arg ...]);

Execute a Lua script server side.

See L<https://redis.io/commands/eval> for more information.

=head2 evalsha

  $res     = $db->evalsha($sha1, $numkeys, $key [key ...], $arg [arg ...]);
  $db      = $db->evalsha($sha1, $numkeys, $key [key ...], $arg [arg ...], sub { my ($db, $err, $res) = @_ });
  $promise = $db->evalsha_p($sha1, $numkeys, $key [key ...], $arg [arg ...]);

Execute a Lua script server side.

See L<https://redis.io/commands/evalsha> for more information.

=head2 exec

See L</exec_p>.

=head2 exec_p

  $array_ref = $db->exec;
  $db        = $db->exec(sub { my ($db, $err, $array_ref) = @_ });
  $promise   = $db->exec_p;

Execute all commands issued after L</multi>.

See L<https://redis.io/commands/exec> for more information.

=head2 exists

  $int     = $db->exists($key [key ...]);
  $db      = $db->exists($key [key ...], sub { my ($db, $err, $int) = @_ });
  $promise = $db->exists_p($key [key ...]);

Determine if a key exists.

See L<https://redis.io/commands/exists> for more information.

=head2 expire

  $int     = $db->expire($key, $seconds);
  $db      = $db->expire($key, $seconds, sub { my ($db, $err, $int) = @_ });
  $promise = $db->expire_p($key, $seconds);

Set a key's time to live in seconds.

See L<https://redis.io/commands/expire> for more information.

=head2 expireat

  $int     = $db->expireat($key, $timestamp);
  $db      = $db->expireat($key, $timestamp, sub { my ($db, $err, $int) = @_ });
  $promise = $db->expireat_p($key, $timestamp);

Set the expiration for a key as a UNIX timestamp.

See L<https://redis.io/commands/expireat> for more information.

=head2 flushall

  $str     = $db->flushall([ASYNC]);
  $db      = $db->flushall([ASYNC], sub { my ($db, $err, $str) = @_ });
  $promise = $db->flushall_p([ASYNC]);

Remove all keys from all databases.

See L<https://redis.io/commands/flushall> for more information.

=head2 flushdb

  $str     = $db->flushdb([ASYNC]);
  $db      = $db->flushdb([ASYNC], sub { my ($db, $err, $str) = @_ });
  $promise = $db->flushdb_p([ASYNC]);

Remove all keys from the current database.

See L<https://redis.io/commands/flushdb> for more information.

=head2 geoadd

  $res     = $db->geoadd($key, $longitude latitude member [longitude latitude member ...]);
  $db      = $db->geoadd($key, $longitude latitude member [longitude latitude member ...], sub { my ($db, $err, $res) = @_ });
  $promise = $db->geoadd_p($key, $longitude latitude member [longitude latitude member ...]);

Add one or more geospatial items in the geospatial index represented using a sorted set.

See L<https://redis.io/commands/geoadd> for more information.

=head2 geodist

  $res     = $db->geodist($key, $member1, $member2, [unit]);
  $db      = $db->geodist($key, $member1, $member2, [unit], sub { my ($db, $err, $res) = @_ });
  $promise = $db->geodist_p($key, $member1, $member2, [unit]);

Returns the distance between two members of a geospatial index.

See L<https://redis.io/commands/geodist> for more information.

=head2 geohash

  $res     = $db->geohash($key, $member [member ...]);
  $db      = $db->geohash($key, $member [member ...], sub { my ($db, $err, $res) = @_ });
  $promise = $db->geohash_p($key, $member [member ...]);

Returns members of a geospatial index as standard geohash strings.

See L<https://redis.io/commands/geohash> for more information.

=head2 geopos

  $array_ref = $db->geopos($key, $member [member ...]);
  $db        = $db->geopos($key, $member [member ...], sub { my ($db, $err, $array_ref) = @_ });
  $promise   = $db->geopos_p($key, $member [member ...]);

Returns longitude and latitude of members of a geospatial index:

  [{lat => $num, lng => $num}, ...]

See L<https://redis.io/commands/geopos> for more information.

=head2 georadius

  $res     = $db->georadius($key, $longitude, $latitude, $radius, $m|km|ft|mi, [WITHCOORD],[WITHDIST], [WITHHASH], [COUNT count], [ASC|DESC], [STORE key], [STOREDIST key]);
  $db      = $db->georadius($key, $longitude, $latitude, $radius, $m|km|ft|mi, [WITHCOORD],[WITHDIST], [WITHHASH], [COUNT count], [ASC|DESC], [STORE key], [STOREDIST key], sub { my ($db, $err, $res) = @_ });
  $promise = $db->georadius_p($key, $longitude, $latitude, $radius, $m|km|ft|mi, [WITHCOORD], [WITHDIST], [WITHHASH], [COUNT count], [ASC|DESC], [STORE key], [STOREDIST key]);

Query a sorted set representing a geospatial index to fetch members matching a given maximum distance from a point.

See L<https://redis.io/commands/georadius> for more information.

=head2 georadiusbymember

  $res     = $db->georadiusbymember($key, $member, $radius, $m|km|ft|mi, [WITHCOORD], [WITHDIST], [WITHHASH], [COUNT count], [ASC|DESC], [STORE key], [STOREDIST key]);
  $db      = $db->georadiusbymember($key, $member, $radius, $m|km|ft|mi, [WITHCOORD], [WITHDIST], [WITHHASH], [COUNT count], [ASC|DESC], [STORE key], [STOREDIST key], sub { my ($db, $err, $res) = @_ });
  $promise = $db->georadiusbymember_p($key, $member, $radius, $m|km|ft|mi, [WITHCOORD], [WITHDIST], [WITHHASH], [COUNT count], [ASC|DESC], [STORE key], [STOREDIST key]);

Query a sorted set representing a geospatial index to fetch members matching a given maximum distance from a member.

See L<https://redis.io/commands/georadiusbymember> for more information.

=head2 get

  $res     = $db->get($key);
  $db      = $db->get($key, sub { my ($db, $err, $res) = @_ });
  $promise = $db->get_p($key);

Get the value of a key.

See L<https://redis.io/commands/get> for more information.

=head2 getbit

  $res     = $db->getbit($key, $offset);
  $db      = $db->getbit($key, $offset, sub { my ($db, $err, $res) = @_ });
  $promise = $db->getbit_p($key, $offset);

Returns the bit value at offset in the string value stored at key.

See L<https://redis.io/commands/getbit> for more information.

=head2 getrange

  $res     = $db->getrange($key, $start, $end);
  $db      = $db->getrange($key, $start, $end, sub { my ($db, $err, $res) = @_ });
  $promise = $db->getrange_p($key, $start, $end);

Get a substring of the string stored at a key.

See L<https://redis.io/commands/getrange> for more information.

=head2 getset

  $res     = $db->getset($key, $value);
  $db      = $db->getset($key, $value, sub { my ($db, $err, $res) = @_ });
  $promise = $db->getset_p($key, $value);

Set the string value of a key and return its old value.

See L<https://redis.io/commands/getset> for more information.

=head2 hdel

  $res     = $db->hdel($key, $field [field ...]);
  $db      = $db->hdel($key, $field [field ...], sub { my ($db, $err, $res) = @_ });
  $promise = $db->hdel_p($key, $field [field ...]);

Delete one or more hash fields.

See L<https://redis.io/commands/hdel> for more information.

=head2 hexists

  $res     = $db->hexists($key, $field);
  $db      = $db->hexists($key, $field, sub { my ($db, $err, $res) = @_ });
  $promise = $db->hexists_p($key, $field);

Determine if a hash field exists.

See L<https://redis.io/commands/hexists> for more information.

=head2 hget

  $res     = $db->hget($key, $field);
  $db      = $db->hget($key, $field, sub { my ($db, $err, $res) = @_ });
  $promise = $db->hget_p($key, $field);

Get the value of a hash field.

See L<https://redis.io/commands/hget> for more information.

=head2 hgetall

  $res     = $db->hgetall($key);
  $db      = $db->hgetall($key, sub { my ($db, $err, $res) = @_ });
  $promise = $db->hgetall_p($key);

Get all the fields and values in a hash. The returned value from Redis is
automatically turned into a hash-ref for convenience.

See L<https://redis.io/commands/hgetall> for more information.

=head2 hincrby

  $res     = $db->hincrby($key, $field, $increment);
  $db      = $db->hincrby($key, $field, $increment, sub { my ($db, $err, $res) = @_ });
  $promise = $db->hincrby_p($key, $field, $increment);

Increment the integer value of a hash field by the given number.

See L<https://redis.io/commands/hincrby> for more information.

=head2 hincrbyfloat

  $res     = $db->hincrbyfloat($key, $field, $increment);
  $db      = $db->hincrbyfloat($key, $field, $increment, sub { my ($db, $err, $res) = @_ });
  $promise = $db->hincrbyfloat_p($key, $field, $increment);

Increment the float value of a hash field by the given amount.

See L<https://redis.io/commands/hincrbyfloat> for more information.

=head2 hkeys

  $res     = $db->hkeys($key);
  $db      = $db->hkeys($key, sub { my ($db, $err, $res) = @_ });
  $promise = $db->hkeys_p($key);

Get all the fields in a hash.

See L<https://redis.io/commands/hkeys> for more information.

=head2 hlen

  $res     = $db->hlen($key);
  $db      = $db->hlen($key, sub { my ($db, $err, $res) = @_ });
  $promise = $db->hlen_p($key);

Get the number of fields in a hash.

See L<https://redis.io/commands/hlen> for more information.

=head2 hmget

  $res     = $db->hmget($key, $field [field ...]);
  $db      = $db->hmget($key, $field [field ...], sub { my ($db, $err, $res) = @_ });
  $promise = $db->hmget_p($key, $field [field ...]);

Get the values of all the given hash fields.

See L<https://redis.io/commands/hmget> for more information.

=head2 hmset

  $res     = $db->hmset($key, $field => $value [field value ...]);
  $db      = $db->hmset($key, $field => $value [field value ...], sub { my ($db, $err, $res) = @_ });
  $promise = $db->hmset_p($key, $field => $value [field value ...]);

Set multiple hash fields to multiple values.

See L<https://redis.io/commands/hmset> for more information.

=head2 hset

  $res     = $db->hset($key, $field, $value);
  $db      = $db->hset($key, $field, $value, sub { my ($db, $err, $res) = @_ });
  $promise = $db->hset_p($key, $field, $value);

Set the string value of a hash field.

See L<https://redis.io/commands/hset> for more information.

=head2 hsetnx

  $res     = $db->hsetnx($key, $field, $value);
  $db      = $db->hsetnx($key, $field, $value, sub { my ($db, $err, $res) = @_ });
  $promise = $db->hsetnx_p($key, $field, $value);

Set the value of a hash field, only if the field does not exist.

See L<https://redis.io/commands/hsetnx> for more information.

=head2 hstrlen

  $res     = $db->hstrlen($key, $field);
  $db      = $db->hstrlen($key, $field, sub { my ($db, $err, $res) = @_ });
  $promise = $db->hstrlen_p($key, $field);

Get the length of the value of a hash field.

See L<https://redis.io/commands/hstrlen> for more information.

=head2 hvals

  $res     = $db->hvals($key);
  $db      = $db->hvals($key, sub { my ($db, $err, $res) = @_ });
  $promise = $db->hvals_p($key);

Get all the values in a hash.

See L<https://redis.io/commands/hvals> for more information.

=head2 info

  $res     = $db->info($section);
  $db      = $db->info($section, sub { my ($db, $err, $res) = @_ });
  $promise = $db->info_p($section);

Get information and statistics about the server. See also L</info_structured>.

See L<https://redis.io/commands/info> for more information.

=head2 info_structured

Same as L</info>, but the result is a hash-ref where the keys are the different
sections, with key/values in a sub hash. Will only be key/values if <$section>
is specified.

=head2 incr

  $res     = $db->incr($key);
  $db      = $db->incr($key, sub { my ($db, $err, $res) = @_ });
  $promise = $db->incr_p($key);

Increment the integer value of a key by one.

See L<https://redis.io/commands/incr> for more information.

=head2 incrby

  $res     = $db->incrby($key, $increment);
  $db      = $db->incrby($key, $increment, sub { my ($db, $err, $res) = @_ });
  $promise = $db->incrby_p($key, $increment);

Increment the integer value of a key by the given amount.

See L<https://redis.io/commands/incrby> for more information.

=head2 incrbyfloat

  $res     = $db->incrbyfloat($key, $increment);
  $db      = $db->incrbyfloat($key, $increment, sub { my ($db, $err, $res) = @_ });
  $promise = $db->incrbyfloat_p($key, $increment);

Increment the float value of a key by the given amount.

See L<https://redis.io/commands/incrbyfloat> for more information.

=head2 keys

  $res     = $db->keys($pattern);
  $db      = $db->keys($pattern, sub { my ($db, $err, $res) = @_ });
  $promise = $db->keys_p($pattern);

Find all keys matching the given pattern.

See L<https://redis.io/commands/keys> for more information.

=head2 lastsave

  $res     = $db->lastsave;
  $db      = $db->lastsave(sub { my ($db, $err, $res) = @_ });
  $promise = $db->lastsave_p;

Get the UNIX time stamp of the last successful save to disk.

See L<https://redis.io/commands/lastsave> for more information.

=head2 lindex

  $res     = $db->lindex($key, $index);
  $db      = $db->lindex($key, $index, sub { my ($db, $err, $res) = @_ });
  $promise = $db->lindex_p($key, $index);

Get an element from a list by its index.

See L<https://redis.io/commands/lindex> for more information.

=head2 linsert

  $res     = $db->linsert($key, $BEFORE|AFTER, $pivot, $value);
  $db      = $db->linsert($key, $BEFORE|AFTER, $pivot, $value, sub { my ($db, $err, $res) = @_ });
  $promise = $db->linsert_p($key, $BEFORE|AFTER, $pivot, $value);

Insert an element before or after another element in a list.

See L<https://redis.io/commands/linsert> for more information.

=head2 llen

  $res     = $db->llen($key);
  $db      = $db->llen($key, sub { my ($db, $err, $res) = @_ });
  $promise = $db->llen_p($key);

Get the length of a list.

See L<https://redis.io/commands/llen> for more information.

=head2 lpop

  $res     = $db->lpop($key);
  $db      = $db->lpop($key, sub { my ($db, $err, $res) = @_ });
  $promise = $db->lpop_p($key);

Remove and get the first element in a list.

See L<https://redis.io/commands/lpop> for more information.

=head2 lpush

  $res     = $db->lpush($key, $value [value ...]);
  $db      = $db->lpush($key, $value [value ...], sub { my ($db, $err, $res) = @_ });
  $promise = $db->lpush_p($key, $value [value ...]);

Prepend one or multiple values to a list.

See L<https://redis.io/commands/lpush> for more information.

=head2 lpushx

  $res     = $db->lpushx($key, $value);
  $db      = $db->lpushx($key, $value, sub { my ($db, $err, $res) = @_ });
  $promise = $db->lpushx_p($key, $value);

Prepend a value to a list, only if the list exists.

See L<https://redis.io/commands/lpushx> for more information.

=head2 lrange

  $res     = $db->lrange($key, $start, $stop);
  $db      = $db->lrange($key, $start, $stop, sub { my ($db, $err, $res) = @_ });
  $promise = $db->lrange_p($key, $start, $stop);

Get a range of elements from a list.

See L<https://redis.io/commands/lrange> for more information.

=head2 lrem

  $res     = $db->lrem($key, $count, $value);
  $db      = $db->lrem($key, $count, $value, sub { my ($db, $err, $res) = @_ });
  $promise = $db->lrem_p($key, $count, $value);

Remove elements from a list.

See L<https://redis.io/commands/lrem> for more information.

=head2 lset

  $res     = $db->lset($key, $index, $value);
  $db      = $db->lset($key, $index, $value, sub { my ($db, $err, $res) = @_ });
  $promise = $db->lset_p($key, $index, $value);

Set the value of an element in a list by its index.

See L<https://redis.io/commands/lset> for more information.

=head2 ltrim

  $res     = $db->ltrim($key, $start, $stop);
  $db      = $db->ltrim($key, $start, $stop, sub { my ($db, $err, $res) = @_ });
  $promise = $db->ltrim_p($key, $start, $stop);

Trim a list to the specified range.

See L<https://redis.io/commands/ltrim> for more information.

=head2 mget

  $res     = $db->mget($key [key ...]);
  $db      = $db->mget($key [key ...], sub { my ($db, $err, $res) = @_ });
  $promise = $db->mget_p($key [key ...]);

Get the values of all the given keys.

See L<https://redis.io/commands/mget> for more information.

=head2 move

  $res     = $db->move($key, $db);
  $db      = $db->move($key, $db, sub { my ($db, $err, $res) = @_ });
  $promise = $db->move_p($key, $db);

Move a key to another database.

See L<https://redis.io/commands/move> for more information.

=head2 mset

  $res     = $db->mset($key value [key value ...]);
  $db      = $db->mset($key value [key value ...], sub { my ($db, $err, $res) = @_ });
  $promise = $db->mset_p($key value [key value ...]);

Set multiple keys to multiple values.

See L<https://redis.io/commands/mset> for more information.

=head2 msetnx

  $res     = $db->msetnx($key value [key value ...]);
  $db      = $db->msetnx($key value [key value ...], sub { my ($db, $err, $res) = @_ });
  $promise = $db->msetnx_p($key value [key value ...]);

Set multiple keys to multiple values, only if none of the keys exist.

See L<https://redis.io/commands/msetnx> for more information.

=head2 multi

See L</multi_p>.

=head2 multi_p

  $res     = $db->multi;
  $db      = $db->multi(sub { my ($db, $err, $res) = @_ });
  $promise = $db->multi_p;

Mark the start of a transaction block. Commands issued after L</multi> will
automatically be discarded if C<$db> goes out of scope. Need to call
L</exec> to commit the queued commands to Redis.

NOTE: the previously supported C<multi_p(@promises)> syntax has been removed,
because it did not work as expected. See
L<https://github.com/jhthorsen/mojo-redis/issues/68> for details.
When L</multi_p> gets called with non-zero arguments, it C<croak()>s.
Use the promise chaining instead:

  $db->multi_p->then(sub {
    Mojo::Promise->all(
      $db->set_p(...),
      $db->incr_p(...),
      ...
    );
  })->then(sub {
	$db->exec_p;
  })->then ...

See L<https://redis.io/commands/multi> for more information.

=head2 object

  $res     = $db->object($subcommand, [arguments [arguments ...]]);
  $db      = $db->object($subcommand, [arguments [arguments ...]], sub { my ($db, $err, $res) =@_ });
  $promise = $db->object_p($subcommand, [arguments [arguments ...]]);

Inspect the internals of Redis objects.

See L<https://redis.io/commands/object> for more information.

=head2 persist

  $res     = $db->persist($key);
  $db      = $db->persist($key, sub { my ($db, $err, $res) = @_ });
  $promise = $db->persist_p($key);

Remove the expiration from a key.

See L<https://redis.io/commands/persist> for more information.

=head2 pexpire

  $res     = $db->pexpire($key, $milliseconds);
  $db      = $db->pexpire($key, $milliseconds, sub { my ($db, $err, $res) = @_ });
  $promise = $db->pexpire_p($key, $milliseconds);

Set a key's time to live in milliseconds.

See L<https://redis.io/commands/pexpire> for more information.

=head2 pexpireat

  $res     = $db->pexpireat($key, $milliseconds-timestamp);
  $db      = $db->pexpireat($key, $milliseconds-timestamp, sub { my ($db, $err, $res) = @_ });
  $promise = $db->pexpireat_p($key, $milliseconds-timestamp);

Set the expiration for a key as a UNIX timestamp specified in milliseconds.

See L<https://redis.io/commands/pexpireat> for more information.

=head2 pfadd

  $res     = $db->pfadd($key, $element [element ...]);
  $db      = $db->pfadd($key, $element [element ...], sub { my ($db, $err, $res) = @_ });
  $promise = $db->pfadd_p($key, $element [element ...]);

Adds the specified elements to the specified HyperLogLog.

See L<https://redis.io/commands/pfadd> for more information.

=head2 pfcount

  $res     = $db->pfcount($key [key ...]);
  $db      = $db->pfcount($key [key ...], sub { my ($db, $err, $res) = @_ });
  $promise = $db->pfcount_p($key [key ...]);

Return the approximated cardinality of the set(s) observed by the HyperLogLog at key(s).

See L<https://redis.io/commands/pfcount> for more information.

=head2 pfmerge

  $res     = $db->pfmerge($destkey, $sourcekey [sourcekey ...]);
  $db      = $db->pfmerge($destkey, $sourcekey [sourcekey ...], sub { my ($db, $err, $res) = @_});
  $promise = $db->pfmerge_p($destkey, $sourcekey [sourcekey ...]);

Merge N different HyperLogLogs into a single one.

See L<https://redis.io/commands/pfmerge> for more information.

=head2 ping

  $res     = $db->ping([message]);
  $db      = $db->ping([message], sub { my ($db, $err, $res) = @_ });
  $promise = $db->ping_p([message]);

Ping the server.

See L<https://redis.io/commands/ping> for more information.

=head2 psetex

  $res     = $db->psetex($key, $milliseconds, $value);
  $db      = $db->psetex($key, $milliseconds, $value, sub { my ($db, $err, $res) = @_ });
  $promise = $db->psetex_p($key, $milliseconds, $value);

Set the value and expiration in milliseconds of a key.

See L<https://redis.io/commands/psetex> for more information.

=head2 pttl

  $res     = $db->pttl($key);
  $db      = $db->pttl($key, sub { my ($db, $err, $res) = @_ });
  $promise = $db->pttl_p($key);

Get the time to live for a key in milliseconds.

See L<https://redis.io/commands/pttl> for more information.

=head2 publish

  $res     = $db->publish($channel, $message);
  $db      = $db->publish($channel, $message, sub { my ($db, $err, $res) = @_ });
  $promise = $db->publish_p($channel, $message);

Post a message to a channel.

See L<https://redis.io/commands/publish> for more information.

=head2 randomkey

  $res     = $db->randomkey;
  $db      = $db->randomkey(sub { my ($db, $err, $res) = @_ });
  $promise = $db->randomkey_p;

Return a random key from the keyspace.

See L<https://redis.io/commands/randomkey> for more information.

=head2 readonly

  $res     = $db->readonly();
  $db      = $db->readonly(, sub { my ($db, $res) = @_ });
  $promise = $db->readonly_p();

Enables read queries for a connection to a cluster slave node.

See L<https://redis.io/commands/readonly> for more information.

=head2 readwrite

  $res     = $db->readwrite();
  $db      = $db->readwrite(, sub { my ($db, $res) = @_ });
  $promise = $db->readwrite_p();

Disables read queries for a connection to a cluster slave node.

See L<https://redis.io/commands/readwrite> for more information.

=head2 rename

  $res     = $db->rename($key, $newkey);
  $db      = $db->rename($key, $newkey, sub { my ($db, $err, $res) = @_ });
  $promise = $db->rename_p($key, $newkey);

Rename a key.

See L<https://redis.io/commands/rename> for more information.

=head2 renamenx

  $res     = $db->renamenx($key, $newkey);
  $db      = $db->renamenx($key, $newkey, sub { my ($db, $err, $res) = @_ });
  $promise = $db->renamenx_p($key, $newkey);

Rename a key, only if the new key does not exist.

See L<https://redis.io/commands/renamenx> for more information.

=head2 role

  $res     = $db->role;
  $db      = $db->role(sub { my ($db, $err, $res) = @_ });
  $promise = $db->role_p;

Return the role of the instance in the context of replication.

See L<https://redis.io/commands/role> for more information.

=head2 rpop

  $res     = $db->rpop($key);
  $db      = $db->rpop($key, sub { my ($db, $err, $res) = @_ });
  $promise = $db->rpop_p($key);

Remove and get the last element in a list.

See L<https://redis.io/commands/rpop> for more information.

=head2 rpoplpush

  $res     = $db->rpoplpush($source, $destination);
  $db      = $db->rpoplpush($source, $destination, sub { my ($db, $err, $res) = @_ });
  $promise = $db->rpoplpush_p($source, $destination);

Remove the last element in a list, prepend it to another list and return it.

See L<https://redis.io/commands/rpoplpush> for more information.

=head2 rpush

  $res     = $db->rpush($key, $value [value ...]);
  $db      = $db->rpush($key, $value [value ...], sub { my ($db, $err, $res) = @_ });
  $promise = $db->rpush_p($key, $value [value ...]);

Append one or multiple values to a list.

See L<https://redis.io/commands/rpush> for more information.

=head2 rpushx

  $res     = $db->rpushx($key, $value);
  $db      = $db->rpushx($key, $value, sub { my ($db, $err, $res) = @_ });
  $promise = $db->rpushx_p($key, $value);

Append a value to a list, only if the list exists.

See L<https://redis.io/commands/rpushx> for more information.

=head2 restore

  $res     = $db->restore($key, $ttl, $serialized-value, [REPLACE]);
  $db      = $db->restore($key, $ttl, $serialized-value, [REPLACE], sub { my ($db, $err, $res) = @_ });
  $promise = $db->restore_p($key, $ttl, $serialized-value, [REPLACE]);

Create a key using the provided serialized value, previously obtained using DUMP.

See L<https://redis.io/commands/restore> for more information.

=head2 sadd

  $res     = $db->sadd($key, $member [member ...]);
  $db      = $db->sadd($key, $member [member ...], sub { my ($db, $err, $res) = @_ });
  $promise = $db->sadd_p($key, $member [member ...]);

Add one or more members to a set.

See L<https://redis.io/commands/sadd> for more information.

=head2 save

  $res     = $db->save;
  $db      = $db->save(sub { my ($db, $err, $res) = @_ });
  $promise = $db->save_p;

Synchronously save the dataset to disk.

See L<https://redis.io/commands/save> for more information.

=head2 scard

  $res     = $db->scard($key);
  $db      = $db->scard($key, sub { my ($db, $err, $res) = @_ });
  $promise = $db->scard_p($key);

Get the number of members in a set.

See L<https://redis.io/commands/scard> for more information.

=head2 script

  $res     = $db->script($sub_command, @args);
  $db      = $db->script($sub_command, @args, sub { my ($db, $err, $res) = @_ });
  $promise = $db->script_p($sub_command, @args);

Execute a script command.

See L<https://redis.io/commands/script-debug>,
L<https://redis.io/commands/script-exists>,
L<https://redis.io/commands/script-flush>,
L<https://redis.io/commands/script-kill> or
L<https://redis.io/commands/script-load> for more information.

=head2 sdiff

  $res     = $db->sdiff($key [key ...]);
  $db      = $db->sdiff($key [key ...], sub { my ($db, $err, $res) = @_ });
  $promise = $db->sdiff_p($key [key ...]);

Subtract multiple sets.

See L<https://redis.io/commands/sdiff> for more information.

=head2 sdiffstore

  $res     = $db->sdiffstore($destination, $key [key ...]);
  $db      = $db->sdiffstore($destination, $key [key ...], sub { my ($db, $err, $res) = @_ });
  $promise = $db->sdiffstore_p($destination, $key [key ...]);

Subtract multiple sets and store the resulting set in a key.

See L<https://redis.io/commands/sdiffstore> for more information.

=head2 set

  $res     = $db->set($key, $value, [expiration EX seconds|PX milliseconds], [NX|XX]);
  $db      = $db->set($key, $value, [expiration EX seconds|PX milliseconds], [NX|XX], sub {my ($db, $err, $res) = @_ });
  $promise = $db->set_p($key, $value, [expiration EX seconds|PX milliseconds], [NX|XX]);

Set the string value of a key.

See L<https://redis.io/commands/set> for more information.

=head2 setbit

  $res     = $db->setbit($key, $offset, $value);
  $db      = $db->setbit($key, $offset, $value, sub { my ($db, $err, $res) = @_ });
  $promise = $db->setbit_p($key, $offset, $value);

Sets or clears the bit at offset in the string value stored at key.

See L<https://redis.io/commands/setbit> for more information.

=head2 setex

  $res     = $db->setex($key, $seconds, $value);
  $db      = $db->setex($key, $seconds, $value, sub { my ($db, $err, $res) = @_ });
  $promise = $db->setex_p($key, $seconds, $value);

Set the value and expiration of a key.

See L<https://redis.io/commands/setex> for more information.

=head2 setnx

  $res     = $db->setnx($key, $value);
  $db      = $db->setnx($key, $value, sub { my ($db, $err, $res) = @_ });
  $promise = $db->setnx_p($key, $value);

Set the value of a key, only if the key does not exist.

See L<https://redis.io/commands/setnx> for more information.

=head2 setrange

  $res     = $db->setrange($key, $offset, $value);
  $db      = $db->setrange($key, $offset, $value, sub { my ($db, $err, $res) = @_ });
  $promise = $db->setrange_p($key, $offset, $value);

Overwrite part of a string at key starting at the specified offset.

See L<https://redis.io/commands/setrange> for more information.

=head2 sinter

  $res     = $db->sinter($key [key ...]);
  $db      = $db->sinter($key [key ...], sub { my ($db, $err, $res) = @_ });
  $promise = $db->sinter_p($key [key ...]);

Intersect multiple sets.

See L<https://redis.io/commands/sinter> for more information.

=head2 sinterstore

  $res     = $db->sinterstore($destination, $key [key ...]);
  $db      = $db->sinterstore($destination, $key [key ...], sub { my ($db, $err, $res) = @_ });
  $promise = $db->sinterstore_p($destination, $key [key ...]);

Intersect multiple sets and store the resulting set in a key.

See L<https://redis.io/commands/sinterstore> for more information.

=head2 sismember

  $res     = $db->sismember($key, $member);
  $db      = $db->sismember($key, $member, sub { my ($db, $err, $res) = @_ });
  $promise = $db->sismember_p($key, $member);

Determine if a given value is a member of a set.

See L<https://redis.io/commands/sismember> for more information.

=head2 slaveof

  $res     = $db->slaveof($host, $port);
  $db      = $db->slaveof($host, $port, sub { my ($db, $err, $res) = @_ });
  $promise = $db->slaveof_p($host, $port);

Make the server a slave of another instance, or promote it as master.

See L<https://redis.io/commands/slaveof> for more information.

=head2 slowlog

  $res     = $db->slowlog($subcommand, [argument]);
  $db      = $db->slowlog($subcommand, [argument], sub { my ($db, $err, $res) = @_ });
  $promise = $db->slowlog_p($subcommand, [argument]);

Manages the Redis slow queries log.

See L<https://redis.io/commands/slowlog> for more information.

=head2 smembers

  $res     = $db->smembers($key);
  $db      = $db->smembers($key, sub { my ($db, $err, $res) = @_ });
  $promise = $db->smembers_p($key);

Get all the members in a set.

See L<https://redis.io/commands/smembers> for more information.

=head2 smove

  $res     = $db->smove($source, $destination, $member);
  $db      = $db->smove($source, $destination, $member, sub { my ($db, $err, $res) = @_ });
  $promise = $db->smove_p($source, $destination, $member);

Move a member from one set to another.

See L<https://redis.io/commands/smove> for more information.

=head2 sort

  $res     = $db->sort($key, [BY pattern], [LIMIT offset count], [GET pattern [GET pattern ...]], [ASC|DESC], [ALPHA], [STORE destination]);
  $db      = $db->sort($key, [BY pattern], [LIMIT offset count], [GET pattern [GET pattern ...]], [ASC|DESC], [ALPHA], [STORE destination], sub { my ($db, $err, $res) = @_ });
  $promise = $db->sort_p($key, [BY pattern], [LIMIT offset count], [GET pattern [GET pattern ...]], [ASC|DESC], [ALPHA], [STORE destination]);

Sort the elements in a list, set or sorted set.

See L<https://redis.io/commands/sort> for more information.

=head2 spop

  $res     = $db->spop($key, [count]);
  $db      = $db->spop($key, [count], sub { my ($db, $err, $res) = @_ });
  $promise = $db->spop_p($key, [count]);

Remove and return one or multiple random members from a set.

See L<https://redis.io/commands/spop> for more information.

=head2 srandmember

  $res     = $db->srandmember($key, [count]);
  $db      = $db->srandmember($key, [count], sub { my ($db, $err, $res) = @_ });
  $promise = $db->srandmember_p($key, [count]);

Get one or multiple random members from a set.

See L<https://redis.io/commands/srandmember> for more information.

=head2 srem

  $res     = $db->srem($key, $member [member ...]);
  $db      = $db->srem($key, $member [member ...], sub { my ($db, $err, $res) = @_ });
  $promise = $db->srem_p($key, $member [member ...]);

Remove one or more members from a set.

See L<https://redis.io/commands/srem> for more information.

=head2 strlen

  $res     = $db->strlen($key);
  $db      = $db->strlen($key, sub { my ($db, $err, $res) = @_ });
  $promise = $db->strlen_p($key);

Get the length of the value stored in a key.

See L<https://redis.io/commands/strlen> for more information.

=head2 sunion

  $res     = $db->sunion($key [key ...]);
  $db      = $db->sunion($key [key ...], sub { my ($db, $err, $res) = @_ });
  $promise = $db->sunion_p($key [key ...]);

Add multiple sets.

See L<https://redis.io/commands/sunion> for more information.

=head2 sunionstore

  $res     = $db->sunionstore($destination, $key [key ...]);
  $db      = $db->sunionstore($destination, $key [key ...], sub { my ($db, $err, $res) = @_ });
  $promise = $db->sunionstore_p($destination, $key [key ...]);

Add multiple sets and store the resulting set in a key.

See L<https://redis.io/commands/sunionstore> for more information.

=head2 time

  $res     = $db->time;
  $db      = $db->time(sub { my ($db, $err, $res) = @_ });
  $promise = $db->time_p;

Return the current server time.

See L<https://redis.io/commands/time> for more information.

=head2 touch

  $res     = $db->touch($key [key ...]);
  $db      = $db->touch($key [key ...], sub { my ($db, $err, $res) = @_ });
  $promise = $db->touch_p($key [key ...]);

Alters the last access time of a key(s). Returns the number of existing keys specified.

See L<https://redis.io/commands/touch> for more information.

=head2 ttl

  $res     = $db->ttl($key);
  $db      = $db->ttl($key, sub { my ($db, $err, $res) = @_ });
  $promise = $db->ttl_p($key);

Get the time to live for a key.

See L<https://redis.io/commands/ttl> for more information.

=head2 type

  $res     = $db->type($key);
  $db      = $db->type($key, sub { my ($db, $err, $res) = @_ });
  $promise = $db->type_p($key);

Determine the type stored at key.

See L<https://redis.io/commands/type> for more information.

=head2 unlink

  $res     = $db->unlink($key [key ...]);
  $db      = $db->unlink($key [key ...], sub { my ($db, $err, $res) = @_ });
  $promise = $db->unlink_p($key [key ...]);

Delete a key asynchronously in another thread. Otherwise it is just as DEL, but non blocking.

See L<https://redis.io/commands/unlink> for more information.

=head2 unwatch

  $res     = $db->unwatch;
  $db      = $db->unwatch(sub { my ($db, $err, $res) = @_ });
  $promise = $db->unwatch_p;

Forget about all watched keys.

See L<https://redis.io/commands/unwatch> for more information.

=head2 watch

  $res     = $db->watch($key [key ...]);
  $db      = $db->watch($key [key ...], sub { my ($db, $err, $res) = @_ });
  $promise = $db->watch_p($key [key ...]);

Watch the given keys to determine execution of the MULTI/EXEC block.

See L<https://redis.io/commands/watch> for more information.

=head2 xadd

  $res     = $db->xadd($key, $ID, $field string [field string ...]);
  $db      = $db->xadd($key, $ID, $field string [field string ...], sub { my ($db, $err, $res) = @_ });
  $promise = $db->xadd_p($key, $ID, $field string [field string ...]);

Appends a new entry to a stream.

See L<https://redis.io/commands/xadd> for more information.

=head2 xlen

  $res     = $db->xlen($key);
  $db      = $db->xlen($key, sub { my ($db, $err, $res) = @_ });
  $promise = $db->xlen_p($key);

Return the number of entires in a stream.

See L<https://redis.io/commands/xlen> for more information.

=head2 xpending

  $res     = $db->xpending($key, $group, [start end count], [consumer]);
  $db      = $db->xpending($key, $group, [start end count], [consumer], sub { my ($db, $err, $res) = @_ });
  $promise = $db->xpending_p($key, $group, [start end count], [consumer]);

Return information and entries from a stream consumer group pending entries list, that are messages fetched but never acknowledged.

See L<https://redis.io/commands/xpending> for more information.

=head2 xrange

  $res     = $db->xrange($key, $start, $end, [COUNT count]);
  $db      = $db->xrange($key, $start, $end, [COUNT count], sub { my ($db, $err, $res) = @_ });
  $promise = $db->xrange_p($key, $start, $end, [COUNT count]);

Return a range of elements in a stream, with IDs matching the specified IDs interval.

See L<https://redis.io/commands/xrange> for more information.

=head2 xread

  $res     = $db->xread([COUNT count], [BLOCK milliseconds], $STREAMS, $key [key ...], $ID [ID ...]);
  $db      = $db->xread([COUNT count], [BLOCK milliseconds], $STREAMS, $key [key ...], $ID [ID ...], sub { my ($db, $err, $res) = @_ });
  $promise = $db->xread_p([COUNT count], [BLOCK milliseconds], $STREAMS, $key [key ...], $ID [ID ...]);

Return never seen elements in multiple streams, with IDs greater than the ones reported by the caller for each stream. Can block.

See L<https://redis.io/commands/xread> for more information.

=head2 xread_structured

Same as L</xread>, but the result is a data structure like this:

  {
    $stream_name => [
      [ $id1 => [@data1] ],
      [ $id2 => [@data2] ],
      ...
    ]
  }

This method is currently EXPERIMENTAL, but will only change if bugs are
discovered.

=head2 xreadgroup

  $res     = $db->xreadgroup($GROUP group consumer, [COUNT count], [BLOCK milliseconds], $STREAMS, $key [key ...], $ID [ID ...]);
  $db      = $db->xreadgroup($GROUP group consumer, [COUNT count], [BLOCK milliseconds], $STREAMS, $key [key ...], $ID [ID ...], sub { my ($db, $err, $res) = @_ });
  $promise = $db->xreadgroup_p($GROUP group consumer, [COUNT count], [BLOCK milliseconds], $STREAMS, $key [key ...], $ID [ID ...]);

Return new entries from a stream using a consumer group, or access the history of the pending entries for a given consumer. Can block.

See L<https://redis.io/commands/xreadgroup> for more information.

=head2 xrevrange

  $res     = $db->xrevrange($key, $end, $start, [COUNT count]);
  $db      = $db->xrevrange($key, $end, $start, [COUNT count], sub { my ($db, $err, $res) = @_ });
  $promise = $db->xrevrange_p($key, $end, $start, [COUNT count]);

Return a range of elements in a stream, with IDs matching the specified IDs interval, in reverse order (from greater to smaller IDs) compared to XRANGE.

See L<https://redis.io/commands/xrevrange> for more information.

=head2 zadd

  $res     = $db->zadd($key, [NX|XX], [CH], [INCR], $score member [score member ...]);
  $db      = $db->zadd($key, [NX|XX], [CH], [INCR], $score member [score member ...], sub {my ($db, $err, $res) = @_ });
  $promise = $db->zadd_p($key, [NX|XX], [CH], [INCR], $score member [score member ...]);

Add one or more members to a sorted set, or update its score if it already exists.

See L<https://redis.io/commands/zadd> for more information.

=head2 zcard

  $res     = $db->zcard($key);
  $db      = $db->zcard($key, sub { my ($db, $err, $res) = @_ });
  $promise = $db->zcard_p($key);

Get the number of members in a sorted set.

See L<https://redis.io/commands/zcard> for more information.

=head2 zcount

  $res     = $db->zcount($key, $min, $max);
  $db      = $db->zcount($key, $min, $max, sub { my ($db, $err, $res) = @_ });
  $promise = $db->zcount_p($key, $min, $max);

Count the members in a sorted set with scores within the given values.

See L<https://redis.io/commands/zcount> for more information.

=head2 zincrby

  $res     = $db->zincrby($key, $increment, $member);
  $db      = $db->zincrby($key, $increment, $member, sub { my ($db, $err, $res) = @_ });
  $promise = $db->zincrby_p($key, $increment, $member);

Increment the score of a member in a sorted set.

See L<https://redis.io/commands/zincrby> for more information.

=head2 zinterstore

  $res     = $db->zinterstore($destination, $numkeys, $key [key ...], [WEIGHTS weight [weight ...]], [AGGREGATE SUM|MIN|MAX]);
  $db      = $db->zinterstore($destination, $numkeys, $key [key ...], [WEIGHTS weight [weight ...]], [AGGREGATE SUM|MIN|MAX], sub { my ($db, $err, $res) = @_ });
  $promise = $db->zinterstore_p($destination, $numkeys, $key [key ...], [WEIGHTS weight [weight ...]], [AGGREGATE SUM|MIN|MAX]);

Intersect multiple sorted sets and store the resulting sorted set in a new key.

See L<https://redis.io/commands/zinterstore> for more information.

=head2 zlexcount

  $res     = $db->zlexcount($key, $min, $max);
  $db      = $db->zlexcount($key, $min, $max, sub { my ($db, $err, $res) = @_ });
  $promise = $db->zlexcount_p($key, $min, $max);

Count the number of members in a sorted set between a given lexicographical range.

See L<https://redis.io/commands/zlexcount> for more information.

=head2 zpopmax

  $res     = $db->zpopmax($key, [count]);
  $db      = $db->zpopmax($key, [count], sub { my ($db, $err, $res) = @_ });
  $promise = $db->zpopmax_p($key, [count]);

Remove and return members with the highest scores in a sorted set.

See L<https://redis.io/commands/zpopmax> for more information.

=head2 zpopmin

  $res     = $db->zpopmin($key, [count]);
  $db      = $db->zpopmin($key, [count], sub { my ($db, $err, $res) = @_ });
  $promise = $db->zpopmin_p($key, [count]);

Remove and return members with the lowest scores in a sorted set.

See L<https://redis.io/commands/zpopmin> for more information.

=head2 zrange

  $res     = $db->zrange($key, $start, $stop, [WITHSCORES]);
  $db      = $db->zrange($key, $start, $stop, [WITHSCORES], sub { my ($db, $err, $res) = @_ });
  $promise = $db->zrange_p($key, $start, $stop, [WITHSCORES]);

Return a range of members in a sorted set, by index.

See L<https://redis.io/commands/zrange> for more information.

=head2 zrangebylex

  $res     = $db->zrangebylex($key, $min, $max, [LIMIT offset count]);
  $db      = $db->zrangebylex($key, $min, $max, [LIMIT offset count], sub { my ($db, $err, $res) = @_ });
  $promise = $db->zrangebylex_p($key, $min, $max, [LIMIT offset count]);

Return a range of members in a sorted set, by lexicographical range.

See L<https://redis.io/commands/zrangebylex> for more information.

=head2 zrangebyscore

  $res     = $db->zrangebyscore($key, $min, $max, [WITHSCORES], [LIMIT offset count]);
  $db      = $db->zrangebyscore($key, $min, $max, [WITHSCORES], [LIMIT offset count], sub {my ($db, $err, $res) = @_ });
  $promise = $db->zrangebyscore_p($key, $min, $max, [WITHSCORES], [LIMIT offset count]);

Return a range of members in a sorted set, by score.

See L<https://redis.io/commands/zrangebyscore> for more information.

=head2 zrank

  $res     = $db->zrank($key, $member);
  $db      = $db->zrank($key, $member, sub { my ($db, $err, $res) = @_ });
  $promise = $db->zrank_p($key, $member);

Determine the index of a member in a sorted set.

See L<https://redis.io/commands/zrank> for more information.

=head2 zrem

  $res     = $db->zrem($key, $member [member ...]);
  $db      = $db->zrem($key, $member [member ...], sub { my ($db, $err, $res) = @_ });
  $promise = $db->zrem_p($key, $member [member ...]);

Remove one or more members from a sorted set.

See L<https://redis.io/commands/zrem> for more information.

=head2 zremrangebylex

  $res     = $db->zremrangebylex($key, $min, $max);
  $db      = $db->zremrangebylex($key, $min, $max, sub { my ($db, $err, $res) = @_ });
  $promise = $db->zremrangebylex_p($key, $min, $max);

Remove all members in a sorted set between the given lexicographical range.

See L<https://redis.io/commands/zremrangebylex> for more information.

=head2 zremrangebyrank

  $res     = $db->zremrangebyrank($key, $start, $stop);
  $db      = $db->zremrangebyrank($key, $start, $stop, sub { my ($db, $err, $res) = @_ });
  $promise = $db->zremrangebyrank_p($key, $start, $stop);

Remove all members in a sorted set within the given indexes.

See L<https://redis.io/commands/zremrangebyrank> for more information.

=head2 zremrangebyscore

  $res     = $db->zremrangebyscore($key, $min, $max);
  $db      = $db->zremrangebyscore($key, $min, $max, sub { my ($db, $err, $res) = @_ });
  $promise = $db->zremrangebyscore_p($key, $min, $max);

Remove all members in a sorted set within the given scores.

See L<https://redis.io/commands/zremrangebyscore> for more information.

=head2 zrevrange

  $res     = $db->zrevrange($key, $start, $stop, [WITHSCORES]);
  $db      = $db->zrevrange($key, $start, $stop, [WITHSCORES], sub { my ($db, $err, $res) = @_ });
  $promise = $db->zrevrange_p($key, $start, $stop, [WITHSCORES]);

Return a range of members in a sorted set, by index, with scores ordered from high to low.

See L<https://redis.io/commands/zrevrange> for more information.

=head2 zrevrangebylex

  $res     = $db->zrevrangebylex($key, $max, $min, [LIMIT offset count]);
  $db      = $db->zrevrangebylex($key, $max, $min, [LIMIT offset count], sub { my ($db, $err, $res) = @_ });
  $promise = $db->zrevrangebylex_p($key, $max, $min, [LIMIT offset count]);

Return a range of members in a sorted set, by lexicographical range, ordered from higher to lower strings.

See L<https://redis.io/commands/zrevrangebylex> for more information.

=head2 zrevrangebyscore

  $res     = $db->zrevrangebyscore($key, $max, $min, [WITHSCORES], [LIMIT offset count]);
  $db      = $db->zrevrangebyscore($key, $max, $min, [WITHSCORES], [LIMIT offset count], sub { my ($db, $err, $res) = @_ });
  $promise = $db->zrevrangebyscore_p($key, $max, $min, [WITHSCORES], [LIMIT offset count]);

Return a range of members in a sorted set, by score, with scores ordered from high to low.

See L<https://redis.io/commands/zrevrangebyscore> for more information.

=head2 zrevrank

  $res     = $db->zrevrank($key, $member);
  $db      = $db->zrevrank($key, $member, sub { my ($db, $err, $res) = @_ });
  $promise = $db->zrevrank_p($key, $member);

Determine the index of a member in a sorted set, with scores ordered from high to low.

See L<https://redis.io/commands/zrevrank> for more information.

=head2 zscore

  $res     = $db->zscore($key, $member);
  $db      = $db->zscore($key, $member, sub { my ($db, $err, $res) = @_ });
  $promise = $db->zscore_p($key, $member);

Get the score associated with the given member in a sorted set.

See L<https://redis.io/commands/zscore> for more information.

=head2 zunionstore

  $res     = $db->zunionstore($destination, $numkeys, $key [key ...], [WEIGHTS weight [weight ...]], [AGGREGATE SUM|MIN|MAX]);
  $db      = $db->zunionstore($destination, $numkeys, $key [key ...], [WEIGHTS weight [weight ...]], [AGGREGATE SUM|MIN|MAX], sub { my ($db, $err, $res) = @_ });
  $promise = $db->zunionstore_p($destination, $numkeys, $key [key ...], [WEIGHTS weight [weight ...]], [AGGREGATE SUM|MIN|MAX]);

Add multiple sorted sets and store the resulting sorted set in a new key.

See L<https://redis.io/commands/zunionstore> for more information.

=head1 SEE ALSO

L<Mojo::Redis>.

=cut

lib/Mojo/Redis/PubSub.pm  view on Meta::CPAN

package Mojo::Redis::PubSub;
use Mojo::Base 'Mojo::EventEmitter';

use Mojo::JSON qw(from_json to_json);

use constant DEBUG => $ENV{MOJO_REDIS_DEBUG};

has connection => sub {
  my $self = shift;
  my $conn = $self->redis->_connection;

  Scalar::Util::weaken($self);
  for my $name (qw(close error response)) {
    my $handler = "_on_$name";
    $conn->on($name => sub { $self and $self->$handler(@_) });
  }

  return $conn;
};

has db => sub {
  my $self = shift;
  my $db   = $self->redis->db;
  Scalar::Util::weaken($db->{redis});
  return $db;
};

has reconnect_interval => 1;
has redis              => sub { Carp::confess('redis is requried in constructor') };

sub channels_p { shift->db->call_p(qw(PUBSUB CHANNELS), @_) }
sub json       { ++$_[0]{json}{$_[1]} and return $_[0] }

sub keyspace_listen {
  my ($self, $cb) = (shift, pop);
  my $key = $self->_keyspace_key(@_);
  $self->{keyspace_listen}{$key} = 1;
  return $self->listen($key, $cb);
}

sub keyspace_unlisten {
  my ($self, $cb) = (shift, ref $_[-1] eq 'CODE' ? pop : undef);
  return $self->unlisten($self->_keyspace_key(@_), $cb);
}

sub listen {
  my ($self, $name, $cb) = @_;

  unless (@{$self->{chans}{$name} ||= []}) {
    Mojo::IOLoop->remove(delete $self->{reconnect_tid}) if $self->{reconnect_tid};
    $self->_write([($name =~ /\*/ ? 'PSUBSCRIBE' : 'SUBSCRIBE') => $name]);
  }

  push @{$self->{chans}{$name}}, $cb;
  return $cb;
}

sub notify_p {
  my ($self, $name, $payload) = @_;
  $payload = to_json $payload if $self->{json}{$name};
  return $self->db->call_p(PUBLISH => $name, $payload);
}

sub notify   { shift->notify_p(@_)->wait }
sub numpat_p { shift->db->call_p(qw(PUBSUB NUMPAT)) }
sub numsub_p { shift->db->call_p(qw(PUBSUB NUMSUB), @_)->then(\&_flatten) }

sub unlisten {
  my ($self, $name, $cb) = @_;
  my $chans = $self->{chans}{$name};

  @$chans = $cb ? grep { $cb ne $_ } @$chans : ();
  unless (@$chans) {
    my $conn = $self->connection;
    $conn->write(($name =~ /\*/ ? 'PUNSUBSCRIBE' : 'UNSUBSCRIBE'), $name) if $conn->is_connected;
    delete $self->{chans}{$name};
  }

  return $self;
}

sub _flatten { +{@{$_[0]}} }

sub _keyspace_key {
  my $args = ref $_[-1] eq 'HASH' ? pop : {};
  my $self = shift;

  local $args->{key}  = $_[0] // $args->{key} // '*';
  local $args->{op}   = $_[1] // $args->{op}  // '*';
  local $args->{type} = $args->{type} || ($args->{key} eq '*' ? 'keyevent' : 'keyspace');

  return sprintf '__%s@%s__:%s', $args->{type}, $args->{db} // $self->redis->url->path->[0] // '*',
    $args->{type} eq 'keyevent' ? $args->{op} : $args->{key};
}

sub _on_close {
  my $self = shift;
  $self->emit(disconnect => $self->connection);

  my $delay = $self->reconnect_interval;
  return $self if $delay < 0 or $self->{reconnect_tid};

  warn qq([Mojo::Redis::PubSub] Reconnecting in ${delay}s...\n) if DEBUG;
  Scalar::Util::weaken($self);
  $self->{reconnect}     = 1;
  $self->{reconnect_tid} = Mojo::IOLoop->timer($delay => sub { $self and $self->_reconnect });
  return $self;
}

sub _on_error { $_[0]->emit(error => $_[2]) }

sub _on_response {
  my ($self, $conn, $res) = @_;
  $self->emit(reconnect => $conn) if delete $self->{reconnect};

  # $res = [pmessage => $name, $channel, $data]
  # $res = [message  =>        $channel, $data]

  return                    unless ref $res eq 'ARRAY';
  return $self->emit(@$res) unless $res->[0] =~ m!^p?message$!i;

  my ($name)          = $res->[0] eq 'pmessage' ? splice @$res, 1, 1 : ($res->[1]);
  my $keyspace_listen = $self->{keyspace_listen}{$name};

  local $@;
  $res->[2] = eval { from_json $res->[2] } if $self->{json}{$name};
  for my $cb (@{$self->{chans}{$name} || []}) {
    $self->$cb($keyspace_listen ? [@$res[1, 2]] : $res->[2], $res->[1]);
  }
}

sub _reconnect {
  my $self = shift;
  delete $self->{$_} for qw(before_connect connection reconnect_tid);
  $self->_write(map { [(/\*/ ? 'PSUBSCRIBE' : 'SUBSCRIBE') => $_] } keys %{$self->{chans}});
}

sub _write {
  my ($self, @commands) = @_;
  my $conn = $self->connection;
  $self->emit(before_connect => $conn) unless $self->{before_connect}++;
  $conn->write(@$_) for @commands;
}

1;

=encoding utf8

=head1 NAME

Mojo::Redis::PubSub - Publish and subscribe to Redis messages

=head1 SYNOPSIS

  use Mojo::Redis;

  my $redis  = Mojo::Redis->new;
  my $pubsub = $redis->pubsub;

  $pubsub->listen("user:superwoman:messages" => sub {
    my ($pubsub, $message, $channel) = @_;
    say "superwoman got a message '$message' from channel '$channel'";
  });

  $pubsub->notify("user:batboy:messages", "How are you doing?");

See L<https://github.com/jhthorsen/mojo-redis/blob/master/examples/chat.pl>
for example L<Mojolicious> application.

=head1 DESCRIPTION

L<Mojo::Redis::PubSub> is an implementation of the Redis Publish/Subscribe
messaging paradigm. This class has the same API as L<Mojo::Pg::PubSub>, so
you can easily switch between the backends.

This object holds one connection for receiving messages, and one connection
for sending messages. They are created lazily the first time L</listen> or
L</notify> is called. These connections does not affect the connection pool
for L<Mojo::Redis>.

See L<pubsub|https://redis.io/topics/pubsub> for more details.

=head1 EVENTS

=head2 before_connect

  $pubsub->on(before_connect => sub { my ($pubsub, $conn) = @_; ... });

Emitted before L</connection> is connected to the redis server. This can be
useful if you want to gather the L<CLIENT ID|https://redis.io/commands/client-id>
or run other commands before it goes into subscribe mode.

=head2 disconnect

  $pubsub->on(disconnect => sub { my ($pubsub, $conn) = @_; ... });

Emitted after L</connection> is disconnected from the redis server.

=head2 psubscribe

  $pubsub->on(psubscribe => sub { my ($pubsub, $channel, $success) = @_; ... });

Emitted when the server responds to the L</listen> request and/or when
L</reconnect> resends psubscribe messages.

This event is EXPERIMENTAL.

=head2 reconnect

  $pubsub->on(reconnect => sub { my ($pubsub, $conn) = @_; ... });

Emitted after switching the L</connection> with a new connection. This event
will only happen if L</reconnect_interval> is 0 or more.

=head2 subscribe

  $pubsub->on(subscribe => sub { my ($pubsub, $channel, $success) = @_; ... });

Emitted when the server responds to the L</listen> request and/or when
L</reconnect> resends subscribe messages.

This event is EXPERIMENTAL.

=head1 ATTRIBUTES

=head2 db

  $db = $pubsub->db;

Holds a L<Mojo::Redis::Database> object that will be used to publish messages
or run other commands that cannot be run by the L</connection>.

=head2 connection

  $conn = $pubsub->connection;

Holds a L<Mojo::Redis::Connection> object that will be used to subscribe to
channels.

=head2 reconnect_interval

  $interval = $pubsub->reconnect_interval;
  $pubsub   = $pubsub->reconnect_interval(1);
  $pubsub   = $pubsub->reconnect_interval(0.1);
  $pubsub   = $pubsub->reconnect_interval(-1);

The amount of time in seconds to wait to L</reconnect> after disconnecting.
Default is 1 (second). L</reconnect> can be disabled by setting this to a
negative value.

=head2 redis

  $conn   = $pubsub->connection;
  $pubsub = $pubsub->connection(Mojo::Redis->new);

Holds a L<Mojo::Redis> object used to create the connections to talk with Redis.

=head1 METHODS

=head2 channels_p

  $promise = $pubsub->channels_p->then(sub { my $channels = shift });
  $promise = $pubsub->channels_p("pat*")->then(sub { my $channels = shift });

Lists the currently active channels. An active channel is a Pub/Sub channel
with one or more subscribers (not including clients subscribed to patterns).

=head2 json

  $pubsub = $pubsub->json("foo");

Activate automatic JSON encoding and decoding with L<Mojo::JSON/"to_json"> and
L<Mojo::JSON/"from_json"> for a channel.

  # Send and receive data structures
  $pubsub->json("foo")->listen(foo => sub {
    my ($pubsub, $payload, $channel) = @_;
    say $payload->{bar};
  });
  $pubsub->notify(foo => {bar => 'I ♥ Mojolicious!'});

=head2 keyspace_listen

  $cb = $pubsub->keyspace_listen(\%args,              sub { my ($pubsub, $message) = @_ }) });
  $cb = $pubsub->keyspace_listen({key => "cool:key"}, sub { my ($pubsub, $message) = @_ }) });
  $cb = $pubsub->keyspace_listen({op  => "del"},      sub { my ($pubsub, $message) = @_ }) });

Used to listen for keyspace notifications. See L<https://redis.io/topics/notifications>
for more details. The channel that will be subscribed to will look like one of
these:

  __keyspace@${db}__:$key $op
  __keyevent@${db}__:$op $key

This means that "key" and "op" is mutually exclusive from the list of
parameters below:

=over 2

=item * db

Default database to listen for events is the database set in
L<Mojo::Redis/url>. "*" is also a valid value, meaning listen for events
happening in all databases.

=item * key

Alternative to passing in C<$key>. Default value is "*".

=item * op

Alternative to passing in C<$op>. Default value is "*".

=back

=head2 keyspace_unlisten

  $pubsub = $pubsub->keyspace_unlisten(@args);
  $pubsub = $pubsub->keyspace_unlisten(@args, $cb);

Stop listening for keyspace events. See L</keyspace_listen> for details about
keyspace events and what C<@args> can be.

=head2 listen

  $cb = $pubsub->listen($channel => sub { my ($pubsub, $message, $channel) = @_ });

Subscribe to an exact channel name
(L<SUBSCRIBE|https://redis.io/commands/subscribe>) or a channel name with a
pattern (L<PSUBSCRIBE|https://redis.io/commands/psubscribe>). C<$channel> in
the callback will be the exact channel name, without any pattern. C<$message>
will be the data published to that the channel.

The returning code ref can be passed on to L</unlisten>.

=head2 notify

  $pubsub->notify($channel => $message);

Send a plain string message to a channel. This method is the same as:

  $pubsub->notify_p($channel => $message)->wait;

=head2 notify_p

  $p = $pubsub->notify_p($channel => $message);

Send a plain string message to a channel and returns a L<Mojo::Promise> object.

=head2 numpat_p

  $promise = $pubsub->channels_p->then(sub { my $int = shift });

Returns the number of subscriptions to patterns (that are performed using the
PSUBSCRIBE command). Note that this is not just the count of clients
subscribed to patterns but the total number of patterns all the clients are
subscribed to.

=head2 numsub_p

  $promise = $pubsub->numsub_p(@channels)->then(sub { my $channels = shift });

Returns the number of subscribers (not counting clients subscribed to
patterns) for the specified channels as a hash-ref, where the keys are
channel names.

=head2 unlisten

  $pubsub = $pubsub->unlisten($channel);
  $pubsub = $pubsub->unlisten($channel, $cb);

Unsubscribe from a channel.

=head1 SEE ALSO

L<Mojo::Redis>.

=cut

t/00-basic.t  view on Meta::CPAN

use Test::More;
use File::Find;

if(($ENV{HARNESS_PERL_SWITCHES} || '') =~ /Devel::Cover/) {
  plan skip_all => 'HARNESS_PERL_SWITCHES =~ /Devel::Cover/';
}
if(!eval 'use Test::Pod; 1') {
  *Test::Pod::pod_file_ok = sub { SKIP: { skip "pod_file_ok(@_) (Test::Pod is required)", 1 } };
}
if(!eval 'use Test::Pod::Coverage; 1') {
  *Test::Pod::Coverage::pod_coverage_ok = sub { SKIP: { skip "pod_coverage_ok(@_) (Test::Pod::Coverage is required)", 1 } };
}
if(!eval 'use Test::CPAN::Changes; 1') {
  *Test::CPAN::Changes::changes_file_ok = sub { SKIP: { skip "changes_ok(@_) (Test::CPAN::Changes is required)", 4 } };
}

find(
  {
    wanted => sub { /\.pm$/ and push @files, $File::Find::name },
    no_chdir => 1
  },
  -e 'blib' ? 'blib' : 'lib',
);

plan tests => @files * 3 + 4;

for my $file (@files) {
  my $module = $file; $module =~ s,\.pm$,,; $module =~ s,.*/?lib/,,; $module =~ s,/,::,g;
  ok eval "use $module; 1", "use $module" or diag $@;
  Test::Pod::pod_file_ok($file);
  Test::Pod::Coverage::pod_coverage_ok($module, { also_private => [ qr/^[A-Z_]+$/ ], });
}

Test::CPAN::Changes::changes_file_ok();

t/benchmark.t  view on Meta::CPAN

use Mojo::Base -strict;
use Test::More;
use Benchmark qw(cmpthese timeit timestr :hireswallclock);

plan skip_all => 'TEST_ONLINE=redis://localhost' unless $ENV{MOJO_REDIS_URL} = $ENV{TEST_ONLINE};
plan skip_all => 'TEST_BENCHMARK=500'            unless my $n_times          = $ENV{TEST_BENCHMARK};

my @classes   = qw(Mojo::Redis Mojo::Redis2);
my @protocols = qw(Protocol::Redis Protocol::Redis::Faster Protocol::Redis::XS);
my $key       = "test:benchmark:$0";
my %t;

for my $class (@classes) {
  eval "require $class;1" or next;

  for my $protocol (@protocols) {
    eval "require $protocol;1" or next;
    my $redis = $class->new->protocol_class($protocol);

    my ($bm, $lrange) = run($redis->isa('Mojo::Redis2') ? $redis : $redis->db, $protocol);
    is_deeply $lrange, [reverse 0 .. $n_times - 1], sprintf '%s/%s %s', ref $redis, $protocol, timestr $bm;

    my $bm_key = join '/', $redis->isa('Mojo::Redis2') ? 'Redis2' : 'Redis',
      $protocol =~ m!Protocol::Redis::(\w+)! ? $1 : 'PP';
    $t{$bm_key} = $bm;
  }
}

compare(qw(Redis/Faster Redis2/Faster));
compare(qw(Redis/Faster Redis/PP));
cmpthese(\%t) if $ENV{HARNESS_IS_VERBOSE};

done_testing;

sub compare {
  my ($an, $bn) = @_;
  return diag "Cannot compare $an and $bn" unless my $ao = $t{$an} and my $bo = $t{$bn};
  ok $ao->cpu_a <= $bo->cpu_a, sprintf '%s (%ss) is not slower than %s (%ss)', $an, $ao->cpu_a, $bn, $bo->cpu_a;
}

sub run {
  my $db = shift;

  $db->del($key);

  my ($lpush, $lrange);
  my $i  = 0;
  my $bm = timeit(
    $n_times,
    sub {
      $lpush = $db->lpush($key => $i++);
      $lrange = $db->lrange($key => 0, -1);
    }
  );

  $db->del($key);

  return $bm, $lrange;
}

t/cache-offline.t  view on Meta::CPAN

BEGIN { $ENV{MOJO_REDIS_CACHE_OFFLINE} = 1 }
use Mojo::Base -strict;
use Test::More;
use Mojo::Redis;

my $redis = Mojo::Redis->new;
my $cache = $redis->cache(namespace => $0);
my $n     = 0;
my $res;

for (1 .. 2) {
  $cache->memoize_p(main => 'cache_me', [{foo => 42}])->then(sub { $res = shift })->wait;
  is_deeply $res, {foo => 42}, 'memoize cache_me with hash';
}

is $n, 1, 'compute only called once per key';

$cache->refresh(1)->memoize_p(main => 'cache_me', [{foo => 42}])->then(sub { $res = shift })->wait;
is $n, 2, 'compute called after refresh()';

$cache->compute_p('some:die:key', sub { die 'oops!' })->catch(sub { $res = shift })->then(sub { $res = shift })->wait;
like $res, qr{oops!}, 'failed to cache';

{
  no warnings 'redefine';
  local *Mojo::Redis::Cache::_time = sub { time + 601 };
  $cache->refresh(0)->memoize_p(main => 'cache_me', [{foo => 42}])->then(sub { $res = shift })->wait;
  is $n, 3, 'compute called after expired';
}

done_testing;

sub cache_me {
  $n++;
  return $_[1] || 'default value';
}

t/cache.t  view on Meta::CPAN

use Mojo::Base -strict;
use Test::More;
use Mojo::Redis;

plan skip_all => 'TEST_ONLINE=redis://localhost' unless $ENV{TEST_ONLINE};

my $redis      = Mojo::Redis->new($ENV{TEST_ONLINE});
my $cache      = $redis->cache(namespace => $0);
my $n_computed = 0;
my $res;

cleanup();

for my $i (1 .. 2) {
  note "run $i";
  $cache->compute_p(
    'some:key',
    60.7,
    sub {
      $n_computed++;
      my $p = Mojo::Promise->new;
      Mojo::IOLoop->timer(0.1 => sub { $p->resolve('some data') });
      return $p;
    }
  )->then(sub { $res = shift })->wait;

  is $res, 'some data', 'computed some:key';

  $cache->compute_p('some:other:key', sub { $n_computed++; +{some => "data"} })->then(sub { $res = shift })->wait;
  is_deeply $res, {some => 'data'}, 'computed some:other:key';

  $cache->memoize_p(main => 'cache_me', [42], 5)->then(sub { $res = shift })->wait;
  is_deeply $res, 42, 'memoize cache_me with 42';

  $cache->memoize_p(main => 'cache_me')->then(sub { $res = shift })->wait;
  is_deeply $res, 'default value', 'memoize cache_me with default';

  $cache->memoize_p(main => 'cache_me', 30)->then(sub { $res = shift })->wait;
  is_deeply $res, 'default value', 'memoize cache_me with default';

  $cache->memoize_p(main => 'cache_me', [{foo => 42}])->then(sub { $res = shift })->wait;
  is_deeply $res, {foo => 42}, 'memoize cache_me with hash';

  $cache->compute_p('some:negative:key', -5.2, sub { $n_computed++; 'too cool' })->then(sub { $res = [@_] })->wait;
  is_deeply $res, ['too cool', $i == 1 ? {computed => 1} : {expired => 0}], 'compute_p with negative expire';
}

is $n_computed, 6, 'compute only called once per key';

note 'refresh';
$cache->refresh(1)->memoize_p(main => 'cache_me', [{foo => 42}])->then(sub { $res = shift })->wait;
is $n_computed, 7, 'compute called after refresh()';

note 'exception';
$cache->compute_p('some:die:key', sub { die 'oops!' })->then(sub { $res = shift })->catch(sub { $res = shift })->wait;
like $res, qr{oops!}, 'failed to cache';

note 'stale cache';
Mojo::Util::monkey_patch('Mojo::Redis::Cache', _time => sub { Time::HiRes::time() + 5.2 });
$cache->refresh(0);
$cache->compute_p('some:negative:key', -5.2, sub { die "yikes!\n" })->then(sub { $res = [@_] })
  ->catch(sub { $res = shift })->wait;
is_deeply $res, ['too cool', {error => "yikes!\n", expired => 1}], 'compute_p expired data and error';

note 'refreshed cache';
$cache->compute_p('some:negative:key', -5.2, sub { $n_computed++; 'cool2' })->then(sub { $res = [@_] })
  ->catch(sub { $res = shift })->wait;
is_deeply $res, ['cool2', {computed => 1, expired => 1}], 'compute_p expired data';

cleanup();
done_testing;

sub cache_me {
  $n_computed++;
  return $_[1] || 'default value';
}

sub cleanup {
  $redis->db->del(
    map {"$0:$_"} 'some:key',
    'some:die:key', 'some:negative:key', 'some:other:key', '@M:main:cache_me:[]', '@M:main:cache_me:[42]',
    '@M:main:cache_me:[{"foo":42}]',
  );
}

t/connection-auth.t  view on Meta::CPAN

use Mojo::Base -strict;
use Test::More;
use Mojo::Redis;

my $port = Mojo::IOLoop::Server->generate_port;
Mojo::IOLoop->server({port => $port}, sub { });

my $redis = Mojo::Redis->new("redis://whatever:s3cret\@localhost:$port/12");
is $redis->db->connection->url->port, $port, 'port';
is $redis->db->connection->url->password, 's3cret', 'password';

my @write;
$redis->on(connection => sub { my ($redis, $conn) = @_; @write = @{$conn->{write}} });

my $db = $redis->db;
my $err;
$db->connection->once(connect => sub { $err = $_[1]; Mojo::IOLoop->stop });
$db->connection->_connect;
Mojo::IOLoop->start;
is_deeply \@write, [["*2\r\n\$4\r\nAUTH\r\n\$6\r\ns3cret\r\n"], ["*2\r\n\$6\r\nSELECT\r\n\$2\r\n12\r\n"],],
  'write queue';

done_testing;

t/connection-lost.t  view on Meta::CPAN

use Mojo::Base -strict;
use Test::More;
use Mojo::Redis;
use Errno qw(ECONNREFUSED ENOTCONN);

# Dummy server
my $port      = Mojo::IOLoop::Server->generate_port;
my $server_id = make_server(Mojo::IOLoop->singleton);
my $redis     = Mojo::Redis->new("redis://localhost:$port");
my $err;

note 'Promises should be rejected on error';
my $db = $redis->db;
Mojo::IOLoop->next_tick(sub { $db->connection->disconnect });
get_p($db)->wait;
is $err, 'Premature connection close', 'client disconnected';

$err = '';
get_p($redis->db)->wait;
is $err, 'Premature connection close', 'server closed stream';

my $err_re = join '|', map { local $! = $_; quotemeta "$!" } ECONNREFUSED, ENOTCONN;
$err = '';
Mojo::IOLoop->remove($server_id);
get_p($redis->db)->wait;

{
  local $TODO = $err =~ /$err_re/ ? '' : "server most likely disappeared ($@)";
  like $err, qr/$err_re/, 'server disappeared';
}

note 'Do not reconnect in the middle of a transaction';
$server_id = make_server($redis->_blocking_connection->ioloop);
$db        = $redis->db;
my $step = 0;
my @err;
for my $m (qw(multi incr incr exec)) {
  eval { $db->$m($m eq 'incr' ? ($0) : ()); ++$step } or do { push @err, $@ };
  note "($step) $@" if $@;
}

is $step, 1, 'all blocking methods fail after the first fail';
like shift(@err), qr{^$_}, "expected $_"
  for 'Premature connection close', 'Redis server has gone away', 'Redis server has gone away';
isnt $redis->_blocking_connection, $db->connection(1), 'fresh connection next time';
is $redis->_blocking_connection->ioloop, $db->connection(1)->ioloop, 'same blocking ioloop';

note 'No blocking connection should be put back into connection queue';
$db = $redis->db;
$db->connection(1)->{stream} = 1;    # pretend we are connected
undef $db;
ok !(grep { warn $_; $_->ioloop ne Mojo::IOLoop->singleton } @{$redis->{queue}}), 'no blocking connections in queue';

done_testing;

sub get_p {
  return shift->get_p($0)->then(sub { diag "Should not be successfule: @_" })->catch(sub { $err = shift });
}

sub make_server {
  return shift->server(
    {port => $port},
    sub {
      my ($loop, $stream) = @_;
      $stream->on(
        read => sub {
          my ($stream, $buf) = @_;
          return $stream->write("+OK\r\n") if $buf =~ /EXEC/;    # Should not come to this
          return $stream->write("+OK\r\n") if $buf =~ /MULTI/;
          return $stream->close;
        }
      );
    }
  );
}

t/connection-sentinel.t  view on Meta::CPAN

use Mojo::Base -strict;
use Test::More;
use Mojo::Redis;

my $port   = Mojo::IOLoop::Server->generate_port;
my $redis  = Mojo::Redis->new("redis://whatever:s3cret\@mymaster/12?sentinel=localhost:$port&sentinel=localhost:$port");
my $conn_n = 0;
my @messages;

Mojo::IOLoop->server(
  {port => $port},
  sub {
    my ($loop, $stream) = @_;
    my $protocol = $redis->protocol_class->new(api => 1);
    my @res      = ({type => '$', data => 'OK'});

    push @res,
      $conn_n == 0
      ? {type => '$', data => 'IDONTKNOW'}
      : {type => '*', data => [{type => '$', data => 'localhost'}, {type => '$', data => $port}]};

    push @res, {type => '$', data => 42};

    my $cid = ++$conn_n;
    $protocol->on_message(sub {
      push @messages, pop;
      $messages[-1]{c} = $cid;
      $stream->write($protocol->encode(shift @res)) if @res;
    });

    $stream->on(read => sub { $protocol->parse(pop) });
  }
);

my $foo;
$redis->db->get_p('foo')->then(sub { $foo = shift })->wait;
is $foo, 42, 'get foo';

my @get_master_addr_by_name = (
  {data => 'SENTINEL',                type => '$'},
  {data => 'get-master-addr-by-name', type => '$'},
  {data => 'mymaster',                type => '$'},
);

is_deeply(
  \@messages,
  [
    {c => 1, data => [{data => 'AUTH', type => '$'}, {data => 's3cret', type => '$'}], type => '*'},
    {c => 1, data => \@get_master_addr_by_name,                                        type => '*'},
    {c => 2, data => [{data => 'AUTH', type => '$'}, {data => 's3cret', type => '$'}], type => '*'},
    {c => 2, data => \@get_master_addr_by_name,                                        type => '*'},
    {c => 3, data => [{data => 'AUTH', type => '$'}, {data => 's3cret', type => '$'}], type => '*'},
    {c => 3, data => [{data => 'SELECT', type => '$'}, {data => '12', type => '$'}],   type => '*'},
    {c => 3, data => [{data => 'GET', type => '$'}, {data => 'foo', type => '$'}],     type => '*'},
  ],
  'discovery + connect + command'
) or diag explain \@messages;

done_testing;

t/connection-unix.t  view on Meta::CPAN

use Mojo::Base -strict;
use Test::More;
use Mojo::Redis;

my $url   = Mojo::URL->new->host('/tmp/redis.sock');
my $redis = Mojo::Redis->new($url);
my $args;

Mojo::Util::monkey_patch('Mojo::IOLoop::Client', 'connect' => sub { $args = $_[1] });
is $redis->db->connection->url->host, '/tmp/redis.sock', 'host';
is $redis->db->connection->url->port, undef,             'port';

$redis->db->connection->_connect;
is_deeply $args, {path => '/tmp/redis.sock', timeout => 10}, 'connect args';

done_testing;

t/connection.t  view on Meta::CPAN

use Mojo::Base -strict;
use Test::More;
use Mojo::Redis;

plan skip_all => 'TEST_ONLINE=redis://localhost/8'      unless $ENV{TEST_ONLINE};
plan skip_all => 'Need a database index in TEST_ONLINE' unless $ENV{TEST_ONLINE} =~ m!/\d+\b!;

my $redis = Mojo::Redis->new($ENV{TEST_ONLINE});

my $db   = $redis->db;
my $conn = $db->connection;
isa_ok($db,   'Mojo::Redis::Database');
isa_ok($conn, 'Mojo::Redis::Connection');

$redis->on(connection => sub { $redis->{connections}++ });

note 'Create one connection';
my $connected = 0;
my $err;
$conn->once(connect => sub { $connected++; Mojo::IOLoop->stop });
$conn->once(error => sub { $err = $_[1]; Mojo::IOLoop->stop });
is $conn->_connect, $conn, '_connect()';
Mojo::IOLoop->start;
is $connected, 1, 'connected' or diag $err;
is @{$redis->{queue} || []}, 0, 'zero connections in queue';

note 'Put connection back into queue';
undef $db;
is @{$redis->{queue}}, 1, 'one connection in queue';

note 'Create more connections than max_connections';
my @db;
push @db, $redis->db for 1 .. 6;    # one extra
$_->connection->_connect->once(connect => sub { ++$connected == 6 and Mojo::IOLoop->stop }) for @db;
Mojo::IOLoop->start;

note 'Put max connections back into the queue';
is $db[0]->connection, $conn, 'reusing connection';
@db = ();
is @{$redis->{queue}}, 5, 'five connections in queue';

note 'Take one connection out of the queue';
$redis->db->connection->disconnect;
undef $db;
is @{$redis->{queue}}, 4, 'four connections in queue';

note 'Write and auto-connect';
my @res;
delete $redis->{queue};
$db = $redis->db;
$conn->write_p('PING')->then(sub { @res = @_; Mojo::IOLoop->stop })->wait;
is_deeply \@res, ['PONG'], 'ping response';

note 'New connection, because disconnected';
$conn = $db->connection;
$conn->disconnect;
$db = $redis->db;
$db->connection->write_p('PING')->wait;
isnt $db->connection, $conn, 'new connection when disconnected';

is $redis->{connections}++, 7, 'connections emitted';

note 'Encoding';
my $str = 'I ♥ Mojolicious!';
$conn = $db->connection;

is $redis->encoding, 'UTF-8', 'default redis encoding';
is $conn->encoding,  'UTF-8', 'encoding passed on to connection';
$conn->write_p(qw(get t:redis:encoding))->then(sub { @res = @_ })->wait;
is_deeply \@res, [undef], 'undefined key not decoded';
$conn->write_p(qw(set t:redis:encoding), $str)->wait;
$conn->write_p(qw(get t:redis:encoding))->then(sub { @res = @_ })->wait;
is_deeply \@res, [$str], 'unicode encoding';

$conn->encoding(undef);
$conn->write_p(qw(set t:redis:encoding), Mojo::Util::encode('UTF-8', $str))->wait;
$conn->encoding('UTF-8');
$conn->write_p(qw(get t:redis:encoding))->then(sub { @res = @_ })->wait;
is $res[0], $str, 'no encoding';

note 'Make sure encoding is reset';
$db = $redis->db;
$db->connection->encoding('whatever');
undef $db;
$db = $redis->db;
is $db->connection->encoding, 'UTF-8', 'connection encoding is reset';

note 'Cleanup';
$conn->write_p(qw(del t:redis:encoding))->wait;

$redis->encoding(undef);
is $redis->db->connection->encoding, undef, 'Encoding changed for new connections';

note 'Fork-safety';
$conn = $db->connection;
undef $db;
$redis->{pid} = -1;
isnt $redis->db->connection, $conn, 'new fork gets a new connecion';
undef $conn;
$redis->{pid} = $$;
$conn         = $redis->_blocking_connection;
$redis->{pid} = -1;
isnt $redis->_blocking_connection, $conn, 'new fork gets a new blocking connection';
undef $conn;
$redis->{pid} = $$;

note 'Connection closes when ref is lost';
$db = $redis->db;
$db->get_p($0)->catch(sub { $err = shift })->wait;    # Make sure we are connected
ok $db->connection->is_connected, 'connected' or diag $err;
my $closed;
$db->connection->on(close => sub { $closed++ });
$redis->max_connections(0);
undef $db;
ok $closed, 'connection was closed on destruction';

note 'New connection, because URL changed';
use Socket;
my $host = $redis->url->host;
$db = $redis->db;
$db->get_p($0)->catch(sub { $err = shift })->wait;    # Make sure we are connected
$redis->url->host($host =~ /[a-z]/ ? inet_ntoa(inet_aton $host) : gethostbyaddr(inet_aton($host), AF_INET));
note 'Changed host to ' . $redis->url->host;
$db = undef;
is @{$redis->{queue}}, 0, 'database was not enqued' or diag $err;

note 'Blocking connection';
$db = $redis->db;
isnt $db->connection(1)->ioloop, Mojo::IOLoop->singleton, 'blocking connection';
isnt $db->connection(1)->ioloop, $db->connection(0)->ioloop,
  'blocking connection does not share non-blocking connection ioloop';

done_testing;

t/cursor.t  view on Meta::CPAN

use Mojo::Base -strict;
use Test::More;
use Mojo::Redis;

use constant ELEMENTS_COUNT => $ENV{REDIS_TEST_ELEMENTS_COUNT} || 1000;

plan skip_all => 'TEST_ONLINE=redis://localhost' unless $ENV{TEST_ONLINE};

my $redis = Mojo::Redis->new($ENV{TEST_ONLINE});
my $db    = $redis->db;
my ($cursor, $expected, $guard, $res);

cleanup();

# Constructor
$cursor = $redis->cursor;
is_deeply $cursor->command, [scan => 0], 'default cursor command';
$cursor = $redis->cursor(scan => 0, match => '*', count => 100);
is_deeply $cursor->command, [scan => 0, match => '*', count => 100], 'scan, match and count';

note 'Reset cursor';
$cursor->command->[1] = 32;
$cursor->{finished} = 1;
$cursor->again;
is $cursor->command->[1], 0, 'cursor is reset';
ok !$cursor->finished, 'finished is reset';

$db->set("redis:scan_test:key$_", $_) for 1 .. ELEMENTS_COUNT;
$expected = [sort map {"redis:scan_test:key$_"} 1 .. ELEMENTS_COUNT];

note 'SCAN';
$cursor = $redis->cursor(scan => 0, match => 'redis:scan_test:key*');
$guard  = 10000;
$res    = [];
while ($guard-- && (my $r = $cursor->next)) { push @$res, @$r }
is_deeply [sort @$res], $expected, 'scan next() blocking';
ok $cursor->finished, 'finished is set';

$res = [];
$cursor->again->all(sub { Mojo::IOLoop->stop; $res = [$_[1], @{$_[2]}] });
Mojo::IOLoop->start;
is_deeply [sort @$res], ['', @$expected], 'all(CODE)';

$res = [];
$cursor->again->all_p->then(sub { $res = shift })->wait;
is_deeply [sort @$res], $expected, 'all_p()';

$cursor = $redis->cursor(keys => 'redis:scan_test:key*');
is_deeply [sort @{$cursor->all}], $expected, 'keys';

note 'HSCAN';
$db->hset('redis:scan_test:hash', "key.$_" => "val.$_") for 1 .. ELEMENTS_COUNT;
$cursor = $redis->cursor(hgetall => 'redis:scan_test:hash');
$cursor->next_p->then(sub { $res = $_[0] })->wait;
my @keys = keys %$res;
my @vals = values %$res;
like $keys[0], qr{^key\.\d+$}, 'hgetall next_p() keys';
like $vals[0], qr{^val\.\d+$}, 'hgetall next_p() vals';
is_deeply($cursor->all, $db->hgetall('redis:scan_test:hash'), 'hgetall');

$cursor = $redis->cursor(hkeys => 'redis:scan_test:hash');
is_deeply([sort @{$cursor->all}], [sort @{$db->hkeys('redis:scan_test:hash')}], 'hkeys');

note 'SSCAN';
$db->sadd('redis:scan_test:set', $_) for 1 .. ELEMENTS_COUNT;
$cursor = $redis->cursor(smembers => 'redis:scan_test:set');
is_deeply([sort @{$cursor->all}], [sort @{$db->smembers('redis:scan_test:set')}], 'smembers');

cleanup();
done_testing;

sub cleanup {
  $db->del("redis:scan_test:key$_", $_) for 1 .. ELEMENTS_COUNT;
  $db->del(qw(redis:scan_test:hash redis:scan_test:set redis:scan_test:zset));
}

t/db.t  view on Meta::CPAN

use Mojo::Base -strict;
use Test::More;
use Mojo::Redis;

plan skip_all => 'TEST_ONLINE=redis://localhost' unless $ENV{TEST_ONLINE};

my $redis = Mojo::Redis->new($ENV{TEST_ONLINE});
my $db    = $redis->db;
my ($res, @res);

# SET
$db = $redis->db;
$db->set($0 => 123, sub { @res = @_; Mojo::IOLoop->stop });
Mojo::IOLoop->start;
is_deeply \@res, [$db, '', 'OK'], 'set';

# GET
$db->get($0 => sub { @res = @_; Mojo::IOLoop->stop });
Mojo::IOLoop->start;
is_deeply \@res, [$db, '', '123'], 'get';

$db->get_p($0)->then(sub { @res = (then => @_) })->catch(sub { @res = (catch => @_) })->wait;
is_deeply \@res, [then => '123'], 'get_p';

# DEL
is_deeply $db->del($0), 1, 'blocking del';

# BLPOP
@res = ();
$db->del_p('some:empty:list', $0);
$db->lpush_p($0 => '456')->then(gather_cb('then'))->catch(gather_cb('catch'));
$db->blpop_p('some:empty:list', $0, 2)->then(gather_cb('popped'))->wait;
is_deeply \@res, ["then: 1", "popped: 456 $0"], 'blpop_p' or diag join ', ', @res;

# HASHES
$db->hmset_p($0, a => 11, b => 22);
$db->hgetall_p($0)->then(sub { $res = shift })->wait;
is_deeply $res, {a => 11, b => 22}, 'hgetall_p';

# Custom command
$db->call_p(HGETALL => $0)->then(sub { $res = [@_] })->wait;
is_deeply [$db->call(HGETALL => $0)], $res, 'call_p() == call()';

$res = $db->hkeys($0);
is_deeply $res, [qw(a b)], 'hkeys';

ok $db->info_structured('memory')->{maxmemory_human}, 'got info_structured';
$db->info_structured_p->then(sub { $res = shift })->wait;
ok $res->{clients}{connected_clients}, 'got info_structured for all sections, clients';
ok $res->{memory}{maxmemory_human},    'got info_structured for all sections, memory';

done_testing;

sub gather_cb {
  my $prefix = shift;
  return sub { push @res, "$prefix: @_" };
}

t/geo.t  view on Meta::CPAN

use Mojo::Base -strict;
use Test::More;
use Mojo::Redis;

plan skip_all => 'TEST_ONLINE=redis://localhost' unless $ENV{TEST_ONLINE};
plan skip_all => 'cpanm Test::Deep' unless eval 'require Test::Deep;1';

Test::Deep->import;

my $redis = Mojo::Redis->new($ENV{TEST_ONLINE});
my $db    = $redis->db;
my $key   = "places:$0";
my $res;

$db->geoadd($key => 13.361389, 38.115556, 'Palermo', 15.087269, 37.502669, 'Catania');

is $db->geodist($key => qw(Palermo Catania)), 166274.1516, 'geodist';

is_deeply $db->georadius($key => qw(15 37 100 km)), ['Catania'], 'georadius 100km';
is_deeply $db->georadius($key => qw(15 37 200 km)), ['Palermo', 'Catania'], 'georadius 200km';

my $tol = 0.00001;
cmp_deeply(
  $db->geopos($key => qw(Catania NonExisting Palermo)),
  [
    {lat => num(37.502669, $tol), lng => num(15.087269, $tol)},
    undef,
    {lat => num(38.115556, $tol), lng => num(13.361389, $tol)},
  ],
  'geopos'
);

$db->del($key);

done_testing;

t/keyspace-listen.t  view on Meta::CPAN

use Mojo::Base -strict;
use Test::More;
use Mojo::Redis;

my @events;
my $redis  = Mojo::Redis->new($ENV{TEST_ONLINE} || 'redis://localhost');
my $pubsub = $redis->pubsub;

my $dbsel = $redis->url->path->[0] // '*';
is $pubsub->_keyspace_key, '__keyevent@'.$dbsel.'__:*', 'keyevent default db wildcard';

$redis->url->path->parse('/5');
is $pubsub->_keyspace_key, '__keyevent@5__:*', 'keyevent default wildcard';
is $pubsub->_keyspace_key({type => 'key*'}), '__key*@5__:*', 'keyboth wildcard listen';
is $pubsub->_keyspace_key(foo => undef), '__keyspace@5__:foo', 'keyspace foo';
is $pubsub->_keyspace_key(undef, 'del'), '__keyevent@5__:del', 'keyevent del';
is $pubsub->_keyspace_key('foo', 'rename', {db => 1, key => 'x', op => 'y'}), '__keyspace@1__:foo',
  'keyspace foo and db';
is $pubsub->_keyspace_key({db => 0, key => 'foo', type => 'key*'}), '__key*@0__:foo', 'key* db and type';

my $cb = $pubsub->keyspace_listen(undef, 'del', {db => 1}, sub { });
is ref($cb), 'CODE', 'keyspace_listen returns callback';
is_deeply $pubsub->{chans}{'__keyevent@1__:del'}, [$cb], 'callback is set up';
is $pubsub->keyspace_unlisten(undef, 'del', {db => 1}, $cb), $pubsub, 'keyspace_unlisten with callback';
ok !$pubsub->{chans}{'__keyevent@1__:del'}, 'callback is removed';
$pubsub->{chans}{'__keyevent@1__:del'} = [$cb];
is $pubsub->keyspace_unlisten(undef, 'del', {db => 1}), $pubsub, 'keyspace_unlisten without callback';
ok !$pubsub->{chans}{'__keyevent@1__:del'}, 'callback is removed';

if ($ENV{TEST_KEA} && $ENV{TEST_ONLINE}) {
  my $kea = $redis->db->config(qw(get notify-keyspace-events))->[1];
  diag "config get notify-keyspace-events == $kea";
  $redis->db->config(qw(set notify-keyspace-events KEA));

  $redis->pubsub->keyspace_listen(\&gather);
  $redis->pubsub->keyspace_listen({type => 'keyspace'}, \&gather);
  Mojo::IOLoop->timer(0.15 => sub { Mojo::IOLoop->stop });
  Mojo::IOLoop->start;

  my $key = 'mojo:redis:test:keyspace:listen';
  $redis->db->tap(set => $key => __FILE__)->del($key => __FILE__);
  Mojo::IOLoop->start;
  $redis->db->config(qw(set notify-keyspace-events), $kea);

  ok + (grep { $_->[1] eq 'del' } @events), 'keyspace del event';
  ok + (grep { $_->[1] eq 'set' } @events), 'keyspace set event';
  ok + (grep { $_->[0] =~ /:set$/ } @events), 'keyevent set event';
  ok + (grep { $_->[0] =~ /:del$/ } @events), 'keyevent del event';
}

done_testing;

sub gather {
  push @events, $_[1];
  Mojo::IOLoop->stop if @events >= 4;
}



( run in 0.978 second using v1.01-cache-2.11-cpan-3989ada0592 )