relix 1.0.3 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -153,7 +153,7 @@ The primary key index is the only index that is required on a model. Under the c
153
153
  primary_key :id
154
154
  end
155
155
 
156
- **Supported Operators**: eq, all
156
+ **Supported Operators**: eq, all
157
157
  **Ordering**: insertion
158
158
 
159
159
 
@@ -165,7 +165,7 @@ Multi indexes allow multiple matching primary keys per indexed value, and are id
165
165
  multi :account_id, order: :created_at
166
166
  end
167
167
 
168
- **Supported Operators**: eq
168
+ **Supported Operators**: eq
169
169
  **Ordering**: can be ordered on any numeric attribute (default is the to_i of the indexed value)
170
170
 
171
171
 
@@ -179,5 +179,33 @@ Unique indexes will raise an error if the same value is indexed twice for a diff
179
179
 
180
180
  Unique indexes ignore nil values - they will not be indexed and an error is not raised if there is more than one object with a value of nil. A multi-value unique index will be completely skipped if any value in it is nil.
181
181
 
182
- **Supported Operators**: eq, all
183
- **Ordering**: can be ordered on any numeric attribute (default is the to_i of the indexed value)
182
+ **Supported Operators**: eq, all
183
+ **Ordering**: can be ordered on any numeric attribute (default is the to_i of the indexed value)
184
+
185
+
186
+ ## Keying
187
+
188
+ A big part of using Redis well is choosing solid keys; Relix has a pluggable keying infrastructure that makes it easy to use different key names for different situations. This actually rose out of the fact that the first release of Relix had a pathetic set of keys, and the need to support existing deployments while moving to something better going forward. Keyers are set on a per-model basis along with other configuration:
189
+
190
+ relix do
191
+ keyer Relix::Keyer::Compact
192
+ end
193
+
194
+ You can set the default keyer like so:
195
+
196
+ Relix.default_keyer(Relix::Keyer::Compact)
197
+
198
+
199
+ ### Standard
200
+
201
+ This keyer is nice and verbose, which makes it ideal for development since you can browse the Redis keyspace and see at a glance how the indexes are stored. **Standard is the default keyer.**
202
+
203
+
204
+ ### Compact
205
+
206
+ Keys take up space, and especially since Redis holds the keyset in memory it can be a big boon with a large data set to keep key names short. The Compact keyer tries to balance a reasonable level of readability (we can't sacrifice the ability to debug production issues) with keeping keys as compact as possible.
207
+
208
+
209
+ ### Legacy
210
+
211
+ This (eventually to be deprecated and removed) strategy exactly mirrors the keying supported by Relix when first released.
@@ -1,4 +1,5 @@
1
1
  require 'relix/core'
2
+ require 'relix/keyer'
2
3
  require 'relix/redis'
3
4
  require 'relix/query'
4
5
  require 'relix/index_set'
@@ -8,13 +8,13 @@ module Relix
8
8
  @index_types ||= {}
9
9
  end
10
10
 
11
- def self.register_index(name, index)
12
- index_types[name.to_sym] = index
11
+ def self.register_index(index)
12
+ index_types[index.kind.to_sym] = index
13
13
  end
14
14
 
15
15
  module ClassMethods
16
16
  def relix(&block)
17
- @relix ||= IndexSet.new(self)
17
+ @relix ||= IndexSet.new(self, Relix.redis)
18
18
  if block_given?
19
19
  @relix.instance_eval(&block)
20
20
  else
@@ -34,4 +34,10 @@ module Relix
34
34
  def index!
35
35
  relix.index!(self)
36
36
  end
37
- end
37
+
38
+ def deindex!
39
+ relix.deindex!(self)
40
+ end
41
+
42
+ class Error < StandardError; end
43
+ end
@@ -1,11 +1,24 @@
1
1
  module Relix
2
2
  class Index
3
- def initialize(name, accessor, options={})
4
- @name = "#{self.class.name}:#{name}"
3
+ def self.kind
4
+ @kind ||= name.gsub(/(?:^.+::|Index$)/, '').gsub(/([a-z])([A-Z])/){"#{$1}_#{$2}"}.downcase
5
+ end
6
+
7
+ def self.compact_kind
8
+ @compact_kind ||= kind[0..0]
9
+ end
10
+
11
+ def initialize(set, base_name, accessor, options={})
12
+ @set = set
13
+ @base_name = base_name
5
14
  @accessor = [accessor].flatten.collect{|a| a.to_s}
6
15
  @options = options
7
16
  end
8
17
 
18
+ def name
19
+ @set.keyer.index(self, @base_name)
20
+ end
21
+
9
22
  def read(object)
10
23
  @accessor.inject({}){|h,e| h[e] = object.send(e); h}
11
24
  end
@@ -25,7 +38,7 @@ module Relix
25
38
  if value_hash.include?(k)
26
39
  value_hash[k].to_s
27
40
  else
28
- raise MissingIndexValueError, "Missing #{k} when looking up by #{@name}"
41
+ raise MissingIndexValueError, "Missing #{k} when looking up by #{name}"
29
42
  end
30
43
  end.join(":")
31
44
  end
@@ -42,10 +55,6 @@ module Relix
42
55
  nil
43
56
  end
44
57
 
45
- def key_for(value)
46
- "#{@name}:#{value}"
47
- end
48
-
49
58
  module Ordering
50
59
  def initialize(*args)
51
60
  super
@@ -74,10 +83,10 @@ module Relix
74
83
  end
75
84
  end
76
85
 
77
- def range_from_options(options, value=nil)
86
+ def range_from_options(r, options, value=nil)
78
87
  start = (options[:offset] || 0)
79
88
  if f = options[:from]
80
- start = (position(f, value) + 1)
89
+ start = (position(r, f, value) + 1)
81
90
  end
82
91
  stop = (options[:limit] ? (start + options[:limit] - 1) : -1)
83
92
  [start, stop]
@@ -85,6 +94,6 @@ module Relix
85
94
  end
86
95
  end
87
96
 
88
- class UnorderableValueError < StandardError; end
89
- class MissingIndexValueError < StandardError; end
97
+ class UnorderableValueError < Relix::Error; end
98
+ class MissingIndexValueError < Relix::Error; end
90
99
  end
@@ -1,15 +1,37 @@
1
1
  module Relix
2
2
  class IndexSet
3
- def initialize(klass)
3
+ attr_accessor :redis
4
+ def initialize(klass, redis)
4
5
  @klass = klass
6
+ @redis = redis
5
7
  @indexes = Hash.new
8
+ @keyer = Keyer.default_for(@klass)
6
9
  end
7
10
 
8
11
  def primary_key(accessor)
9
- add_index(:primary_key, 'primary_key', on: accessor)
12
+ @primary_key_index = add_index(:primary_key, accessor)
10
13
  end
11
14
  alias pk primary_key
12
15
 
16
+ def primary_key_index
17
+ unless @primary_key_index
18
+ if parent
19
+ @primary_key_index = parent.primary_key_index
20
+ else
21
+ raise MissingPrimaryKeyError.new("You must declare a primary key for #{@klass.name}")
22
+ end
23
+ end
24
+ @primary_key_index
25
+ end
26
+
27
+ def keyer(value=nil, options={})
28
+ if value
29
+ @keyer = value.new(@klass, options)
30
+ else
31
+ @keyer
32
+ end
33
+ end
34
+
13
35
  def method_missing(m, *args)
14
36
  if Relix.index_types.keys.include?(m.to_sym)
15
37
  add_index(m, *args)
@@ -20,7 +42,7 @@ module Relix
20
42
 
21
43
  def add_index(index_type, name, options={})
22
44
  accessor = (options.delete(:on) || name)
23
- @indexes[name.to_s] = Relix.index_types[index_type].new(key_prefix(name), accessor, options)
45
+ @indexes[name.to_s] = Relix.index_types[index_type].new(self, name, accessor, options)
24
46
  end
25
47
 
26
48
  def indexes
@@ -28,56 +50,65 @@ module Relix
28
50
  end
29
51
 
30
52
  def lookup(&block)
31
- unless primary_key = indexes['primary_key']
32
- raise MissingPrimaryKeyError.new("You must declare a primary key for #{@klass.name}")
33
- end
34
53
  if block
35
54
  query = Query.new(self)
36
55
  yield(query)
37
56
  query.run
38
57
  else
39
- primary_key.all
58
+ primary_key_index.all(@redis)
40
59
  end
41
60
  end
42
61
 
43
- def index!(object)
44
- unless primary_key_index = indexes['primary_key']
45
- raise MissingPrimaryKeyError.new("You must declare a primary key for #{@klass.name}")
46
- end
47
- pk = primary_key_index.read_normalized(object)
48
- current_values_name = "#{key_prefix('current_values')}:#{pk}"
62
+ def index_ops(object, pk)
63
+ current_values_name = current_values_name(pk)
64
+ @redis.watch current_values_name
65
+ current_values = @redis.hgetall(current_values_name)
49
66
 
50
- Relix.redis do |r|
51
- loop do
52
- r.watch current_values_name
53
- current_values = r.hgetall(current_values_name)
54
- indexers = []
55
- indexes.each do |name,index|
56
- ((watch = index.watch) && r.watch(*watch))
67
+ ops = indexes.collect do |name,index|
68
+ ((watch = index.watch) && @redis.watch(*watch))
57
69
 
58
- value = index.read_normalized(object)
59
- old_value = current_values[name]
70
+ value = index.read_normalized(object)
71
+ old_value = current_values[name]
60
72
 
61
- next if value == old_value
62
- current_values[name] = value
73
+ next if value == old_value
74
+ current_values[name] = value
63
75
 
64
- next unless index.filter(r, object, value)
76
+ next unless index.filter(@redis, object, value)
65
77
 
66
- query_value = index.query(r, value)
67
- indexers << proc do
68
- index.index(r, pk, object, value, old_value, *query_value)
69
- end
70
- end
71
- r.multi do
72
- indexers.each do |indexer|
73
- indexer.call
74
- end
75
- r.hmset(current_values_name, *current_values.flatten)
76
- end.each do |result|
77
- raise RedisIndexingError.new(result.message) if Exception === result
78
- end
79
- break
78
+ query_value = index.query(@redis, value)
79
+ proc do
80
+ index.index(@redis, pk, object, value, old_value, *query_value)
80
81
  end
82
+ end.compact
83
+
84
+ ops << proc do
85
+ @redis.hmset(current_values_name, *current_values.flatten)
86
+ end
87
+
88
+ ops
89
+ end
90
+
91
+ def index!(object)
92
+ pk = primary_key_for(object)
93
+
94
+ handle_concurrent_modifications(pk) do
95
+ index_ops(object, pk)
96
+ end
97
+ end
98
+
99
+ def deindex!(object)
100
+ pk = primary_key_for(object)
101
+
102
+ handle_concurrent_modifications(pk) do
103
+ current_values_name = current_values_name(pk)
104
+ @redis.watch current_values_name
105
+ current_values = @redis.hgetall(current_values_name)
106
+
107
+ indexes.map do |name, index|
108
+ ((watch = index.watch) && @redis.watch(*watch))
109
+ old_value = current_values[name]
110
+ proc { index.deindex(@redis, pk, object, old_value) }
111
+ end.tap { |ops| ops << proc { @redis.del current_values_name } }
81
112
  end
82
113
  end
83
114
 
@@ -92,8 +123,42 @@ module Relix
92
123
  end
93
124
  @parent
94
125
  end
126
+
127
+ def current_values_name(pk)
128
+ @keyer.values(pk)
129
+ end
130
+
131
+ private
132
+
133
+ def handle_concurrent_modifications(primary_key)
134
+ retries = 5
135
+ loop do
136
+ ops = yield
137
+
138
+ results = @redis.multi do
139
+ ops.each do |op|
140
+ op.call(primary_key)
141
+ end
142
+ end
143
+
144
+ if results
145
+ results.each do |result|
146
+ raise RedisIndexingError.new(result.message) if Exception === result
147
+ end
148
+ break
149
+ else
150
+ retries -= 1
151
+ raise ExceededRetriesForConcurrentWritesError.new if retries <= 0
152
+ end
153
+ end
154
+ end
155
+
156
+ def primary_key_for(object)
157
+ primary_key_index.read_normalized(object)
158
+ end
95
159
  end
96
160
 
97
- class MissingPrimaryKeyError < StandardError; end
98
- class RedisIndexingError < StandardError; end
99
- end
161
+ class MissingPrimaryKeyError < Relix::Error; end
162
+ class RedisIndexingError < Relix::Error; end
163
+ class ExceededRetriesForConcurrentWritesError < Relix::Error; end
164
+ end
@@ -7,15 +7,23 @@ module Relix
7
7
  r.zrem(key_for(old_value), pk)
8
8
  end
9
9
 
10
- def eq(value, options={})
11
- Relix.redis.zrange(key_for(value), *range_from_options(options, value))
10
+ def deindex(r, pk, object, old_value)
11
+ r.zrem(key_for(old_value), pk)
12
+ end
13
+
14
+ def eq(r, value, options={})
15
+ r.zrange(key_for(value), *range_from_options(r, options, value))
12
16
  end
13
17
 
14
- def position(pk, value)
15
- position = Relix.redis.zrank(key_for(value), pk)
18
+ def position(r, pk, value)
19
+ position = r.zrank(key_for(value), pk)
16
20
  raise MissingIndexValueError, "Cannot find key #{pk} in index for #{value}" unless position
17
21
  position
18
22
  end
23
+
24
+ def key_for(value)
25
+ @set.keyer.component(name, value)
26
+ end
19
27
  end
20
- register_index :multi, MultiIndex
28
+ register_index MultiIndex
21
29
  end
@@ -3,28 +3,32 @@ module Relix
3
3
  include Ordering
4
4
 
5
5
  def watch
6
- @name
6
+ name
7
7
  end
8
8
 
9
9
  def filter(r, object, value)
10
- !r.zrank(@name, value)
10
+ !r.zrank(name, value)
11
11
  end
12
12
 
13
13
  def query(r, value)
14
- r.zcard(@name)
14
+ r.zcard(name)
15
15
  end
16
16
 
17
17
  def index(r, pk, object, value, old_value, rank)
18
- r.zadd(@name, rank, pk)
18
+ r.zadd(name, rank, pk)
19
19
  end
20
20
 
21
- def all(options={})
22
- Relix.redis.zrange(@name, *range_from_options(options))
21
+ def deindex(r, pk, object, old_value)
22
+ r.zrem(name, pk)
23
23
  end
24
24
 
25
- def eq(value, options)
25
+ def all(r, options={})
26
+ r.zrange(name, *range_from_options(r, options))
27
+ end
28
+
29
+ def eq(r, value, options)
26
30
  [value]
27
31
  end
28
32
  end
29
- register_index :primary_key, PrimaryKeyIndex
33
+ register_index PrimaryKeyIndex
30
34
  end
@@ -2,44 +2,51 @@ module Relix
2
2
  class UniqueIndex < Index
3
3
  include Ordering
4
4
 
5
- def initialize(*args)
6
- super
7
- @sorted_set_name = "#{@name}:zset"
8
- @hash_name = "#{@name}:hash"
5
+ def sorted_set_name
6
+ @set.keyer.component(name, 'ordering')
7
+ end
8
+
9
+ def hash_name
10
+ @set.keyer.component(name, 'lookup')
9
11
  end
10
12
 
11
13
  def watch
12
- @hash_name
14
+ hash_name
13
15
  end
14
16
 
15
17
  def filter(r, object, value)
16
18
  return true if read(object).values.any?{|e| e.nil?}
17
- if r.hexists(@hash_name, value)
18
- raise NotUniqueError.new("'#{value}' is not unique in index #{@name}")
19
+ if r.hexists(hash_name, value)
20
+ raise NotUniqueError.new("'#{value}' is not unique in index #{name}")
19
21
  end
20
22
  true
21
23
  end
22
24
 
23
25
  def index(r, pk, object, value, old_value)
24
26
  if read(object).values.all?{|e| !e.nil?}
25
- r.hset(@hash_name, value, pk)
26
- r.zadd(@sorted_set_name, score(object, value), pk)
27
+ r.hset(hash_name, value, pk)
28
+ r.zadd(sorted_set_name, score(object, value), pk)
27
29
  else
28
- r.hdel(@hash_name, value)
29
- r.zrem(@sorted_set_name, pk)
30
+ r.hdel(hash_name, value)
31
+ r.zrem(sorted_set_name, pk)
30
32
  end
31
- r.hdel(@hash_name, old_value)
33
+ r.hdel(hash_name, old_value)
34
+ end
35
+
36
+ def deindex(r, pk, object, old_value)
37
+ r.hdel(hash_name, old_value)
38
+ r.zrem(sorted_set_name, pk)
32
39
  end
33
40
 
34
- def all(options={})
35
- Relix.redis.zrange(@sorted_set_name, *range_from_options(options))
41
+ def all(r, options={})
42
+ r.zrange(sorted_set_name, *range_from_options(r, options))
36
43
  end
37
44
 
38
- def eq(value, options={})
39
- [Relix.redis.hget(@hash_name, value)].compact
45
+ def eq(r, value, options={})
46
+ [r.hget(hash_name, value)].compact
40
47
  end
41
48
  end
42
- register_index :unique, UniqueIndex
49
+ register_index UniqueIndex
43
50
 
44
- class NotUniqueError < StandardError; end
51
+ class NotUniqueError < Relix::Error; end
45
52
  end
@@ -8,19 +8,20 @@ module Relix
8
8
  def [](index_name)
9
9
  index = @model.indexes[index_name.to_s]
10
10
  raise MissingIndexError.new("No index declared for #{index_name}") unless index
11
- @clause = Clause.new(index)
11
+ @clause = Clause.new(@model.redis, index)
12
12
  end
13
13
 
14
14
  def run
15
15
  if @clause
16
16
  @clause.lookup
17
17
  else
18
- @model.indexes['primary_key'].lookup
18
+ @model.primary_key_index.lookup
19
19
  end
20
20
  end
21
21
 
22
22
  class Clause
23
- def initialize(index)
23
+ def initialize(redis, index)
24
+ @redis = redis
24
25
  @index = index
25
26
  @options = {}
26
27
  end
@@ -39,13 +40,13 @@ module Relix
39
40
  if @options[:limit] == 0
40
41
  []
41
42
  elsif @all
42
- @index.all(@options)
43
+ @index.all(@redis, @options)
43
44
  else
44
- @index.eq(@value, @options)
45
+ @index.eq(@redis, @value, @options)
45
46
  end
46
47
  end
47
48
  end
48
49
  end
49
50
 
50
- class MissingIndexError < StandardError; end
51
+ class MissingIndexError < Relix::Error; end
51
52
  end
@@ -4,8 +4,7 @@ require 'redis'
4
4
  module Relix
5
5
  def self.redis
6
6
  unless @redis
7
- @redis = ::Redis.new(host: @redis_host, port: @redis_port)
8
- @redis.select @redis_db if @redis_db
7
+ @redis = new_redis_client
9
8
  end
10
9
  if block_given?
11
10
  yield(@redis)
@@ -14,6 +13,12 @@ module Relix
14
13
  end
15
14
  end
16
15
 
16
+ def self.new_redis_client
17
+ ::Redis.new(host: @redis_host, port: @redis_port).tap do |client|
18
+ client.select @redis_db if @redis_db
19
+ end
20
+ end
21
+
17
22
  def self.host=(value)
18
23
  @redis_host = value
19
24
  end
@@ -1,3 +1,3 @@
1
1
  module Relix
2
- VERSION = "1.0.3"
2
+ VERSION = "1.1.0"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: relix
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.3
4
+ version: 1.1.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,11 +9,11 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2011-11-04 00:00:00.000000000Z
12
+ date: 2012-01-05 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: hiredis
16
- requirement: &70309856251460 !ruby/object:Gem::Requirement
16
+ requirement: &70209042647380 !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ~>
@@ -21,10 +21,10 @@ dependencies:
21
21
  version: 0.4.1
22
22
  type: :runtime
23
23
  prerelease: false
24
- version_requirements: *70309856251460
24
+ version_requirements: *70209042647380
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: redis
27
- requirement: &70309856249460 !ruby/object:Gem::Requirement
27
+ requirement: &70209042646920 !ruby/object:Gem::Requirement
28
28
  none: false
29
29
  requirements:
30
30
  - - ~>
@@ -32,7 +32,7 @@ dependencies:
32
32
  version: 2.2.2
33
33
  type: :runtime
34
34
  prerelease: false
35
- version_requirements: *70309856249460
35
+ version_requirements: *70209042646920
36
36
  description: ! 'Relix is a layer that can be added on to any model to make all the
37
37
  normal types of querying you want to do: equality, less than/greater than, in set,
38
38
  range, limit, etc., quick and painless. Relix depends on Redis to be awesome at
@@ -67,6 +67,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
67
67
  - - ! '>='
68
68
  - !ruby/object:Gem::Version
69
69
  version: '0'
70
+ segments:
71
+ - 0
72
+ hash: 2795857590381463961
70
73
  required_rubygems_version: !ruby/object:Gem::Requirement
71
74
  none: false
72
75
  requirements: