redis_directory 1.0.4

Sign up to get free protection for your applications and to get access to all the features.
data/README ADDED
@@ -0,0 +1,24 @@
1
+ = Redis::Directory
2
+
3
+ Redis::Directory assumes a Redis installation running on a default
4
+ port and database 0 that will contain connection information for various
5
+ other Redis databases. For example, if you were using a Redis database to
6
+ store the content of cached pages, and this was running on a cluster of
7
+ two Redis instances, with multiple applications connecting partitioned by
8
+ database, then your connection might look like this:
9
+
10
+ require "redis"
11
+ require "redis/distributed"
12
+
13
+ # The ACME Corp database is #27
14
+ cache = Redis::Distributed.new %w( redis://redis.example:4400/27 redis://redis.example:4401/27 )
15
+
16
+ Redis::Directory uses a centralized Redis database to store the
17
+ connection information so you don't have to remember "magic numbers" for
18
+ each client/database mapping, and can easily update port-numbers, hostnames
19
+ and cluster-members as necessary. The same connection with
20
+ Redis::Directory would look like this:
21
+
22
+ require "redis_directory"
23
+
24
+ cache = Redis::Directory.new("redis.example").get("cache", "acme")
data/Rakefile ADDED
@@ -0,0 +1,26 @@
1
+ require "rake"
2
+ require "rake/clean"
3
+ require "rake/testtask"
4
+ require "rake/rdoctask"
5
+
6
+ task :default => :test
7
+
8
+ Rake::TestTask.new(:test) do |t|
9
+ t.libs << "test"
10
+ t.test_files = FileList["test/**/*_test.rb"]
11
+ t.verbose = true
12
+ end
13
+
14
+ CLEAN.include ["*.gem", "rdoc"]
15
+ RDOC_OPTS = [ "--quiet", "--inline-source", "--line-numbers", "--title", "Redis Directory: A database connection manager for Redis", "--main", "README" ]
16
+
17
+ Rake::RDocTask.new do |rdoc|
18
+ rdoc.rdoc_dir = "rdoc"
19
+ rdoc.options += RDOC_OPTS
20
+ rdoc.rdoc_files.add %w"README MIT-LICENSE lib/redis_directory.rb"
21
+ end
22
+
23
+ desc "Package redis_directory"
24
+ task :package do
25
+ sh %{gem build redis_directory.gemspec}
26
+ end
@@ -0,0 +1,90 @@
1
+ require "redis"
2
+ require "redis/distributed"
3
+ require "json"
4
+
5
+ class Redis::Directory
6
+
7
+ SERVICES_KEY = "services"
8
+ DATABASES_KEY = "databases"
9
+
10
+ # Hard-coded, because I don't know how to get this from the Redis connection at runtime.
11
+ MAXIMUM_DATABASE_COUNT = 65535
12
+
13
+ # This error is thrown when you request a service that is not defined in the directory's "services" key.
14
+ class UndefinedServiceError < StandardError
15
+ def initialize(directory, service_name)
16
+ super "#{service_name} is not an available service! (#{directory.services.keys.sort})"
17
+ end
18
+ end
19
+
20
+ # If you are unable to reserve a database during a connection attempt, this error is raised.
21
+ class ReservationError < StandardError
22
+ def initialize(directory, service_name, connection_name)
23
+ super "Unable to reserve a database for #{service_name}:#{connection_name}!\nCurrent databases for service: #{directory.redis.hgetall("#{service_name}-service")}"
24
+ end
25
+ end
26
+
27
+ # You must provide the +connection_options+ to the directory server.
28
+ def initialize(connection_options)
29
+ @redis = Redis.connect(connection_options)
30
+ end
31
+
32
+ def services
33
+ if redis.exists(SERVICES_KEY)
34
+ redis.hgetall(SERVICES_KEY).inject({}) do |h,(k,v)|
35
+ h[k] = JSON.parse(v); h
36
+ end
37
+ else
38
+ {}
39
+ end
40
+ end
41
+
42
+ # This locates the next available database for a given service, starting at an index of 1.
43
+ # The 0 database is reserved in case you have a default connection, or local redis server
44
+ # (in which case 0 should be the directory database to avoid conflicts).
45
+ def next_db(service_name)
46
+ raise UndefinedServiceError.new(self, service_name) unless services.keys.include?(service_name)
47
+ databases = redis.hvals("#{service_name}-service").map { |i| i.to_i }.sort
48
+ (1..MAXIMUM_DATABASE_COUNT).select { |i| break i unless databases.include? i }
49
+ end
50
+
51
+ def reserve(service_name, connection_name)
52
+ new_db = nil
53
+ # redis.multi do
54
+ if redis.hexists("#{service_name}-service", connection_name)
55
+ new_db = redis.hget("#{service_name}-service", connection_name)
56
+ else
57
+ new_db = next_db(service_name)
58
+ redis.hset("#{service_name}-service", connection_name, new_db)
59
+ end
60
+ # end
61
+ new_db
62
+ end
63
+
64
+ def get(service_name, connection_name)
65
+ db = reserve(service_name, connection_name)
66
+ raise ReservationError.new(self, service_name, connection_name) if db.nil?
67
+
68
+ connection_list = services[service_name].map do |server|
69
+ if server =~ /^redis\:\/\//
70
+ "#{server}/#{db}"
71
+ else
72
+ "redis://#{server}/#{db}"
73
+ end
74
+ end
75
+ connection = nil
76
+
77
+ if connection_list.size == 1
78
+ connection = Redis.connect(:url => connection_list.first)
79
+ else
80
+ connection = Redis::Distributed.new(connection_list)
81
+ end
82
+
83
+ connection.set("connection-name", connection_name)
84
+ connection
85
+ end
86
+
87
+ def redis
88
+ @redis
89
+ end
90
+ end
@@ -0,0 +1,47 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = "redis_directory"
3
+ s.version = "1.0.4"
4
+ s.platform = Gem::Platform::RUBY
5
+ s.summary = "Redis Directory for retriving/storing redis connections in a central database."
6
+
7
+ s.description = <<-EOF
8
+ Redis::Directory assumes a Redis installation running on a default
9
+ port and database 0 that will contain connection information for various
10
+ other Redis databases. For example, if you were using a Redis database to
11
+ store the content of cached pages, and this was running on a cluster of
12
+ two Redis instances, with multiple applications connecting partitioned by
13
+ database, then your connection might look like this:
14
+
15
+ require "redis"
16
+ require "redis/distributed"
17
+
18
+ # The ACME Corp database is #27
19
+ cache = Redis::Distributed.new "redis://redis.example:4400/27", "redis://redis.example:4401/27"
20
+
21
+ Redis::Directory uses a centralized Redis database to store the
22
+ connection information so you don't have to remember "magic numbers" for
23
+ each client/database mapping, and can easily update port-numbers/hostnames,
24
+ cluster-members as necessary. The same connection with
25
+ Redis::Directory would look like this:
26
+
27
+ require "redis_directory"
28
+
29
+ cache = Redis::Directory.new("redis.example").connect("cache", "acme")
30
+ EOF
31
+
32
+ s.files = Dir[ "README", "lib/**/*", "test/**/*", "redis_directory.gemspec", "Rakefile" ]
33
+ s.require_path = "lib"
34
+ s.test_files = Dir["test/**/*_test.rb"]
35
+
36
+ s.author = "Sam Smoot"
37
+ s.email = "ssmoot@gmail.com"
38
+ s.homepage = "https://github.com/wiecklabs/redis_directory"
39
+ s.license = "MIT"
40
+
41
+ s.has_rdoc = true
42
+ s.rdoc_options = [ "--inline-source", "--line-numbers", "--title", "Redis Directory: A database connection manager for Redis", "README", "MIT-LICENSE", "lib" ]
43
+
44
+ s.add_dependency "redis"
45
+ s.add_dependency "minitest" if RUBY_VERSION =~ /1\.8/
46
+ s.add_dependency "json"
47
+ end
@@ -0,0 +1,129 @@
1
+ require File.dirname(__FILE__) + "/helper"
2
+
3
+ class ConnectionTest < MiniTest::Unit::TestCase
4
+
5
+ def setup
6
+ @directory = Redis::Directory.new(:url => (ENV["REDIS_DIRECTORY"] || "redis://localhost"))
7
+
8
+ @redis = @directory.redis
9
+ @redis.hset Redis::Directory::SERVICES_KEY, "cache", [ "redis://localhost" ].to_json
10
+ @redis.hset Redis::Directory::SERVICES_KEY, "sessions", [ "redis://localhost" ].to_json
11
+ @redis.hset Redis::Directory::SERVICES_KEY, "queue", [ "redis://localhost" ].to_json
12
+ end
13
+
14
+ def teardown
15
+ @redis.flushall
16
+ @redis.quit
17
+ end
18
+
19
+ # Our first test verifies that the "services" key (populated here in #setup) is not empty.
20
+ # Since we can't know the hostname(s) and ports for the Redis members you've setup, you
21
+ # must pre-populate this.
22
+ # This can be as easy as using the redis-cli on the command-line to populate your directory:
23
+ #
24
+ # redis-cli -h redis.example.com -p 8440 hset services cache '["redis.example.com:9100", "redis.example.com:9101"]'
25
+ # redis-cli -h redis.example.com -p 8440 hset services sessions '["redis.example.com:9200", "redis.example.com:9201"]'
26
+ # redis-cli -h redis.example.com -p 8440 hset services queue '["redis.example.com:9300"]'
27
+ #
28
+ # Then +@directory.services+ would be equal to:
29
+ #
30
+ # {
31
+ # "cache" => ["redis.example.com:9100", "redis.example.com:9101"],
32
+ # "sessions" => ["redis.example.com:9200", "redis.example.com:9201"],
33
+ # "queue" => ["redis.example.com:9300"]
34
+ # }
35
+ #
36
+ # These are the definitions for your cluster(s) of services.
37
+ # It's recommended that you dedicate individual use-cases to individual clusters.
38
+ # This helps you balance your loads more evenly across multi-core machines
39
+ # (since Redis is single-threaded) as well as allowing you to tune memory limits,
40
+ # flush-to-disk intervals, etc on a per-cluster basis.
41
+ def test_services_are_staticly_assigned
42
+ refute_empty @directory.services
43
+ end
44
+
45
+ def test_services_is_a_hash_of_connection_strings
46
+ assert_kind_of Hash, @directory.services
47
+ end
48
+
49
+ # Because we use a Redis::Distributed connection, you should consistently
50
+ # use Arrays for your cluster connections even if there is only one item
51
+ # in the cluster.
52
+ def test_service_value_is_an_array
53
+ assert_kind_of Array, @directory.services["queue"]
54
+ end
55
+
56
+ # Here we're just confirming that when we ask for it, the next availble
57
+ # database number for a given service is retrieved.
58
+ def test_database_number_is_retrieved
59
+ assert_kind_of Fixnum, @directory.next_db("sessions")
60
+ end
61
+
62
+ # If the service/cluster is undefined, then we should raise a
63
+ # UndefinedServiceError when requesting it's next database number.
64
+ def test_database_number_errors_for_service_not_exist
65
+ assert_raises(Redis::Directory::UndefinedServiceError) do
66
+ @directory.next_db("quack")
67
+ end
68
+ end
69
+
70
+ # Once we've made a connection (+Redis::Directory#get+), we should be able
71
+ # to confirm that the database we've connected to is for this name.
72
+ def test_database_contains_connection_name
73
+ assert_equal "acme", @directory.get("cache", "acme").get("connection-name")
74
+ end
75
+
76
+ # If we reserve a database, it should be tracked in a Hash describing the
77
+ # currently reserved services for that name. That way we can look it up later
78
+ # and re-use it instead of always provisioning new databases every time you
79
+ # make a new request.
80
+ def test_database_is_reserved
81
+ @directory.reserve("cache", "acme")
82
+ assert @redis.hexists("cache-service", "acme")
83
+ refute_nil @redis.hget("cache-service", "acme")
84
+ end
85
+
86
+ # This test confirms an internal implementation detail, ensuring that we don't
87
+ # have collisions between multiple database names for a given service.
88
+ def test_additional_cache_connection_increments
89
+ @directory.reserve("cache", "acme")
90
+ @directory.reserve("cache", "trioptimum")
91
+ assert_equal @redis.hlen("cache-service"), @redis.hget("cache-service", "trioptimum").to_i
92
+ end
93
+
94
+ # When there are "gaps" in the list of tracked databases for a service, those
95
+ # should be filled to keep the maximum database number as low as possible.
96
+ # This way as long as you clean up disused databases by removing them from the
97
+ # service keys, eg:
98
+ #
99
+ # redis-cli hdel cache-service acme
100
+ #
101
+ # Then we'll re-use that database number, meaning that as long as you've set
102
+ # (in your redis.conf) your maximum databases equal to or great than the number
103
+ # you plan to use, +Redis::Directory+ should not exceed that number.
104
+ def test_sparse_ids_are_filled
105
+ @redis.hset("cache-service", "corporatron", 5)
106
+
107
+ @directory.reserve("cache", "bnl")
108
+ assert_equal 1, @redis.hget("cache-service", "bnl").to_i
109
+
110
+ @directory.reserve("cache", "axiom")
111
+ assert_equal 2, @redis.hget("cache-service", "axiom").to_i
112
+ end
113
+
114
+ # In order to use as few database numbers as possible, and again, allowing you
115
+ # to tune Redis configuration settings like +databases+ on a per service/cluster
116
+ # basis, database numbers may not be consistent across services/clusters for a
117
+ # given client/name. This is especially true if not all clients consume all services,
118
+ # as the example below demonstrates.
119
+ def test_database_ids_are_unbalanced
120
+ bnl = @directory.reserve("cache", "bnl")
121
+ hacker = @directory.reserve("cache", "trioptimum")
122
+ acme = @directory.reserve("cache", "acme")
123
+
124
+ shodan = @directory.reserve("sessions", "trioptimum")
125
+
126
+ refute_equal hacker, shodan
127
+ end
128
+
129
+ end
data/test/helper.rb ADDED
@@ -0,0 +1,7 @@
1
+ ENV["ENVIRONMENT"] = "test"
2
+
3
+ require "rubygems"
4
+ require "bundler/setup"
5
+
6
+ require "minitest/autorun"
7
+ require File.dirname(__FILE__) + "/../lib/redis_directory"
metadata ADDED
@@ -0,0 +1,140 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: redis_directory
3
+ version: !ruby/object:Gem::Version
4
+ hash: 31
5
+ prerelease:
6
+ segments:
7
+ - 1
8
+ - 0
9
+ - 4
10
+ version: 1.0.4
11
+ platform: ruby
12
+ authors:
13
+ - Sam Smoot
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2012-03-03 00:00:00 Z
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: redis
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ none: false
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ hash: 3
29
+ segments:
30
+ - 0
31
+ version: "0"
32
+ type: :runtime
33
+ version_requirements: *id001
34
+ - !ruby/object:Gem::Dependency
35
+ name: minitest
36
+ prerelease: false
37
+ requirement: &id002 !ruby/object:Gem::Requirement
38
+ none: false
39
+ requirements:
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ hash: 3
43
+ segments:
44
+ - 0
45
+ version: "0"
46
+ type: :runtime
47
+ version_requirements: *id002
48
+ - !ruby/object:Gem::Dependency
49
+ name: json
50
+ prerelease: false
51
+ requirement: &id003 !ruby/object:Gem::Requirement
52
+ none: false
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ hash: 3
57
+ segments:
58
+ - 0
59
+ version: "0"
60
+ type: :runtime
61
+ version_requirements: *id003
62
+ description: |
63
+ Redis::Directory assumes a Redis installation running on a default
64
+ port and database 0 that will contain connection information for various
65
+ other Redis databases. For example, if you were using a Redis database to
66
+ store the content of cached pages, and this was running on a cluster of
67
+ two Redis instances, with multiple applications connecting partitioned by
68
+ database, then your connection might look like this:
69
+
70
+ require "redis"
71
+ require "redis/distributed"
72
+
73
+ # The ACME Corp database is #27
74
+ cache = Redis::Distributed.new "redis://redis.example:4400/27", "redis://redis.example:4401/27"
75
+
76
+ Redis::Directory uses a centralized Redis database to store the
77
+ connection information so you don't have to remember "magic numbers" for
78
+ each client/database mapping, and can easily update port-numbers/hostnames,
79
+ cluster-members as necessary. The same connection with
80
+ Redis::Directory would look like this:
81
+
82
+ require "redis_directory"
83
+
84
+ cache = Redis::Directory.new("redis.example").connect("cache", "acme")
85
+
86
+ email: ssmoot@gmail.com
87
+ executables: []
88
+
89
+ extensions: []
90
+
91
+ extra_rdoc_files: []
92
+
93
+ files:
94
+ - README
95
+ - lib/redis_directory.rb
96
+ - test/connection_test.rb
97
+ - test/helper.rb
98
+ - redis_directory.gemspec
99
+ - Rakefile
100
+ homepage: https://github.com/wiecklabs/redis_directory
101
+ licenses:
102
+ - MIT
103
+ post_install_message:
104
+ rdoc_options:
105
+ - --inline-source
106
+ - --line-numbers
107
+ - --title
108
+ - "Redis Directory: A database connection manager for Redis"
109
+ - README
110
+ - MIT-LICENSE
111
+ - lib
112
+ require_paths:
113
+ - lib
114
+ required_ruby_version: !ruby/object:Gem::Requirement
115
+ none: false
116
+ requirements:
117
+ - - ">="
118
+ - !ruby/object:Gem::Version
119
+ hash: 3
120
+ segments:
121
+ - 0
122
+ version: "0"
123
+ required_rubygems_version: !ruby/object:Gem::Requirement
124
+ none: false
125
+ requirements:
126
+ - - ">="
127
+ - !ruby/object:Gem::Version
128
+ hash: 3
129
+ segments:
130
+ - 0
131
+ version: "0"
132
+ requirements: []
133
+
134
+ rubyforge_project:
135
+ rubygems_version: 1.8.12
136
+ signing_key:
137
+ specification_version: 3
138
+ summary: Redis Directory for retriving/storing redis connections in a central database.
139
+ test_files:
140
+ - test/connection_test.rb