relix 1.0.3 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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: