redis_migrator 0.0.1 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +1 -0
  3. data/Gemfile +3 -0
  4. data/README.md +13 -1
  5. data/Rakefile +6 -0
  6. data/lib/redis_migrator/redis_helper.rb +5 -36
  7. data/lib/redis_migrator/redis_native_migrator.rb +27 -0
  8. data/lib/redis_migrator/redis_pipe_migrator.rb +66 -0
  9. data/lib/redis_migrator/redis_populator.rb +1 -1
  10. data/lib/redis_migrator.rb +19 -20
  11. data/migrator_benchmark.rb +1 -2
  12. data/redis_migrator.gemspec +8 -4
  13. data/spec/different_redis_type_migrator.rb +67 -0
  14. data/spec/pretested_migrator.rb +47 -0
  15. data/spec/redis_migrator_spec.rb +41 -0
  16. data/spec/redis_native_migrator_spec.rb +44 -0
  17. data/spec/redis_pipe_migrator_spec.rb +51 -0
  18. data/spec/shared_hosts_context.rb +10 -0
  19. data/spec/spec_helper.rb +9 -7
  20. metadata +85 -49
  21. data/spec/migrator_spec.rb +0 -63
  22. data/spec/mock_redis/lib/mock_redis/assertions.rb +0 -13
  23. data/spec/mock_redis/lib/mock_redis/database.rb +0 -432
  24. data/spec/mock_redis/lib/mock_redis/distributed.rb +0 -6
  25. data/spec/mock_redis/lib/mock_redis/exceptions.rb +0 -3
  26. data/spec/mock_redis/lib/mock_redis/expire_wrapper.rb +0 -25
  27. data/spec/mock_redis/lib/mock_redis/hash_methods.rb +0 -118
  28. data/spec/mock_redis/lib/mock_redis/list_methods.rb +0 -187
  29. data/spec/mock_redis/lib/mock_redis/multi_db_wrapper.rb +0 -86
  30. data/spec/mock_redis/lib/mock_redis/set_methods.rb +0 -126
  31. data/spec/mock_redis/lib/mock_redis/string_methods.rb +0 -203
  32. data/spec/mock_redis/lib/mock_redis/transaction_wrapper.rb +0 -80
  33. data/spec/mock_redis/lib/mock_redis/undef_redis_methods.rb +0 -11
  34. data/spec/mock_redis/lib/mock_redis/utility_methods.rb +0 -25
  35. data/spec/mock_redis/lib/mock_redis/version.rb +0 -3
  36. data/spec/mock_redis/lib/mock_redis/zset.rb +0 -110
  37. data/spec/mock_redis/lib/mock_redis/zset_methods.rb +0 -210
  38. data/spec/mock_redis/lib/mock_redis.rb +0 -119
  39. data/spec/redis_helper_spec.rb +0 -58
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 98a618ae2641c4bf6eadc6cae753d8219f0ab039
4
+ data.tar.gz: e07f091488059a7d20f7ce0bdf05fb43566632ef
5
+ SHA512:
6
+ metadata.gz: 7cda9a9894f2a79c4a500eb15cf9ab6f5efe6fcf512112b9dcfe818c0f672ee4e1894d2aae5d03624db69aee3452821cabf1597c3c6f1588f93785bdd6b5711f
7
+ data.tar.gz: f6d7e25a19eff8d52cc6704d528eeb6c5fa03bfd5ea4d8f6ac5fec92ba675b14c55d1c3ff8b5d332511b6c69ef86f784e5e5a6d2a0fc381ca9026a1a24126bd5
data/.gitignore CHANGED
@@ -17,3 +17,4 @@ tmp
17
17
  _yardoc
18
18
  doc/
19
19
  .DS_Store
20
+ .idea/
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec
data/README.md CHANGED
@@ -22,7 +22,7 @@ and determines for which keys routes were changed. Then it moves those keys to n
22
22
  old_redis_hosts = ["redis://host1.com:6379", "redis://host2.com:6379"]
23
23
 
24
24
  # a list of redis-urls for a new cluster
25
- old_redis_hosts = ["redis://host1.com:6379", "redis://host2.com:6379", "redis://host3.com:6379"]
25
+ new_redis_hosts = ["redis://host1.com:6379", "redis://host2.com:6379", "redis://host3.com:6379"]
26
26
 
27
27
  migrator = Redis::Migrator.new(old_redis_hosts, new_redis_hosts)
28
28
  migrator.run
@@ -31,5 +31,17 @@ and determines for which keys routes were changed. Then it moves those keys to n
31
31
  * ruby 1.9 or jruby (with --1.9 flag)
32
32
  * redis >=2.4.14 (only on machine where migrator will be running)
33
33
 
34
+ ##Contributing
35
+
36
+ # First fork the project.
37
+ # Then bundle
38
+ bundle
39
+
40
+ # and make sure tests pass
41
+ bundle exec rspec spec
42
+
43
+ # Add features and profit.
44
+ # Send a pull request back to the original repository.
45
+
34
46
  ##TODO
35
47
  * Error handling
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new
5
+
6
+ task :test => :spec
@@ -1,9 +1,8 @@
1
1
  class Redis
2
2
  module Helper
3
-
4
3
  def to_redis_proto(*cmd)
5
4
  cmd.inject("*#{cmd.length}\r\n") {|acc, arg|
6
- acc << "$#{arg.length}\r\n#{arg}\r\n"
5
+ acc << "$#{arg.to_s.bytesize}\r\n#{arg}\r\n"
7
6
  }
8
7
  end
9
8
 
@@ -13,40 +12,10 @@ class Redis
13
12
  db = path[1..-1].to_i rescue 0
14
13
 
15
14
  {
16
- :host => node.host,
17
- :port => node.port || 6379,
18
- :db => db
15
+ host: node.host,
16
+ port: node.port || 6379,
17
+ db: db
19
18
  }
20
19
  end
21
-
22
- def copy_string(pipe, key)
23
- value = redis.get(key)
24
- pipe << to_redis_proto("SET", key, value)
25
- end
26
-
27
- def copy_hash(pipe, key)
28
- redis.hgetall(key).each do |field, value|
29
- pipe << to_redis_proto("HSET", key, field, value)
30
- end
31
- end
32
-
33
- def copy_list(pipe, key)
34
- redis.lrange(key, 0, -1).each do |value|
35
- pipe << to_redis_proto("LPUSH", key, value)
36
- end
37
- end
38
-
39
- def copy_set(pipe, key)
40
- redis.smembers(key).each do |member|
41
- pipe << to_redis_proto("SADD", key, member)
42
- end
43
- end
44
-
45
- def copy_zset(pipe, key)
46
- redis.zrange(key, 0, -1, :with_scores => true).each_slice(2) do |member, score|
47
- pipe << to_redis_proto("ZADD", key, score, member)
48
- end
49
- end
50
-
51
20
  end
52
- end
21
+ end
@@ -0,0 +1,27 @@
1
+ class Redis
2
+ class NativeMigrator
3
+ def initialize(old_hosts)
4
+ Thread.current[:redis] = Redis::Distributed.new(old_hosts)
5
+ end
6
+
7
+ def redis
8
+ Thread.current[:redis]
9
+ end
10
+
11
+ def migrate(node_options, keys, _)
12
+ new_node_options = { host: node_options[:host],
13
+ port: node_options[:port],
14
+ db: node_options[:db] }
15
+
16
+ grouped_by_old_nodes = keys.group_by do |key|
17
+ redis.node_for(key)
18
+ end
19
+
20
+ grouped_by_old_nodes.each do |old_node, node_keys|
21
+ old_node.pipelined do
22
+ node_keys.each { |key| old_node.migrate(key, new_node_options) }
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,66 @@
1
+ class Redis
2
+ class PipeMigrator
3
+ include Redis::Helper
4
+
5
+ def initialize(old_hosts)
6
+ Thread.current[:redis] = Redis::Distributed.new(old_hosts)
7
+ end
8
+
9
+ def redis
10
+ Thread.current[:redis]
11
+ end
12
+
13
+ def migrate(node_options, keys, options)
14
+ host, port, db = node_options[:host], node_options[:port], node_options[:db]
15
+ pipe = IO.popen("redis-cli -h #{host} -p #{port} -n #{db} --pipe", IO::RDWR)
16
+
17
+ keys.each {|key|
18
+ copy_key(pipe, key)
19
+
20
+ #remove key from old node
21
+ redis.node_for(key).del(key) unless options[:do_not_remove]
22
+ }
23
+
24
+ pipe.close
25
+ end
26
+
27
+ # Copy a given Redis key to a Redis pipe
28
+ # @param pipe [IO] a pipe opened redis-cli --pipe
29
+ # @param key [String] a Redis key that needs to be copied
30
+ def copy_key(pipe, key)
31
+ key_type = redis.type(key)
32
+ return false unless ['list', 'hash', 'string', 'set', 'zset'].include?(key_type)
33
+
34
+ self.send("copy_#{key_type}", pipe, key)
35
+ end
36
+
37
+ def copy_string(pipe, key)
38
+ value = redis.get(key)
39
+ pipe << to_redis_proto('SET', key, value)
40
+ end
41
+
42
+ def copy_hash(pipe, key)
43
+ redis.hgetall(key).each do |field, value|
44
+ pipe << to_redis_proto('HSET', key, field, value)
45
+ end
46
+ end
47
+
48
+ def copy_list(pipe, key)
49
+ redis.lrange(key, 0, -1).each do |value|
50
+ pipe << to_redis_proto('RPUSH', key, value)
51
+ end
52
+ end
53
+
54
+ def copy_set(pipe, key)
55
+ redis.smembers(key).reverse.each do |member|
56
+ pipe << to_redis_proto('SADD', key, member)
57
+ end
58
+ end
59
+
60
+ def copy_zset(pipe, key)
61
+ redis.zrange(key, 0, -1, with_scores: true).each do |member, score|
62
+ pipe << to_redis_proto('ZADD', key, score, member)
63
+ end
64
+ end
65
+ end
66
+ end
@@ -33,7 +33,7 @@ class Redis
33
33
 
34
34
  keys.each do |key|
35
35
  size.times.map do |x|
36
- f << to_redis_proto("SADD", key, ::Digest::MD5.hexdigest("f" + x.to_s))
36
+ f << to_redis_proto('SADD', key, ::Digest::MD5.hexdigest('f' + x.to_s))
37
37
  end
38
38
  end
39
39
 
@@ -1,7 +1,8 @@
1
- require 'rubygems'
2
1
  require 'redis'
3
2
  require 'redis/distributed'
4
3
  require_relative 'redis_migrator/redis_helper'
4
+ require_relative 'redis_migrator/redis_pipe_migrator'
5
+ require_relative 'redis_migrator/redis_native_migrator'
5
6
 
6
7
  class Redis
7
8
  class Migrator
@@ -48,19 +49,8 @@ class Redis
48
49
  # @param options [Hash] additional options, such as :do_not_remove => true
49
50
  def migrate_keys(node, keys, options={})
50
51
  return false if keys.empty? || keys.nil?
51
-
52
- Thread.current[:redis] = Redis::Distributed.new(old_hosts)
53
52
 
54
- pipe = IO.popen("redis-cli -h #{node[:host]} -p #{node[:port]} -n #{node[:db]} --pipe", IO::RDWR)
55
-
56
- keys.each {|key|
57
- copy_key(pipe, key)
58
-
59
- #remove key from old node
60
- redis.node_for(key).del(key) unless options[:do_not_remove]
61
- }
62
-
63
- pipe.close
53
+ migrator(options[:do_not_remove]).new(old_hosts).migrate(node, keys, options)
64
54
  end
65
55
 
66
56
  # Runs a migration process for a Redis cluster.
@@ -82,15 +72,24 @@ class Redis
82
72
  threads.each{|t| t.join}
83
73
  end
84
74
 
85
- # Copy a given Redis key to a Redis pipe
86
- # @param pipe [IO] a pipe opened redis-cli --pipe
87
- # @param key [String] a Redis key that needs to be copied
88
- def copy_key(pipe, key)
89
- key_type = old_cluster.type(key)
90
- return false unless ['list', 'hash', 'string', 'set', 'zset'].include?(key_type)
75
+ private
76
+
77
+ def nodes
78
+ old_cluster.nodes + new_cluster.nodes
79
+ end
91
80
 
92
- self.send("copy_#{key_type}", pipe, key)
81
+ def old_nodes
82
+ @old_nodes ||= nodes.select { |node| node.info['redis_version'].to_f < 2.6 }
93
83
  end
94
84
 
85
+ def migrator(keep_original)
86
+ @migrator ||= begin
87
+ if old_nodes.any? || keep_original
88
+ Redis::PipeMigrator
89
+ else
90
+ Redis::NativeMigrator
91
+ end
92
+ end
93
+ end
95
94
  end # class Migrator
96
95
  end # class Redis
@@ -18,5 +18,4 @@ migrator = Redis::Migrator.new(old_redis_hosts, new_redis_hosts)
18
18
  Benchmark.bm do |x|
19
19
  x.report("populate:") { r.populate_cluster(1000, 100) }
20
20
  x.report("migrate:") { migrator.run }
21
- end
22
-
21
+ end
@@ -3,8 +3,8 @@ $:.push File.expand_path("./lib", __FILE__)
3
3
 
4
4
  Gem::Specification.new do |s|
5
5
  s.name = "redis_migrator"
6
- s.version = "0.0.1"
7
- s.date = "2012-07-05"
6
+ s.version = "0.1.1"
7
+ s.date = "2014-04-10"
8
8
  s.platform = Gem::Platform::RUBY
9
9
  s.authors = ["Artem Yankov"]
10
10
  s.email = ["artem.yankov@gmail.com"]
@@ -16,5 +16,9 @@ Gem::Specification.new do |s|
16
16
  s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
17
17
  s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
18
18
  s.require_paths = ["lib"]
19
- s.add_dependency('redis', '>= 2.2.2')
20
- end
19
+ s.add_dependency('redis', '>= 3.0.0')
20
+ s.add_development_dependency 'rspec', '~> 2.6'
21
+ s.add_development_dependency 'rake'
22
+ s.add_development_dependency 'debugger'
23
+ s.add_development_dependency 'mock_redis'
24
+ end
@@ -0,0 +1,67 @@
1
+ shared_examples 'different redis type migrator' do
2
+ context 'key of type' do
3
+ let(:keys) { [key] }
4
+
5
+ subject { migrator.migrate(node, keys, {}) }
6
+
7
+ context 'string' do
8
+ let(:key) { 'a' }
9
+
10
+ it 'should copy' do
11
+ old_cluster.set(key, 'some_string')
12
+ subject
13
+ destination_cluster.get(key).should == 'some_string'
14
+ end
15
+ end
16
+
17
+
18
+ context 'hash' do
19
+ let(:key) { 'myhash' }
20
+
21
+ it 'should copy' do
22
+ old_cluster.hmset(key,
23
+ 'first_name', 'James',
24
+ 'last_name', 'Randi',
25
+ 'age', '83')
26
+ subject
27
+ destination_cluster.hgetall(key).should == {'first_name' => 'James',
28
+ 'last_name' => 'Randi',
29
+ 'age' => '83'}
30
+ end
31
+ end
32
+
33
+ context 'list' do
34
+ let(:key) { 'mylist' }
35
+
36
+ it 'should copy' do
37
+ ('a'..'z').to_a.each { |val| old_cluster.lpush(key, val) }
38
+ values = old_cluster.lrange(key, 0, -1)
39
+ subject
40
+ destination_cluster.lrange(key, 0, -1).should == values
41
+ end
42
+ end
43
+
44
+ context 'set' do
45
+ let(:key) { 'myset' }
46
+ it 'should copy' do
47
+ ('a'..'z').to_a.each { |val| old_cluster.sadd(key, val) }
48
+ values = old_cluster.smembers(key)
49
+ subject
50
+ destination_cluster.smembers(key).should == values
51
+ end
52
+ end
53
+
54
+ context 'zset' do
55
+ let(:key) { 'myzset' }
56
+ it 'should copy' do
57
+ ('a'..'z').to_a.each { |val| old_cluster.zadd(key, rand(100), val) }
58
+ old_range = old_cluster.zrange(key, 0, -1, with_scores: true).sort
59
+
60
+ subject
61
+
62
+ new_range = destination_cluster.zrange(key, 0, -1, with_scores: true).sort
63
+ new_range.should == old_range
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,47 @@
1
+ shared_context 'common keys' do
2
+ let(:keys) { %w(q s j) }
3
+ let(:node) { { host: 'localhost', port: 6378, db: 1 } }
4
+ let(:options) { {} }
5
+
6
+ before do
7
+ prefill_cluster(old_cluster)
8
+ migrator.migrate(node, keys, options)
9
+ end
10
+
11
+ def common_keys(cluster)
12
+ (cluster.keys('*') & keys).sort
13
+ end
14
+
15
+ subject { common_keys(cluster) }
16
+ end
17
+
18
+ shared_examples 'pretested migrator' do
19
+ include_context 'common keys'
20
+
21
+ context do
22
+ let(:cluster) { destination_cluster }
23
+ it 'should copy given keys to a new cluster' do
24
+ should == %w(j q s)
25
+ end
26
+ end
27
+ end
28
+
29
+ shared_examples 'safe pretested migrator' do
30
+ include_context 'common keys'
31
+
32
+ context do
33
+ let(:cluster) { old_cluster }
34
+
35
+ it 'should remove copied keys from the old redis node' do
36
+ should == []
37
+ end
38
+
39
+ context 'when asked to not remove' do
40
+ let(:options) { { do_not_remove: true } }
41
+ it 'should keep keys on old node' do
42
+ should == ["j", "q", "s"]
43
+ end
44
+ end
45
+ end
46
+ end
47
+
@@ -0,0 +1,41 @@
1
+ require_relative 'shared_hosts_context'
2
+
3
+ describe Redis::Migrator do
4
+ include_context 'shared hosts context'
5
+ let(:migrator) { Redis::Migrator.new(old_hosts, new_hosts) }
6
+
7
+ describe '#changed_keys' do
8
+ before { prefill_cluster(migrator.old_cluster) }
9
+
10
+ it 'should show keys which need migration' do
11
+ migrator.changed_keys.should == {'redis://localhost:6377/0' => %w(h q s y j m n o)}
12
+ end
13
+ end
14
+
15
+ describe '#migrator' do
16
+ subject { migrator.send(:migrator, keep_original) }
17
+ let(:keep_original) { false }
18
+
19
+ before do
20
+ allow_any_instance_of(MockRedis).to(
21
+ receive(:info).and_return({ 'redis_version' => version })
22
+ )
23
+ end
24
+
25
+ context 'when all instances are old' do
26
+ let(:version) { '2.4.1' }
27
+ it { should == Redis::PipeMigrator}
28
+ end
29
+
30
+ context 'when all instances are new' do
31
+ let(:version) { '2.6.14' }
32
+ it { should == Redis::NativeMigrator }
33
+
34
+ context 'when asking to preserve data on source' do
35
+ let(:keep_original) { true }
36
+
37
+ it { should == Redis::PipeMigrator }
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,44 @@
1
+ require_relative 'shared_hosts_context'
2
+ require_relative 'different_redis_type_migrator'
3
+ require_relative 'pretested_migrator'
4
+
5
+ describe Redis::NativeMigrator do
6
+ let(:migrator) { Redis::NativeMigrator.new(old_hosts) }
7
+ include_context 'shared hosts context'
8
+
9
+ let(:old_cluster) { Redis::Distributed.new(old_hosts) }
10
+ let(:new_cluster) { Redis::Distributed.new(new_hosts) }
11
+
12
+ before { allow(migrator).to receive(:redis).and_return(old_cluster) }
13
+
14
+ describe '#migrate' do
15
+ context do
16
+ let(:node) { new_cluster.node_for(key) }
17
+ let(:source_node) { old_cluster.node_for(key) }
18
+ let(:destination_cluster) { source_node.client.select(10); source_node }
19
+ before do
20
+ allow_any_instance_of(MockRedis).to receive(:migrate) do |key|
21
+ source_node.move(key, 10)
22
+ end
23
+ end
24
+
25
+ it_behaves_like 'different redis type migrator'
26
+ end
27
+
28
+ context do
29
+ let(:old_cluster) { Redis::Distributed.new([old_hosts.first]) }
30
+ let(:source_node) { old_cluster.nodes.first }
31
+ let(:destination_cluster) { source_node.client.select(10); source_node }
32
+
33
+ before do
34
+ allow_any_instance_of(MockRedis).to receive(:migrate) do |key|
35
+ source_node.move(key, 10)
36
+ end
37
+ end
38
+
39
+ it_behaves_like 'pretested migrator'
40
+ # Not supported in Redis < 3.0
41
+ # it_behaves_like 'safe pretested migrator'
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,51 @@
1
+ require_relative 'shared_hosts_context'
2
+ require_relative 'different_redis_type_migrator'
3
+ require_relative 'pretested_migrator'
4
+
5
+ describe Redis::PipeMigrator do
6
+ let(:migrator) { Redis::PipeMigrator.new(old_hosts) }
7
+ include_context 'shared hosts context'
8
+
9
+ let(:old_cluster) { Redis::Distributed.new(old_hosts) }
10
+ let(:destination_cluster) { Redis::Distributed.new(new_hosts) }
11
+ let(:pipe) { PipeMock.new(destination_cluster) }
12
+
13
+ before { allow(migrator).to receive(:redis).and_return(old_cluster) }
14
+
15
+ describe '#migrate' do
16
+ context do
17
+ let(:node) { {} }
18
+ before { expect(IO).to receive(:popen).and_return(pipe) }
19
+ it_behaves_like 'different redis type migrator'
20
+ end
21
+
22
+ context do
23
+ before do
24
+ command = 'redis-cli -h localhost -p 6378 -n 1 --pipe'
25
+ expect(IO).to receive(:popen).with(command, IO::RDWR).and_return(pipe)
26
+ end
27
+
28
+ it_behaves_like 'pretested migrator'
29
+ it_behaves_like 'safe pretested migrator'
30
+ end
31
+ end
32
+
33
+ describe '#copy_key' do
34
+ subject { migrator.copy_key(nil, key) }
35
+
36
+ context 'with unknown key' do
37
+ let(:key) { 'some_key' }
38
+ it { should == false }
39
+ end
40
+
41
+ context 'when known set key' do
42
+ let(:key) { 'a' }
43
+ before { old_cluster.sadd('a', 1) }
44
+
45
+ it 'calls copy_set' do
46
+ expect(migrator).to receive(:copy_set).with(nil, 'a')
47
+ subject
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,10 @@
1
+ shared_context 'shared hosts context' do
2
+ let(:old_hosts) { %w(redis://localhost:6379 redis://localhost:6378) }
3
+ let(:new_hosts) { old_hosts + ['redis://localhost:6377'] }
4
+
5
+ before do
6
+ expect(Redis).to receive(:new).at_least(1).times do |options|
7
+ MockRedis.new(options)
8
+ end
9
+ end
10
+ end
data/spec/spec_helper.rb CHANGED
@@ -1,10 +1,6 @@
1
- $LOAD_PATH.unshift File.expand_path(File.join(File.dirname(__FILE__), "mock_redis/lib"))
2
-
1
+ require 'rspec'
3
2
  require_relative "../lib/redis_migrator.rb"
4
-
5
- # include patched version of mock_redis
6
- # that works with Redis::Distributed
7
- require "mock_redis.rb"
3
+ require 'mock_redis'
8
4
 
9
5
  class PipeMock
10
6
  def initialize(redis)
@@ -26,4 +22,10 @@ class Redis
26
22
  cmd
27
23
  end
28
24
  end
29
- end
25
+ end
26
+
27
+ def prefill_cluster(cluster)
28
+ ('a'..'z').to_a.each do |key|
29
+ (1..5).to_a.each {|val| cluster.sadd(key, val)}
30
+ end
31
+ end