From 34f9d05c4db5c535e27e64f1dba12e3d042de383 Mon Sep 17 00:00:00 2001 From: "nicholas a. evans" Date: Wed, 28 Oct 2015 11:49:28 -0400 Subject: [PATCH] ConfigLoaders::Redis: basic redis-backed configuration Intended to be wrapped by `Throttled` config loader. n.b. if you use this, you will need to reset the redis configuration in your `after_prefork` hook. (Until #135 handles it automatically.) --- lib/resque/pool/config_loaders.rb | 1 + lib/resque/pool/config_loaders/redis.rb | 65 ++++++++++++++ spec/config_loaders/redis_spec.rb | 110 ++++++++++++++++++++++++ 3 files changed, 176 insertions(+) create mode 100644 lib/resque/pool/config_loaders/redis.rb create mode 100644 spec/config_loaders/redis_spec.rb diff --git a/lib/resque/pool/config_loaders.rb b/lib/resque/pool/config_loaders.rb index 5032db4..cfdb6c4 100644 --- a/lib/resque/pool/config_loaders.rb +++ b/lib/resque/pool/config_loaders.rb @@ -5,6 +5,7 @@ class Pool module ConfigLoaders autoload :FileOrHashLoader, "resque/pool/config_loaders/file_or_hash_loader" + autoload :Redis, "resque/pool/config_loaders/redis" autoload :Throttled, "resque/pool/config_loaders/throttled" end diff --git a/lib/resque/pool/config_loaders/redis.rb b/lib/resque/pool/config_loaders/redis.rb new file mode 100644 index 0000000..f54e54b --- /dev/null +++ b/lib/resque/pool/config_loaders/redis.rb @@ -0,0 +1,65 @@ +require "resque" +require "resque/pool" + +module Resque + class Pool + module ConfigLoaders + + # Read/write pool config from redis. + # Should be wrapped in +ConfigLoaders::Throttled+. + # + # n.b. The environment needs to be passed in up-front, and will be ignored + # during +call+. + class Redis + attr_reader :redis + attr_reader :app, :pool, :env, :name + + def initialize(app_name: Pool.app_name, + pool_name: Pool.pool_name, + environment: "unknown", + config_name: "config", + redis: Resque.redis) + @app = app_name + @pool = pool_name + @env = environment + @name = config_name + @redis = redis + end + + # n.b. environment must be set up-front and will be ignored here. + def call(_) + redis.hgetall(key).tap do |h| + h.each do |k,v| + h[k] = v.to_i + end + end + end + + # read individual worker config + def [](worker) + redis.hget(key, worker).to_i + end + + # write individual worker config + def []=(worker, count) + redis.hset(key, worker, count.to_i) + end + + # remove worker config + def delete(worker) + redis.multi do + redis.hget(key, worker) + redis.hdel(key, worker) + end.first.to_i + end + + # n.b. this is probably namespaced under +resque+ + def key + @key ||= ["pool", "config", app, pool, env, name].join(":") + end + + end + + end + end +end diff --git a/spec/config_loaders/redis_spec.rb b/spec/config_loaders/redis_spec.rb new file mode 100644 index 0000000..6713c00 --- /dev/null +++ b/spec/config_loaders/redis_spec.rb @@ -0,0 +1,110 @@ +require 'spec_helper' +require 'resque/pool/config_loaders/redis' + +module Resque::Pool::ConfigLoaders + + describe Redis do + before(:each) do + Resque.redis.flushdb + expect(Resque.redis.keys.count).to eq(0) + end + + after(:all) do + Resque.redis.flushdb + end + + subject(:config) { Redis.new(environment: env) } + subject(:env) { "prd" } + + describe "initialization" do + it "uses default app_name and pool_name from Resque::Pool" do + expect(Redis.new.app).to eq(Resque::Pool.app_name) + expect(Redis.new.pool).to eq(Resque::Pool.pool_name) + end + it "uses default 'unknown' environment" do + expect(Redis.new.env).to eq("unknown") + end + it "uses default 'config' name" do + expect(Redis.new.name).to eq("config") + end + it "constructs redis key (expecting to be namespaced under resque)" do + config = Redis.new(app_name: "foo", + pool_name: "bar", + environment: "dev", + config_name: "override") + expect(config.key).to eq("pool:config:foo:bar:dev:override") + end + it "uses resque's redis connection (probably namespaced)" do + expect(Redis.new.redis).to eq(Resque.redis) + expect(Redis.new(redis: :another).redis).to eq(:another) + end + end + + describe "basic API" do + + it "starts out empty" do + expect(config.call(env)).to eq({}) + end + + it "has hash-like index setters" do + config["foo"] = 2 + config["bar"] = 3 + config["numbers_only"] = "elephant" + expect(config.call(env)).to eq({ + "foo" => 2, + "bar" => 3, + "numbers_only" => 0, + }) + end + + it "has indifferent access (but call returns string keys)" do + config[:foo] = 1 + config["foo"] = 2 + expect(config[:foo]).to eq(2) + expect(config.call(env)).to eq("foo" => 2) + end + + it "has hash-like index getters" do + config["foo"] = 86 + config["bar"] = 99 + expect(config["foo"]).to eq(86) + expect(config["bar"]).to eq(99) + expect(config["nonexistent"]).to eq(0) + end + + it "can remove keys (not just set them to zero)" do + config["foo"] = 99 + config["bar"] = 7 + expect(config.delete("foo")).to eq(99) + expect(config.call(env)).to eq("bar" => 7) + end + + end + + describe "persistance" do + + it "can be loaded from another instance" do + config["qA"] = 24 + config["qB"] = 33 + config2 = Redis.new environment: env + expect(config2.call(env)).to eq("qA" => 24, "qB" => 33) + end + + it "won't clash with different configs" do + config[:foo] = 1 + config[:bar] = 2 + config2 = Redis.new app_name: "another" + expect(config2.call(env)).to eq({}) + config3 = Redis.new pool_name: "another" + expect(config3.call(env)).to eq({}) + config4 = Redis.new config_name: "another" + expect(config4.call(env)).to eq({}) + config5 = Redis.new environment: "another" + expect(config5.call(env)).to eq({}) + end + + end + + end + +end