Redis::Promise

This Ruby gem adds promises built on Redis. It enables you to resolve or reject a redis promise based on a key and await the result in another thread or on another machine/service.

It can be used in tandem with job frameworks like resque or sidekiq in order to get back a result from an asynchronous task.

Installation

Install the gem and add to the application's Gemfile by executing:

bundle add redis-promise

If bundler is not being used to manage dependencies, install the gem by executing:

gem install redis-promise

Usage

This library adds two basic types Redis::Promise and Redis::Promise::Resolver. The Promise object can be used to await a resolved (success) value or a rejected (error) value. The Resolver object can be used to resolve or reject a promise. These two objects are tied together by a common redis key where the result gets stored.

The basic idea is that you use a Redis::Promise::Resolver to resolve/reject the promise in one context eg. a thread or a different service. You then use Redis::Promise in another context to get (await) the value.

Threads

Using the Redis::Promise::create helper to create a promise and its resolver at the same time.

require 'redis/promise'

redis = Redis.new
promise, resolver = Redis::Promise.create(redis)

threads = []
threads << Thread.new do
  sleep(3)
  resolver.resolve("OK!")
end

threads << Thread.new do
  result = promise.await #=> "OK!"
end

threads.each(&:join)
# should print "OK!" after 3 seconds

Creating a separate promise and resolver. Notice that the promise and resolver have to get their own Redis connection instances, that's because the promise.await blocks the entire redis connection making it impossible to resolve the promise using the same connection.

require 'redis/promise'

redis = Redis.new
threads = []

promise = Redis::Promise.new(redis)
threads << Thread.new do
  puts promise.await #=> "OK!"
end

resolver = Redis::Promise::Resolver.new(redis, key: promise.key)
threads << Thread.new do
  sleep(3)
  resolver.resolve("OK!")
end

threads.each(&:join)

Resque

You could use it in resque or any other job system to get the result from an asynchronous task.

# ========
# job definition

class SomeJob
  @queue = :example

  def self.perform(promise_key)
    resolver = Redis::Promise::Resolver.new(
      Resque.redis,
      key: promise_key,
    )
    resolver.resolve(42)
  end
end

# ========
# some other place in the app

promise = Redis::Promise.new(Resque.redis)
Resque.enqueue(SomeJob, promise.key)

promise.await #=> 42
# you got a value back from a resque job!

Plugin

There is an additional resque plugin you can load to make it more convenient.

require 'redis/promise/resque'

class SomeJob
  include Redis::Promise::Resque

  @queue = :example

  run do |n|
    raise ArgumentError, "number must be positive: #{n}" if n < 0

    n * 69
  end
end

# enqueueing returns a promise
promise = SomeJob.enqueue(42)
promise.await #=> 2898

# errors thrown in the job get caught and rethrown on await
promise = SomeJob.enqueue(-2)
promise.await
#! Redis::Promise::RejectedError(value: "[ArgumentError]: number must be positive: -2")

Development

After checking out the repo, run bin/setup to install dependencies. Then, run rake test to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.

To install this gem onto your local machine, run bundle exec rake install. To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and the created tag, and push the .gem file to rubygems.org.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/Verseth/ruby-redis-promise.