redisrank 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (57) hide show
  1. checksums.yaml +7 -0
  2. data/.document +5 -0
  3. data/.gitignore +27 -0
  4. data/.rspec +2 -0
  5. data/Gemfile +4 -0
  6. data/LICENSE +20 -0
  7. data/README.md +297 -0
  8. data/Rakefile +69 -0
  9. data/lib/redisrank.rb +106 -0
  10. data/lib/redisrank/buffer.rb +110 -0
  11. data/lib/redisrank/collection.rb +20 -0
  12. data/lib/redisrank/connection.rb +89 -0
  13. data/lib/redisrank/core_ext.rb +5 -0
  14. data/lib/redisrank/core_ext/bignum.rb +8 -0
  15. data/lib/redisrank/core_ext/date.rb +8 -0
  16. data/lib/redisrank/core_ext/fixnum.rb +8 -0
  17. data/lib/redisrank/core_ext/hash.rb +20 -0
  18. data/lib/redisrank/core_ext/time.rb +3 -0
  19. data/lib/redisrank/date.rb +88 -0
  20. data/lib/redisrank/event.rb +98 -0
  21. data/lib/redisrank/finder.rb +245 -0
  22. data/lib/redisrank/finder/date_set.rb +99 -0
  23. data/lib/redisrank/key.rb +84 -0
  24. data/lib/redisrank/label.rb +69 -0
  25. data/lib/redisrank/mixins/database.rb +11 -0
  26. data/lib/redisrank/mixins/date_helper.rb +8 -0
  27. data/lib/redisrank/mixins/options.rb +41 -0
  28. data/lib/redisrank/mixins/synchronize.rb +52 -0
  29. data/lib/redisrank/model.rb +77 -0
  30. data/lib/redisrank/result.rb +18 -0
  31. data/lib/redisrank/scope.rb +18 -0
  32. data/lib/redisrank/summary.rb +90 -0
  33. data/lib/redisrank/version.rb +3 -0
  34. data/redisrank.gemspec +31 -0
  35. data/spec/Find Results +3349 -0
  36. data/spec/buffer_spec.rb +104 -0
  37. data/spec/collection_spec.rb +20 -0
  38. data/spec/connection_spec.rb +67 -0
  39. data/spec/core_ext/hash_spec.rb +26 -0
  40. data/spec/database_spec.rb +10 -0
  41. data/spec/date_spec.rb +95 -0
  42. data/spec/event_spec.rb +86 -0
  43. data/spec/finder/date_set_spec.rb +527 -0
  44. data/spec/finder_spec.rb +205 -0
  45. data/spec/key_spec.rb +129 -0
  46. data/spec/label_spec.rb +86 -0
  47. data/spec/model_helper.rb +31 -0
  48. data/spec/model_spec.rb +191 -0
  49. data/spec/options_spec.rb +36 -0
  50. data/spec/redis-test.conf +9 -0
  51. data/spec/result_spec.rb +23 -0
  52. data/spec/scope_spec.rb +27 -0
  53. data/spec/spec_helper.rb +18 -0
  54. data/spec/summary_spec.rb +177 -0
  55. data/spec/synchronize_spec.rb +125 -0
  56. data/spec/thread_safety_spec.rb +39 -0
  57. metadata +235 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 750cd449e44719036541e0fd53226339bc06da26
4
+ data.tar.gz: 808d6302c856ae4f132da7420e6c45434b94819f
5
+ SHA512:
6
+ metadata.gz: 412112104821d68d5603ff6df2a629b519c61f4286cbecd10896b273bbea978b334df366b63e77cf77a613cbfbc68cc27f890f63623abf62b29a4b19a474fa96
7
+ data.tar.gz: 3e35841285088e42ffc6ba54b753aa17d9e147fef79d0639384c0dfeb072dc1f58a6c88e4419e51a7ed0129d896c880d56a7aef8d02d4ec69a553f420dfd57d4
@@ -0,0 +1,5 @@
1
+ README.md
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
@@ -0,0 +1,27 @@
1
+ ## MAC OS
2
+ .DS_Store
3
+
4
+ ## TEXTMATE
5
+ *.tmproj
6
+ tmtags
7
+
8
+ ## EMACS
9
+ *~
10
+ \#*
11
+ .\#*
12
+
13
+ ## VIM
14
+ *.swp
15
+
16
+ ## PROJECT::GENERAL
17
+ coverage
18
+ rdoc
19
+ pkg/*
20
+ *.gem
21
+ .bundle
22
+ Gemfile.lock
23
+
24
+ ## PROJECT::SPECIFIC
25
+ .yardoc/*
26
+ spec/db/*
27
+ doc/*
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'http://rubygems.org/'
2
+
3
+ # Specify your gem's dependencies in redisrank.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2011 Jim Myhrberg.
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,297 @@
1
+ A Redis-backed statistics storage and querying library written in Ruby.
2
+
3
+ Redisrank was created taking as reference a Gem called Redistat by Jimeh.
4
+ The motivations for the gem creation were similar to the Redistat too, I had a
5
+ collection solution which was MySQL-based with the following requirements.
6
+
7
+ * Fetch the top most ...
8
+ * This ranks should be fetched in any time-range
9
+ * Screamingly fast
10
+
11
+ ## Installation
12
+
13
+ gem install redisrank
14
+
15
+ If you are using Ruby 1.8.x, it's recommended you also install the
16
+ `SystemTimer` gem, as the Redis gem will otherwise complain.
17
+
18
+ ## Usage (Crash Course)
19
+
20
+ view\_stats.rb:
21
+
22
+ ```ruby
23
+ require 'redisrank'
24
+
25
+ class ViewRank
26
+ include Redisrank::Model
27
+ end
28
+
29
+ # if using Redisrank in multiple threads set this
30
+ # somewhere in the beginning of the execution stack
31
+ Redisrank.thread_safe = true
32
+ ```
33
+
34
+
35
+ ### Simple Example
36
+
37
+ Store:
38
+
39
+ ```ruby
40
+ ViewRank.store('hello', {:world => 4})
41
+ ViewRank.store('hello', {:world => 2}, 2.hours.ago)
42
+ ```
43
+
44
+ Fetch:
45
+
46
+ ```ruby
47
+ ViewRank.find('hello', 1.hour.ago, 1.hour.from_now).all
48
+ #=> [{'world' => 4}]
49
+ ViewRank.find('hello', 3.hour.ago, 1.hour.from_now).rank
50
+ #=> {'world' => 4}
51
+ ViewRank.find('hello', 3.hour.ago, 1.hour.ago).rank
52
+ #=> {'world' => 2}
53
+ ```
54
+
55
+ ### Other usefull Use Cases
56
+
57
+
58
+
59
+ ## Terminology
60
+
61
+ ### Scope
62
+
63
+ A type of global-namespace for storing data. When using the `Redisrank::Model`
64
+ wrapper, the scope is automatically set to the class name. In the examples
65
+ above, the scope is `ViewRank`. Can be overridden by calling the `#scope`
66
+ class method on your model class.
67
+
68
+ ### Label
69
+
70
+ Identifier string to separate different types and groups of statistics from
71
+ each other. The first argument of the `#store`, `#find`, and `#fetch` methods
72
+ is the label that you're storing to, or fetching from.
73
+
74
+ Labels support multiple grouping levels by splitting the label string with `/`
75
+ and storing the same stats for each level. For example, when storing data to a
76
+ label called `views/product/44`, the data is stored for the label you specify,
77
+ and also for `views/product` and `views`. You may also configure a different
78
+ group separator using the `Redisrank.group_separator=` method. For example:
79
+
80
+ ```ruby
81
+ Redisrank.group_separator = '|'
82
+ ```
83
+
84
+ A word of caution: Don't use a crazy number of group levels. As two levels
85
+ causes twice as many `hincrby` calls to Redis as not using the grouping
86
+ feature. Hence using 10 grouping levels, causes 10 times as many write calls
87
+ to Redis.
88
+
89
+ ### Input Statistics Data
90
+
91
+ You provide Redisrank with the data you want to store using a Ruby Hash. This
92
+ data is then stored in a corresponding Redis hash with identical key/field
93
+ names.
94
+
95
+ Key names in the hash also support grouping features similar to those
96
+ available for Labels. Again, the more levels you use, the more write calls to
97
+ Redis, so avoid using 10-15 levels.
98
+
99
+ ### Depth (Storage Accuracy)
100
+
101
+ Define how accurately data should be stored, and how accurately it's looked up
102
+ when fetching it again. By default Redisrank uses a depth value of `:hour`,
103
+ which means it's impossible to separate two events which were stored at 10:18
104
+ and 10:23. In Redis they are both stored within a date key of `2011031610`.
105
+
106
+ You can set depth within your model using the `#depth` class method. Available
107
+ depths are: `:year`, `:month`, `:day`, `:hour`, `:min`, `:sec`
108
+
109
+ ### Time Ranges
110
+
111
+ When you fetch data, you need to specify a start and an end time. The
112
+ selection behavior can seem a bit weird at first when, but makes sense when
113
+ you understand how Redisrank works internally.
114
+
115
+ For example, if we are using a Depth value of `:hour`, and we trigger a fetch
116
+ call starting at `1.hour.ago` (13:34), till `Time.now` (14:34), only stats
117
+ from 13:00:00 till 13:59:59 are returned, as they were all stored within the
118
+ key for the 13th hour. If both 13:00 and 14:00 was returned, you would get
119
+ results from two whole hours. Hence if you want up to the second data, use an
120
+ end time of `1.hour.from_now`.
121
+
122
+ ### The Finder Object
123
+
124
+ Calling the `#find` method on a Redisrank model class returns a
125
+ `Redisrank::Finder` object. The finder is a lazy-loaded gateway to your
126
+ data. Meaning you can create a new finder, and modify instantiated finder's
127
+ label, scope, dates, and more. It does not call Redis and fetch the data until
128
+ you call `#total`, `#all`, `#map`, `#each`, or `#each_with_index` on the
129
+ finder.
130
+
131
+ This section does need further expanding as there's a lot to cover when it
132
+ comes to the finder.
133
+
134
+
135
+ ## Key Expiry
136
+
137
+ Support for expiring keys from Redis is available, allowing you too keep
138
+ varying levels of details for X period of time. This allows you easily keep
139
+ things nice and tidy by only storing varying levels detailed stats only for as
140
+ long as you need.
141
+
142
+ In the below example we define how long Redis keys for varying depths are
143
+ stored. Second by second stats are available for 10 minutes, minute by minute
144
+ stats for 6 hours, hourly stats for 3 months, daily stats for 2 years, and
145
+ yearly stats are retained forever.
146
+
147
+ ```ruby
148
+ class ViewRank
149
+ include Redisrank::Model
150
+
151
+ depth :sec
152
+
153
+ expire \
154
+ :sec => 10.minutes.to_i,
155
+ :min => 6.hours.to_i,
156
+ :hour => 3.months.to_i,
157
+ :day => 2.years.to_i
158
+ end
159
+ ```
160
+
161
+ Keep in mind that when storing stats for a custom date in the past for
162
+ example, the expiry time for the keys will be relative to now. The values you
163
+ specify are simply passed to the `Redis#expire` method.
164
+
165
+
166
+ ## Internals
167
+
168
+ ### Storing / Writing
169
+
170
+ Redisrank stores all data into a Redis hash keys. The Redis key name the used
171
+ consists of three parts. The scope, label, and datetime:
172
+
173
+ {scope}/{label}:{datetime}
174
+
175
+ For example, this...
176
+
177
+ ```ruby
178
+ ViewRank.store('views/product/44', {'count/chrome/11' => 1})
179
+ ```
180
+
181
+ ...would store the follow hash of data...
182
+
183
+ ```ruby
184
+ { 'count' => 1, 'count/chrome' => 1, 'count/chrome/11' => 1 }
185
+ ```
186
+
187
+ ...to all 12 of these Redis hash keys...
188
+
189
+ ViewRank/views:2011
190
+ ViewRank/views:201103
191
+ ViewRank/views:20110315
192
+ ViewRank/views:2011031510
193
+ ViewRank/views/product:2011
194
+ ViewRank/views/product:201103
195
+ ViewRank/views/product:20110315
196
+ ViewRank/views/product:2011031510
197
+ ViewRank/views/product/44:2011
198
+ ViewRank/views/product/44:201103
199
+ ViewRank/views/product/44:20110315
200
+ ViewRank/views/product/44:2011031510
201
+
202
+ ...by creating the Redis key, and/or hash field if needed, otherwise it simply
203
+ increments the already existing data.
204
+
205
+ It would also create the following Redis sets to keep track of which child
206
+ labels are available:
207
+
208
+ ViewRank.label_index:
209
+ ViewRank.label_index:views
210
+ ViewRank.label_index:views/product
211
+
212
+ It should now be more obvious to you why you should think about how you use
213
+ the grouping capabilities so you don't go crazy and use 10-15 levels. Storing
214
+ is done through Redis' `hincrby` call, which only supports a single key/field
215
+ combo. Meaning the above example would call `hincrby` a total of 36 times to
216
+ store the data, and `sadd` a total of 3 times to ensure the label index is
217
+ accurate. 39 calls is however not a problem for Redis, most calls happen in
218
+ less than 0.15ms (0.00015 seconds) on my local machine.
219
+
220
+
221
+ ### Fetching / Reading
222
+
223
+ By default when fetching statistics, Redisrank will figure out how to do the
224
+ least number of reads from Redis. First it checks how long range you're
225
+ fetching. If whole days, months or years for example fit within the start and
226
+ end dates specified, it will fetch the one key for the day/month/year in
227
+ question. It further drills down to the smaller units.
228
+
229
+ It is also intelligent enough to not fetch each day from 3-31 of a month,
230
+ instead it would fetch the data for the whole month and the first two days,
231
+ which are then removed from the summary of the whole month. This means three
232
+ calls to `hgetall` instead of 29 if each whole day was fetched.
233
+
234
+ ### Buffer
235
+
236
+ The buffer is a new, still semi-beta, feature aimed to reduce the number of
237
+ Redis `hincrby` that Redisrank sends. This should only really be useful when
238
+ you're hitting north of 30,000 Redis requests per second, if your Redis server
239
+ has limited resources, or against my recommendation you've opted to use 10,
240
+ 20, or more label grouping levels.
241
+
242
+ Buffering tries to fold together multiple `store` calls into as few as
243
+ possible by merging the statistics hashes from all calls and groups them based
244
+ on scope, label, date depth, and more. You configure the the buffer by setting
245
+ `Redisrank.buffer_size` to an integer higher than 1. This basically tells
246
+ Redisrank how many `store` calls to buffer in memory before writing all data to
247
+ Redis.
248
+
249
+
250
+ ## Todo
251
+
252
+ * More details in Readme.
253
+ * Documentation.
254
+ * Anything else that becomes apparent after real-world use.
255
+
256
+
257
+ ## Credits
258
+
259
+ [Encore Alert](http://encorealert.com/) that allowed me to spend some
260
+ company time to further develop the project. @jimeh for creating the Redistat
261
+ that was not a inspiration but the base version of Redisrank.
262
+
263
+
264
+ ## Note on Patches/Pull Requests
265
+
266
+ * Fork the project.
267
+ * Make your feature addition or bug fix.
268
+ * Add tests for it. This is important so I don't break it in a
269
+ future version unintentionally.
270
+ * Commit, do not mess with rakefile, version, or history. (if you want to
271
+ have your own version, that is fine but bump version in a commit by itself I
272
+ can ignore when I pull)
273
+ * Send me a pull request. Bonus points for topic branches.
274
+
275
+
276
+ ## License and Copyright
277
+
278
+ Copyright (c) 2011 Jim Myhrberg.
279
+
280
+ Permission is hereby granted, free of charge, to any person obtaining
281
+ a copy of this software and associated documentation files (the
282
+ "Software"), to deal in the Software without restriction, including
283
+ without limitation the rights to use, copy, modify, merge, publish,
284
+ distribute, sublicense, and/or sell copies of the Software, and to
285
+ permit persons to whom the Software is furnished to do so, subject to
286
+ the following conditions:
287
+
288
+ The above copyright notice and this permission notice shall be
289
+ included in all copies or substantial portions of the Software.
290
+
291
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
292
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
293
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
294
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
295
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
296
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
297
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,69 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
3
+
4
+
5
+ #
6
+ # Rspec
7
+ #
8
+
9
+ require 'rspec/core/rake_task'
10
+ RSpec::Core::RakeTask.new(:spec) do |spec|
11
+ spec.pattern = 'spec/**/*_spec.rb'
12
+ end
13
+
14
+ RSpec::Core::RakeTask.new(:rcov) do |spec|
15
+ spec.pattern = 'spec/**/*_spec.rb'
16
+ spec.rcov = true
17
+ spec.rcov_opts = ['--exclude', 'spec']
18
+ end
19
+
20
+ task :default => [:start, :spec, :stop]
21
+
22
+
23
+ #
24
+ # Start/stop Redis test server
25
+ #
26
+
27
+ REDIS_DIR = File.expand_path(File.join("..", "spec"), __FILE__)
28
+ REDIS_CNF = File.join(REDIS_DIR, "redis-test.conf")
29
+ REDIS_PID = File.join(REDIS_DIR, "db", "redis.pid")
30
+
31
+ desc "Start the Redis test server"
32
+ task :start do
33
+ unless File.exists?(REDIS_PID)
34
+ system "redis-server #{REDIS_CNF}"
35
+ end
36
+ end
37
+
38
+ desc "Stop the Redis test server"
39
+ task :stop do
40
+ if File.exists?(REDIS_PID)
41
+ system "kill #{File.read(REDIS_PID)}"
42
+ system "rm #{REDIS_PID}"
43
+ end
44
+ end
45
+
46
+
47
+ #
48
+ # Yard
49
+ #
50
+
51
+ begin
52
+ require 'yard'
53
+ YARD::Rake::YardocTask.new
54
+ rescue LoadError
55
+ task :yardoc do
56
+ abort "YARD is not available. In order to run yardoc, you must: sudo gem install yard"
57
+ end
58
+ end
59
+
60
+
61
+ #
62
+ # Misc.
63
+ #
64
+
65
+ desc "Start an irb console with Redisrank pre-loaded."
66
+ task :console do
67
+ exec "irb -r spec/spec_helper"
68
+ end
69
+ task :c => :console
@@ -0,0 +1,106 @@
1
+
2
+ require 'rubygems'
3
+ require 'date'
4
+ require 'time'
5
+ require 'digest/sha1'
6
+ require 'monitor'
7
+
8
+ # Active Support 2.x or 3.x
9
+ require 'active_support'
10
+ if !{}.respond_to?(:with_indifferent_access)
11
+ require 'active_support/core_ext/hash/indifferent_access'
12
+ require 'active_support/core_ext/hash/reverse_merge'
13
+ end
14
+
15
+ require 'time_ext'
16
+ require 'redis'
17
+ require 'json'
18
+
19
+ require 'redisrank/mixins/options'
20
+ require 'redisrank/mixins/synchronize'
21
+ require 'redisrank/mixins/database'
22
+ require 'redisrank/mixins/date_helper'
23
+
24
+ require 'redisrank/connection'
25
+ require 'redisrank/buffer'
26
+ require 'redisrank/collection'
27
+ require 'redisrank/date'
28
+ require 'redisrank/event'
29
+ require 'redisrank/finder'
30
+ require 'redisrank/key'
31
+ require 'redisrank/label'
32
+ require 'redisrank/model'
33
+ require 'redisrank/result'
34
+ require 'redisrank/scope'
35
+ require 'redisrank/summary'
36
+ require 'redisrank/version'
37
+
38
+ require 'redisrank/core_ext'
39
+
40
+
41
+ module Redisrank
42
+
43
+ KEY_NEXT_ID = ".next_id"
44
+ KEY_EVENT = ".event:"
45
+ KEY_LABELS = "Redisrank.labels:" # used for reverse label hash lookup
46
+ KEY_EVENT_IDS = ".event_ids"
47
+ LABEL_INDEX = ".label_index:"
48
+ GROUP_SEPARATOR = "/"
49
+
50
+ class InvalidOptions < ArgumentError; end
51
+ class RedisServerIsTooOld < Exception; end
52
+
53
+ class << self
54
+
55
+ def buffer
56
+ Buffer.instance
57
+ end
58
+
59
+ def buffer_size
60
+ buffer.size
61
+ end
62
+
63
+ def buffer_size=(size)
64
+ buffer.size = size
65
+ end
66
+
67
+ def thread_safe
68
+ Synchronize.thread_safe
69
+ end
70
+
71
+ def thread_safe=(value)
72
+ Synchronize.thread_safe = value
73
+ end
74
+
75
+ def connection(ref = nil)
76
+ Connection.get(ref)
77
+ end
78
+ alias :redis :connection
79
+
80
+ def connection=(connection)
81
+ Connection.add(connection)
82
+ end
83
+ alias :redis= :connection=
84
+
85
+ def connect(options)
86
+ Connection.create(options)
87
+ end
88
+
89
+ def flush
90
+ puts "WARNING: Redisrank.flush is deprecated. Use Redisrank.redis.flushdb instead."
91
+ connection.flushdb
92
+ end
93
+
94
+ def group_separator
95
+ @group_separator ||= GROUP_SEPARATOR
96
+ end
97
+ attr_writer :group_separator
98
+
99
+ end
100
+ end
101
+
102
+
103
+ # ensure buffer is flushed on program exit
104
+ Kernel.at_exit do
105
+ Redisrank.buffer.flush(true)
106
+ end