minuteman 1.0.0.pre → 1.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile.lock +4 -1
- data/README.md +29 -18
- data/Rakefile +6 -1
- data/lib/minuteman.rb +81 -20
- data/lib/minuteman/bit_operations.rb +10 -4
- data/lib/minuteman/bit_operations/data.rb +3 -2
- data/lib/minuteman/bit_operations/operation.rb +8 -4
- data/lib/minuteman/bit_operations/plain.rb +8 -4
- data/lib/minuteman/bit_operations/result.rb +3 -2
- data/lib/minuteman/bit_operations/with_data.rb +32 -12
- data/lib/minuteman/keys_methods.rb +2 -0
- data/lib/minuteman/time_events.rb +4 -3
- data/lib/minuteman/time_span.rb +6 -4
- data/lib/minuteman/time_spans/day.rb +4 -0
- data/lib/minuteman/time_spans/hour.rb +4 -0
- data/lib/minuteman/time_spans/minute.rb +4 -0
- data/lib/minuteman/time_spans/month.rb +4 -0
- data/lib/minuteman/time_spans/week.rb +4 -0
- data/lib/minuteman/time_spans/year.rb +4 -0
- data/minuteman.gemspec +4 -2
- data/test/bench/minuteman_bench.rb +37 -0
- data/test/test_helper.rb +1 -0
- data/test/unit/minuteman_test.rb +61 -6
- metadata +25 -6
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
minuteman (1.0.
|
4
|
+
minuteman (1.0.2)
|
5
5
|
redis (~> 3.0.2)
|
6
6
|
|
7
7
|
GEM
|
@@ -10,6 +10,8 @@ GEM
|
|
10
10
|
minitest (4.2.0)
|
11
11
|
rake (0.9.2.2)
|
12
12
|
redis (3.0.2)
|
13
|
+
redis-namespace (1.2.1)
|
14
|
+
redis (~> 3.0.0)
|
13
15
|
|
14
16
|
PLATFORMS
|
15
17
|
ruby
|
@@ -18,3 +20,4 @@ DEPENDENCIES
|
|
18
20
|
minitest (~> 4.2.0)
|
19
21
|
minuteman!
|
20
22
|
rake
|
23
|
+
redis-namespace (~> 1.2.1)
|
data/README.md
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
![Minuteman](http://elcuervo.github.com/minuteman/img/minuteman-readme.png)
|
2
|
+
|
1
3
|
# Minuteman
|
2
4
|
[![Code Climate](https://codeclimate.com/badge.png)](https://codeclimate.com/github/elcuervo/minuteman)
|
3
5
|
[![Build Status](https://secure.travis-ci.org/elcuervo/minuteman.png?branch=master)](https://travis-ci.org/elcuervo/minuteman)
|
@@ -8,10 +10,6 @@ during the American Revolutionary War. _They provided a highly mobile, rapidly
|
|
8
10
|
deployed force that allowed the colonies to respond immediately to war threats,
|
9
11
|
hence the name._
|
10
12
|
|
11
|
-
![Minuteman](http://upload.wikimedia.org/wikipedia/commons/thumb/4/4b/Minute_Man_Statue_Lexington_Massachusetts_cropped.jpg/220px-Minute_Man_Statue_Lexington_Massachusetts_cropped.jpg)
|
12
|
-
|
13
|
-
Fast analytics using Redis bitwise operations
|
14
|
-
|
15
13
|
## Origin
|
16
14
|
Freenode - #cuba.rb - 2012/10/30 15:20 UYT
|
17
15
|
|
@@ -58,29 +56,42 @@ gem install minuteman
|
|
58
56
|
|
59
57
|
## Usage
|
60
58
|
|
59
|
+
Currently Minutemen supports two options `:silent (default: false)` and `:redis
|
60
|
+
(default: {})`
|
61
|
+
|
62
|
+
### Options
|
63
|
+
|
64
|
+
**silent**: when `true` the operations will not raise errors to prevent failures
|
65
|
+
|
66
|
+
**redis**: can be a Hash with the options to be sent to `Redis.new` or a `Redis`
|
67
|
+
connection already established (`Redis::Namespace` works as well).
|
68
|
+
|
61
69
|
```ruby
|
62
70
|
require "minuteman"
|
63
71
|
|
64
|
-
# Accepts an options hash that will be sent as is to Redis.new
|
72
|
+
# Accepts an options[:redis] hash that will be sent as is to Redis.new
|
65
73
|
analytics = Minuteman.new
|
66
74
|
|
75
|
+
# You can also reuse your Redis or Redis::Namespace connection
|
76
|
+
analytics = Minuteman.new(redis: Redis::Namespace.new(:mm, redis: Redis.new))
|
77
|
+
|
67
78
|
# Mark an event for a given id
|
68
|
-
analytics.
|
69
|
-
analytics.
|
79
|
+
analytics.track("login:successful", user.id)
|
80
|
+
analytics.track("login:successful", other_user.id)
|
70
81
|
|
71
82
|
# Mark in bulk
|
72
|
-
analytics.
|
83
|
+
analytics.track("programming:love:ruby", User.where(favorite: "ruby").pluck(:id))
|
73
84
|
|
74
|
-
# Fetch events for a given time
|
85
|
+
# Fetch events for a given time (default is Time.now.utc)
|
75
86
|
today_events = analytics.day("login:successful", Time.now.utc)
|
76
87
|
|
77
88
|
# This also exists
|
78
|
-
analytics.year("login:successful"
|
79
|
-
analytics.month("login:successful"
|
80
|
-
analytics.week("login:successful"
|
81
|
-
analytics.day("login:successful"
|
82
|
-
analytics.hour("login:successful"
|
83
|
-
analytics.minute("login:successful"
|
89
|
+
analytics.year("login:successful")
|
90
|
+
analytics.month("login:successful")
|
91
|
+
analytics.week("login:successful")
|
92
|
+
analytics.day("login:successful")
|
93
|
+
analytics.hour("login:successful")
|
94
|
+
analytics.minute("login:successful")
|
84
95
|
|
85
96
|
# Lists all the tracked events
|
86
97
|
analytics.events
|
@@ -123,7 +134,7 @@ set1 ^ set2
|
|
123
134
|
Let's assume this scenario:
|
124
135
|
|
125
136
|
You have a list of users and want to know which of them have been going throught
|
126
|
-
some of the
|
137
|
+
some of the tracks you made.
|
127
138
|
|
128
139
|
```ruby
|
129
140
|
paid = analytics.month("buy:complete")
|
@@ -138,8 +149,8 @@ Currently the supported commands to interact with arrays are `&` and `-`
|
|
138
149
|
### Example
|
139
150
|
|
140
151
|
```ruby
|
141
|
-
invited = analytics.month("email:invitation"
|
142
|
-
successful_buys = analytics.month("buy:complete"
|
152
|
+
invited = analytics.month("email:invitation")
|
153
|
+
successful_buys = analytics.month("buy:complete")
|
143
154
|
|
144
155
|
successful_buys_after_invitation = invited & successful_buys
|
145
156
|
successful_buys_after_invitation.include?(user.id)
|
data/Rakefile
CHANGED
@@ -4,5 +4,10 @@ Rake::TestTask.new("spec") do |t|
|
|
4
4
|
t.pattern = "test/**/*_test.rb"
|
5
5
|
end
|
6
6
|
|
7
|
+
Rake::TestTask.new("bench") do |t|
|
8
|
+
t.pattern = "test/bench/*_bench.rb"
|
9
|
+
end
|
10
|
+
|
7
11
|
task :default => [:test]
|
8
|
-
task :
|
12
|
+
task :all => [:test, :bench]
|
13
|
+
task :test => [:spec]
|
data/lib/minuteman.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
require "redis"
|
2
2
|
require "time"
|
3
|
+
require "forwardable"
|
3
4
|
require "minuteman/time_events"
|
4
5
|
|
5
6
|
# Until redis gem gets updated
|
@@ -17,17 +18,36 @@ class Redis
|
|
17
18
|
end
|
18
19
|
end
|
19
20
|
|
21
|
+
# Public: Minuteman core classs
|
22
|
+
#
|
20
23
|
class Minuteman
|
21
|
-
|
24
|
+
extend Forwardable
|
25
|
+
|
26
|
+
class << self
|
27
|
+
attr_accessor :redis, :options
|
28
|
+
|
29
|
+
# Public: Prevents a fatal error if the options are set to silent
|
30
|
+
#
|
31
|
+
def safe(&block)
|
32
|
+
yield if block
|
33
|
+
rescue Redis::BaseError => e
|
34
|
+
raise e unless options[:silent]
|
35
|
+
end
|
36
|
+
end
|
22
37
|
|
23
38
|
PREFIX = "minuteman"
|
24
39
|
|
40
|
+
def_delegators self, :redis, :redis=, :options, :options=, :safe
|
41
|
+
|
25
42
|
# Public: Initializes Minuteman
|
26
43
|
#
|
27
|
-
#
|
44
|
+
# options - An options hash to change how Minuteman behaves
|
28
45
|
#
|
29
46
|
def initialize(options = {})
|
30
|
-
|
47
|
+
redis_options = options.delete(:redis) || {}
|
48
|
+
|
49
|
+
self.options = default_options.merge!(options)
|
50
|
+
self.redis = define_connection(redis_options)
|
31
51
|
end
|
32
52
|
|
33
53
|
# Public: Generates the methods to fech data
|
@@ -36,9 +56,12 @@ class Minuteman
|
|
36
56
|
# date - A Time object used to do the search
|
37
57
|
#
|
38
58
|
%w[year month week day hour minute].each do |method_name|
|
39
|
-
define_method(method_name) do |
|
59
|
+
define_method(method_name) do |*args|
|
60
|
+
event_name, date = *args
|
61
|
+
date ||= Time.now.utc
|
62
|
+
|
40
63
|
constructor = self.class.const_get(method_name.capitalize)
|
41
|
-
constructor.new(
|
64
|
+
constructor.new(event_name, date)
|
42
65
|
end
|
43
66
|
end
|
44
67
|
|
@@ -50,49 +73,87 @@ class Minuteman
|
|
50
73
|
# Examples
|
51
74
|
#
|
52
75
|
# analytics = Minuteman.new
|
53
|
-
# analytics.
|
54
|
-
# analytics.
|
76
|
+
# analytics.track("login", 1)
|
77
|
+
# analytics.track("login", [2, 3, 4])
|
55
78
|
#
|
56
|
-
def
|
79
|
+
def track(event_name, ids, time = Time.now.utc)
|
57
80
|
event_time = time.kind_of?(Time) ? time : Time.parse(time.to_s)
|
58
|
-
time_events = TimeEvents.start(
|
81
|
+
time_events = TimeEvents.start(event_name, event_time)
|
59
82
|
|
60
|
-
|
61
|
-
time_events.each do |event|
|
62
|
-
Array(ids).each { |id| redis.setbit(event.key, id, 1) }
|
63
|
-
end
|
64
|
-
end
|
83
|
+
track_events(time_events, Array(ids))
|
65
84
|
end
|
66
85
|
|
67
86
|
# Public: List all the events given the minuteman namespace
|
68
87
|
#
|
69
88
|
def events
|
70
|
-
keys =
|
89
|
+
keys = safe { redis.keys([PREFIX, "*", "????"].join("_")) }
|
71
90
|
keys.map { |key| key.split("_")[1] }
|
72
91
|
end
|
73
92
|
|
74
93
|
# Public: List all the operations executed in a given the minuteman namespace
|
75
94
|
#
|
76
95
|
def operations
|
77
|
-
|
96
|
+
safe { redis.keys([operations_cache_key_prefix, "*"].join("_")) }
|
78
97
|
end
|
79
98
|
|
80
99
|
# Public: Resets the bit operation cache keys
|
81
100
|
#
|
82
101
|
def reset_operations_cache
|
83
|
-
keys =
|
84
|
-
|
102
|
+
keys = safe { redis.keys([operations_cache_key_prefix, "*"].join("_")) }
|
103
|
+
safe { redis.del(keys) } if keys.any?
|
85
104
|
end
|
86
105
|
|
87
106
|
# Public: Resets all the used keys
|
88
107
|
#
|
89
108
|
def reset_all
|
90
|
-
keys =
|
91
|
-
|
109
|
+
keys = safe { redis.keys([PREFIX, "*"].join("_")) }
|
110
|
+
safe { redis.del(keys) } if keys.any?
|
92
111
|
end
|
93
112
|
|
94
113
|
private
|
95
114
|
|
115
|
+
# Private: Default configuration options
|
116
|
+
#
|
117
|
+
def default_options
|
118
|
+
{
|
119
|
+
cache: true,
|
120
|
+
silent: false
|
121
|
+
}
|
122
|
+
end
|
123
|
+
|
124
|
+
# Private: Determines to use or instance a Redis connection
|
125
|
+
#
|
126
|
+
# object: Can be the options to instance a Redis connection or a connection
|
127
|
+
# itself
|
128
|
+
#
|
129
|
+
def define_connection(object)
|
130
|
+
case object
|
131
|
+
when Redis, defined?(Redis::Namespace) && Redis::Namespace
|
132
|
+
object
|
133
|
+
else
|
134
|
+
Redis.new(object)
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
# Private: Marks ids for a given time events
|
139
|
+
#
|
140
|
+
# time_events: A set of TimeEvents
|
141
|
+
# ids: The ids to be tracked
|
142
|
+
#
|
143
|
+
def track_events(time_events, ids)
|
144
|
+
safe_multi do
|
145
|
+
time_events.each do |event|
|
146
|
+
ids.each { |id| safe { redis.setbit(event.key, id, 1) } }
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
# Private: Executes a block within a safe connection using redis.multi
|
152
|
+
#
|
153
|
+
def safe_multi(&block)
|
154
|
+
safe { redis.multi(&block) }
|
155
|
+
end
|
156
|
+
|
96
157
|
# Private: The prefix key of all the operations
|
97
158
|
#
|
98
159
|
def operations_cache_key_prefix
|
@@ -1,7 +1,13 @@
|
|
1
1
|
require "minuteman/bit_operations/operation"
|
2
2
|
|
3
|
+
# Public: Minuteman core classs
|
4
|
+
#
|
3
5
|
class Minuteman
|
4
6
|
module BitOperations
|
7
|
+
extend Forwardable
|
8
|
+
|
9
|
+
def_delegators :Minuteman, :safe, :redis
|
10
|
+
|
5
11
|
# Public: Checks for the existance of ids on a given set
|
6
12
|
#
|
7
13
|
# ids - Array of ids
|
@@ -14,13 +20,13 @@ class Minuteman
|
|
14
20
|
# Public: Resets the current key
|
15
21
|
#
|
16
22
|
def reset
|
17
|
-
redis.rem(key)
|
23
|
+
safe { redis.rem(key) }
|
18
24
|
end
|
19
25
|
|
20
26
|
# Public: Cheks for the amount of ids stored on the current key
|
21
27
|
#
|
22
28
|
def length
|
23
|
-
redis.bitcount(key)
|
29
|
+
safe { redis.bitcount(key) }
|
24
30
|
end
|
25
31
|
|
26
32
|
# Public: Calculates the NOT of the current key
|
@@ -70,7 +76,7 @@ class Minuteman
|
|
70
76
|
# id: The bit
|
71
77
|
#
|
72
78
|
def getbit(id)
|
73
|
-
redis.getbit(key, id) == 1
|
79
|
+
safe { redis.getbit(key, id) == 1 }
|
74
80
|
end
|
75
81
|
|
76
82
|
# Private: Cxecutes an operation between the current timespan and another
|
@@ -85,7 +91,7 @@ class Minuteman
|
|
85
91
|
# Private: Memoizes the operation class
|
86
92
|
#
|
87
93
|
def operate
|
88
|
-
@_operate ||= Operation.new(
|
94
|
+
@_operate ||= Operation.new(self)
|
89
95
|
end
|
90
96
|
end
|
91
97
|
end
|
@@ -1,14 +1,15 @@
|
|
1
1
|
require "minuteman/bit_operations"
|
2
2
|
|
3
|
+
# Public: Minuteman core classs
|
4
|
+
#
|
3
5
|
class Minuteman
|
4
6
|
module BitOperations
|
5
7
|
# Public: The conversion of an array to an operable class
|
6
8
|
#
|
7
|
-
# redis - The Redis connection
|
8
9
|
# key - The key where the result it's stored
|
9
10
|
# data - The original data of the intersection
|
10
11
|
#
|
11
|
-
class Data < Struct.new(:
|
12
|
+
class Data < Struct.new(:key, :data)
|
12
13
|
include BitOperations
|
13
14
|
include Enumerable
|
14
15
|
|
@@ -1,16 +1,17 @@
|
|
1
1
|
require "minuteman/bit_operations/plain"
|
2
2
|
require "minuteman/bit_operations/with_data"
|
3
3
|
|
4
|
+
# Public: Minuteman core classs
|
5
|
+
#
|
4
6
|
class Minuteman
|
5
7
|
module BitOperations
|
6
8
|
# Public: Handles the operations between two timespans
|
7
9
|
#
|
8
|
-
# redis: The Redis connection
|
9
10
|
# type: The operation type
|
10
11
|
# timespan: One of the timespans to be permuted
|
11
12
|
# other: The other timespan to be permuted
|
12
13
|
#
|
13
|
-
class Operation < Struct.new(:
|
14
|
+
class Operation < Struct.new(:timespan)
|
14
15
|
# Public: Caches operations against Array
|
15
16
|
#
|
16
17
|
class Cache
|
@@ -68,7 +69,7 @@ class Minuteman
|
|
68
69
|
return minus_operation if type == "MINUS" && operable?
|
69
70
|
return cache[other] if cache.include?(other)
|
70
71
|
|
71
|
-
caching { klass.new(
|
72
|
+
caching { klass.new(type, other, timespan.key).call }
|
72
73
|
end
|
73
74
|
|
74
75
|
private
|
@@ -83,7 +84,10 @@ class Minuteman
|
|
83
84
|
#
|
84
85
|
def caching
|
85
86
|
executed_class = yield
|
86
|
-
|
87
|
+
if other.is_a?(Array) && Minuteman.options[:cache]
|
88
|
+
cache[other] = executed_class
|
89
|
+
end
|
90
|
+
|
87
91
|
executed_class
|
88
92
|
end
|
89
93
|
|
@@ -1,18 +1,22 @@
|
|
1
1
|
require "minuteman/keys_methods"
|
2
2
|
require "minuteman/bit_operations/result"
|
3
3
|
|
4
|
+
# Public: Minuteman core classs
|
5
|
+
#
|
4
6
|
class Minuteman
|
5
7
|
module BitOperations
|
6
8
|
# Public: The class to handle operations with others timespans
|
7
9
|
#
|
8
|
-
# redis: The Redis connection
|
9
10
|
# type: The operation type
|
10
11
|
# timespan: The timespan to be permuted
|
11
12
|
# source_key: The original key to do the operation
|
12
13
|
#
|
13
|
-
class Plain < Struct.new(:
|
14
|
+
class Plain < Struct.new(:type, :timespan, :source_key)
|
15
|
+
extend Forwardable
|
14
16
|
include KeysMethods
|
15
17
|
|
18
|
+
def_delegators :Minuteman, :redis, :safe
|
19
|
+
|
16
20
|
def call
|
17
21
|
events = if source_key == timespan
|
18
22
|
Array(source_key)
|
@@ -21,9 +25,9 @@ class Minuteman
|
|
21
25
|
end
|
22
26
|
|
23
27
|
key = destination_key(type, events)
|
24
|
-
redis.bitop(type, key, events)
|
28
|
+
safe { redis.bitop(type, key, events) }
|
25
29
|
|
26
|
-
Result.new(
|
30
|
+
Result.new(key)
|
27
31
|
end
|
28
32
|
end
|
29
33
|
end
|
@@ -1,13 +1,14 @@
|
|
1
1
|
require "minuteman/bit_operations"
|
2
2
|
|
3
|
+
# Public: Minuteman core classs
|
4
|
+
#
|
3
5
|
class Minuteman
|
4
6
|
module BitOperations
|
5
7
|
# Public: The result of intersecting results
|
6
8
|
#
|
7
|
-
# redis - The Redis connection
|
8
9
|
# key - The key where the result it's stored
|
9
10
|
#
|
10
|
-
class Result < Struct.new(:
|
11
|
+
class Result < Struct.new(:key)
|
11
12
|
include BitOperations
|
12
13
|
end
|
13
14
|
end
|
@@ -1,35 +1,55 @@
|
|
1
1
|
require "minuteman/keys_methods"
|
2
2
|
require "minuteman/bit_operations/data"
|
3
3
|
|
4
|
+
# Public: Minuteman core classs
|
5
|
+
#
|
4
6
|
class Minuteman
|
5
7
|
module BitOperations
|
6
8
|
# Public: The class to handle operations with datasets
|
7
9
|
#
|
8
|
-
# redis: The Redis connection
|
9
10
|
# type: The operation type
|
10
11
|
# data: The data to be permuted
|
11
12
|
# source_key: The original key to do the operation
|
12
13
|
#
|
13
|
-
class WithData < Struct.new(:
|
14
|
+
class WithData < Struct.new(:type, :data, :source_key)
|
15
|
+
extend Forwardable
|
14
16
|
include KeysMethods
|
15
17
|
|
18
|
+
def_delegators :Minuteman, :redis, :safe
|
19
|
+
|
16
20
|
def call
|
17
|
-
normalized_data = Array(data)
|
18
21
|
key = destination_key("data-#{type}", normalized_data)
|
19
|
-
command = case type
|
20
|
-
when "AND" then :select
|
21
|
-
when "MINUS" then :reject
|
22
|
-
end
|
23
22
|
|
24
|
-
|
25
|
-
redis.
|
23
|
+
if !safe { redis.exists(key) }
|
24
|
+
intersected_data.each { |id| safe { redis.setbit(key, id, 1) } }
|
26
25
|
end
|
27
26
|
|
28
|
-
|
29
|
-
|
27
|
+
Data.new(key, intersected_data)
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
# Private: Normalized data
|
33
|
+
#
|
34
|
+
def normalized_data
|
35
|
+
Array(data)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Private: Defines command to get executed based on the type
|
39
|
+
#
|
40
|
+
def command
|
41
|
+
case type
|
42
|
+
when "AND" then :select
|
43
|
+
when "MINUS" then :reject
|
30
44
|
end
|
45
|
+
end
|
31
46
|
|
32
|
-
|
47
|
+
# Private: The intersected data depending on the command executed
|
48
|
+
#
|
49
|
+
def intersected_data
|
50
|
+
normalized_data.send(command) do |id|
|
51
|
+
Minuteman.redis.getbit(source_key, id) == 1
|
52
|
+
end
|
33
53
|
end
|
34
54
|
end
|
35
55
|
end
|
@@ -1,16 +1,17 @@
|
|
1
1
|
require "minuteman/time_spans"
|
2
2
|
|
3
|
+
# Public: Minuteman core classs
|
4
|
+
#
|
3
5
|
class Minuteman
|
4
6
|
module TimeEvents
|
5
7
|
# Public: Helper to get all the time trackers ready
|
6
8
|
#
|
7
|
-
# redis - The Redis connection
|
8
9
|
# event_name - The event to be tracked
|
9
10
|
# date - A given Time object
|
10
11
|
#
|
11
|
-
def self.start(
|
12
|
+
def self.start(event_name, time)
|
12
13
|
[Year, Month, Week, Day, Hour, Minute].map do |t|
|
13
|
-
t.new(
|
14
|
+
t.new(event_name, time)
|
14
15
|
end
|
15
16
|
end
|
16
17
|
end
|
data/lib/minuteman/time_span.rb
CHANGED
@@ -1,22 +1,24 @@
|
|
1
1
|
require "minuteman/bit_operations"
|
2
2
|
|
3
|
+
# Public: Minuteman core classs
|
4
|
+
#
|
3
5
|
class Minuteman
|
6
|
+
# Public: The timespan class. All the time span classes inherit from this one
|
7
|
+
#
|
4
8
|
class TimeSpan
|
5
9
|
include BitOperations
|
6
10
|
|
7
|
-
attr_reader :key
|
11
|
+
attr_reader :key
|
8
12
|
|
9
13
|
DATE_FORMAT = "%s-%02d-%02d"
|
10
14
|
TIME_FORMAT = "%02d:%02d"
|
11
15
|
|
12
16
|
# Public: Initializes the base TimeSpan class
|
13
17
|
#
|
14
|
-
# redis - The Redis connection
|
15
18
|
# event_name - The event to be tracked
|
16
19
|
# date - A given Time object
|
17
20
|
#
|
18
|
-
def initialize(
|
19
|
-
@redis = redis
|
21
|
+
def initialize(event_name, date)
|
20
22
|
@key = build_key(event_name, time_format(date))
|
21
23
|
end
|
22
24
|
|
data/minuteman.gemspec
CHANGED
@@ -1,9 +1,10 @@
|
|
1
1
|
Gem::Specification.new do |s|
|
2
2
|
s.name = "minuteman"
|
3
|
-
s.version = "1.0.
|
3
|
+
s.version = "1.0.2"
|
4
4
|
s.summary = "Bit Analytics"
|
5
5
|
s.description = "Fast and furious tracking system using Redis bitwise operations"
|
6
6
|
s.authors = ["elcuervo"]
|
7
|
+
s.licenses = ["MIT"]
|
7
8
|
s.email = ["yo@brunoaguirre.com"]
|
8
9
|
s.homepage = "http://github.com/elcuervo/minuteman"
|
9
10
|
s.files = `git ls-files`.split("\n")
|
@@ -11,5 +12,6 @@ Gem::Specification.new do |s|
|
|
11
12
|
|
12
13
|
s.add_dependency("redis", "~> 3.0.2")
|
13
14
|
|
14
|
-
s.add_development_dependency("minitest",
|
15
|
+
s.add_development_dependency("minitest", "~> 4.2.0")
|
16
|
+
s.add_development_dependency("redis-namespace", "~> 1.2.1")
|
15
17
|
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require_relative "../test_helper"
|
2
|
+
require "minitest/benchmark"
|
3
|
+
|
4
|
+
describe Minuteman do
|
5
|
+
before do
|
6
|
+
today = Time.now.utc
|
7
|
+
last_week = today - (3600 * 24 * 7)
|
8
|
+
|
9
|
+
@analytics = Minuteman.new
|
10
|
+
@analytics.mark("login", 12)
|
11
|
+
@analytics.mark("login", [2, 42])
|
12
|
+
@analytics.mark("login:successful", 567, last_week)
|
13
|
+
|
14
|
+
@week_events = @analytics.week("login")
|
15
|
+
@last_week_events = @analytics.week("login", last_week)
|
16
|
+
@last_week_events2 = @analytics.month("login:successful", last_week)
|
17
|
+
end
|
18
|
+
|
19
|
+
bench_performance_constant("AND") { @week_events & @last_week_events }
|
20
|
+
bench_performance_constant("OR") { @week_events | @last_week_events }
|
21
|
+
bench_performance_constant("XOR") { @week_events ^ @last_week_events }
|
22
|
+
bench_performance_constant("NOT") { ~@week_events }
|
23
|
+
bench_performance_constant("MINUS") { @week_events - @last_week_events }
|
24
|
+
|
25
|
+
bench_performance_constant "complex operations" do
|
26
|
+
@week_events & (@last_week_events ^ @last_week_events2)
|
27
|
+
end
|
28
|
+
|
29
|
+
bench_performance_constant "intersections using cache" do
|
30
|
+
5.times { @week_events & [2, 12, 43] }
|
31
|
+
end
|
32
|
+
|
33
|
+
bench_performance_constant "intersections not using cache" do
|
34
|
+
@analytics.options[:cache] = false
|
35
|
+
5.times { @week_events & [2, 12, 43] }
|
36
|
+
end
|
37
|
+
end
|
data/test/test_helper.rb
CHANGED
data/test/unit/minuteman_test.rb
CHANGED
@@ -5,14 +5,14 @@ describe Minuteman do
|
|
5
5
|
@analytics = Minuteman.new
|
6
6
|
|
7
7
|
today = Time.now.utc
|
8
|
-
last_month
|
9
|
-
last_week
|
8
|
+
last_month = today - (3600 * 24 * 30)
|
9
|
+
last_week = today - (3600 * 24 * 7)
|
10
10
|
last_minute = today - 120
|
11
11
|
|
12
|
-
@analytics.
|
13
|
-
@analytics.
|
14
|
-
@analytics.
|
15
|
-
@analytics.
|
12
|
+
@analytics.track("login", 12)
|
13
|
+
@analytics.track("login", [2, 42])
|
14
|
+
@analytics.track("login", 2, last_week)
|
15
|
+
@analytics.track("login:successful", 567, last_month)
|
16
16
|
|
17
17
|
@year_events = @analytics.year("login", today)
|
18
18
|
@week_events = @analytics.week("login", today)
|
@@ -160,3 +160,58 @@ describe Minuteman do
|
|
160
160
|
assert_equal 2, ids.size
|
161
161
|
end
|
162
162
|
end
|
163
|
+
|
164
|
+
describe "Using options" do
|
165
|
+
it "should be able to stop using the cache" do
|
166
|
+
minuteman = Minuteman.new
|
167
|
+
assert_equal true, minuteman.options[:cache]
|
168
|
+
|
169
|
+
minuteman.options[:cache] = false
|
170
|
+
|
171
|
+
assert_equal false, minuteman.options[:cache]
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
describe "Changing Minuteman redis connections" do
|
176
|
+
it "should support using a Redis instance" do
|
177
|
+
redis = Redis.new
|
178
|
+
minuteman = Minuteman.new(redis: redis)
|
179
|
+
|
180
|
+
assert_equal redis, Minuteman.redis
|
181
|
+
assert_equal redis, minuteman.redis
|
182
|
+
end
|
183
|
+
|
184
|
+
it "should support changing the current connection" do
|
185
|
+
redis = Redis.new
|
186
|
+
minuteman = Minuteman.new
|
187
|
+
|
188
|
+
assert redis != Minuteman.redis
|
189
|
+
|
190
|
+
minuteman.redis = redis
|
191
|
+
|
192
|
+
assert_equal redis, minuteman.redis
|
193
|
+
end
|
194
|
+
|
195
|
+
it "should support Redis::Namespace" do
|
196
|
+
namespace = Redis::Namespace.new(:ns, redis: Redis.new)
|
197
|
+
|
198
|
+
minuteman = Minuteman.new(redis: namespace)
|
199
|
+
|
200
|
+
assert_equal namespace, Minuteman.redis
|
201
|
+
assert_equal namespace, minuteman.redis
|
202
|
+
end
|
203
|
+
|
204
|
+
it "should fail silently" do
|
205
|
+
minuteman = Minuteman.new(silent: true, redis: { port: 1234 })
|
206
|
+
|
207
|
+
minuteman.track("test", 1)
|
208
|
+
end
|
209
|
+
|
210
|
+
it "should fail loudly" do
|
211
|
+
minuteman = Minuteman.new(redis: { port: 1234 })
|
212
|
+
|
213
|
+
assert_raises Redis::CannotConnectError do
|
214
|
+
minuteman.track("test", 1)
|
215
|
+
end
|
216
|
+
end
|
217
|
+
end
|
metadata
CHANGED
@@ -1,15 +1,15 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: minuteman
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.
|
5
|
-
prerelease:
|
4
|
+
version: 1.0.2
|
5
|
+
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
8
8
|
- elcuervo
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-11-
|
12
|
+
date: 2012-11-29 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: redis
|
@@ -43,6 +43,22 @@ dependencies:
|
|
43
43
|
- - ~>
|
44
44
|
- !ruby/object:Gem::Version
|
45
45
|
version: 4.2.0
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: redis-namespace
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ~>
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: 1.2.1
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ~>
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 1.2.1
|
46
62
|
description: Fast and furious tracking system using Redis bitwise operations
|
47
63
|
email:
|
48
64
|
- yo@brunoaguirre.com
|
@@ -74,10 +90,12 @@ files:
|
|
74
90
|
- lib/minuteman/time_spans/week.rb
|
75
91
|
- lib/minuteman/time_spans/year.rb
|
76
92
|
- minuteman.gemspec
|
93
|
+
- test/bench/minuteman_bench.rb
|
77
94
|
- test/test_helper.rb
|
78
95
|
- test/unit/minuteman_test.rb
|
79
96
|
homepage: http://github.com/elcuervo/minuteman
|
80
|
-
licenses:
|
97
|
+
licenses:
|
98
|
+
- MIT
|
81
99
|
post_install_message:
|
82
100
|
rdoc_options: []
|
83
101
|
require_paths:
|
@@ -91,9 +109,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
91
109
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
92
110
|
none: false
|
93
111
|
requirements:
|
94
|
-
- - ! '
|
112
|
+
- - ! '>='
|
95
113
|
- !ruby/object:Gem::Version
|
96
|
-
version:
|
114
|
+
version: '0'
|
97
115
|
requirements: []
|
98
116
|
rubyforge_project:
|
99
117
|
rubygems_version: 1.8.23
|
@@ -101,5 +119,6 @@ signing_key:
|
|
101
119
|
specification_version: 3
|
102
120
|
summary: Bit Analytics
|
103
121
|
test_files:
|
122
|
+
- test/bench/minuteman_bench.rb
|
104
123
|
- test/test_helper.rb
|
105
124
|
- test/unit/minuteman_test.rb
|