redisrank 0.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.
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