em-bucketer 0.1.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: eaaf6c54984c095f03204b6682e10fb6a357ac82
4
- data.tar.gz: 011accd36e8332183ddb1b4028416027c81e10cb
3
+ metadata.gz: aa43207fc878f4d9f0f15851590ec66803f45694
4
+ data.tar.gz: 9cd15644641ffccdd96552f45bbc5b420de5a262
5
5
  SHA512:
6
- metadata.gz: a9ee2a1101c26baf9bce9383079a7192251231fccb02cceca8d46c2d90d6aa1e1cdeb9ce22228812fbfa56b707ae603a2b92d0241dc923e66828921a12847258
7
- data.tar.gz: 985c4c1b96dee1b3ecba7e94aa7779c13733397aea0e3ef3fec52f63d9d9614e2f4894a95fd833453ca4892e0213bb6ce0f5f6c119b9181f6e443081b784a8a4
6
+ metadata.gz: 7b4ee20c2e7ad8cb933b5a0de2dfea6cd9b05ee7375cd156ea8659feda615890e8a6d52490e63dafb6d71e44b992eb33b066889b207b21efe693862fcff19fd7
7
+ data.tar.gz: 96447882b8d6cdf6a20dc62a25f69af698de3708eb523735fcec40ae503edab3237b919ba72aa69ae9861d2350e7ec9b3a0110383eb5e857db439ed075e44a0d
data/README.md CHANGED
@@ -21,7 +21,43 @@ Or install it yourself as:
21
21
 
22
22
  $ gem install em-bucketer
23
23
 
24
- ## Usage
24
+ ## Ordered Bucketer
25
+ The ordered bucketer adds items to buckets and gives them back in the same
26
+ order in which they were put in.
27
+
28
+ ### Usage
29
+
30
+ ```ruby
31
+ require 'em-bucketer'
32
+ EM.run do
33
+ bucketer = EM::Bucketer::Ordered::InMemory.new(:bucket_threshold_size => 5)
34
+
35
+ bucketer.on_bucket_full do |bucket_id|
36
+ p "yay bucket #{bucket_id} filled up!"
37
+
38
+ bucketer.pop_all(bucket_id) do |items|
39
+ EM.stop
40
+ items.each do |item|
41
+ p "got back #{item}"
42
+ end
43
+ end
44
+ end
45
+
46
+ bucketer.add_item("1", {:foo => :bar})
47
+ bucketer.add_item("1", {:foo => :bar})
48
+ bucketer.add_item("1", {:foo => :bar})
49
+ bucketer.add_item("1", {:bar => :foo})
50
+ bucketer.add_item("1", {:bar => :foo})
51
+ end
52
+ ```
53
+
54
+ ## Unordered Bucketer
55
+ The unordered bucketer requires you to pass in an 'id' which is used to ensure
56
+ that no duplicates occur. This bucketer type should be used if you want to not
57
+ have two of the same item in a bucket at one time. This type of bucketer does
58
+ not guarantee that you get back items in the same order that they went in.
59
+
60
+ ### Usage
25
61
 
26
62
  ```ruby
27
63
  require 'em-bucketer'
@@ -153,11 +153,9 @@ module EventMachine::Bucketer
153
153
  private
154
154
 
155
155
  def get_and_remove_iterator(bucket_id, count, values, completion)
156
- added = 0
157
156
  proc do |tuple, iter|
158
157
  key, val = tuple[0], tuple[1]
159
- if added < count
160
- added += 1
158
+ if values.count < count
161
159
  values << val
162
160
  iter.next
163
161
  else
@@ -0,0 +1,8 @@
1
+ module EventMachine::Bucketer
2
+ module Ordered
3
+ end
4
+ end
5
+
6
+ require 'eventmachine'
7
+ require 'em-bucketer/ordered/in_memory'
8
+ #require 'em-bucketer/ordered/redis'
@@ -0,0 +1,191 @@
1
+ module EventMachine::Bucketer
2
+ module Ordered::Base
3
+ def setup(bucket_threshold_size, bucket_max_age)
4
+ @bucket_threshold_size = bucket_threshold_size
5
+ @bucket_max_age = bucket_max_age
6
+ @buckets = {}
7
+ @on_bucket_full_callbacks = []
8
+ @on_bucket_timeout_callbacks = []
9
+ @timers = {}
10
+ end
11
+
12
+ # Adds a item to the specified bucket and
13
+ # calls the block when it is done
14
+ #
15
+ # @param bucket_id [String] the bucket id of
16
+ # the bucket to put the item in
17
+ # @param item [Object] the item to be
18
+ # placed in the bucket
19
+ def add_item(bucket_id, item, &blk)
20
+ add_timer_if_first(bucket_id)
21
+ EM::Completion.new.tap do |c|
22
+ c.callback(&blk) if block_given?
23
+ add_item_to_db(bucket_id, item).callback do
24
+ c.succeed
25
+ check_bucket_full(bucket_id)
26
+ end.errback do |e|
27
+ c.fail e
28
+ end
29
+ end
30
+ end
31
+
32
+ # Get at most `count` items back from a
33
+ # specific bucket and remove them from
34
+ # the bucket. These should be the first
35
+ # `count` items you added to the bucket
36
+ # that have not yet been removed from the
37
+ # bucket.
38
+ #
39
+ # @param bucket_id [String] the bucket id
40
+ # you want the items from
41
+ # @param count [Integer] the number of
42
+ # items you want from the bucket
43
+ # @yield [Array] the first `count`
44
+ # items in the bucket
45
+ def pop_count(bucket_id, count, reset_timer: true, &blk)
46
+ EM::Completion.new.tap do |c|
47
+ c.callback(&blk) if block_given?
48
+ pop_count_from_db(bucket_id, count, &blk).callback do |items|
49
+ reset_timer(bucket_id) if reset_timer
50
+ c.succeed items
51
+ end.errback do |e|
52
+ c.fail e
53
+ end
54
+ end
55
+ end
56
+
57
+ # Get all items back from a specific bucket
58
+ # and remove them from the bucket.
59
+ #
60
+ # @param bucket_id [String] the bucket id
61
+ # you want the items from
62
+ # @yield [Array] all items in the bucket
63
+ def pop_all(bucket_id, &blk)
64
+ clear_timer(bucket_id)
65
+ EM::Completion.new.tap do |c|
66
+ c.callback(&blk) if block_given?
67
+ pop_all_from_db(bucket_id, &blk).callback do |items|
68
+ c.succeed items
69
+ end.errback do |e|
70
+ c.fail e
71
+ end
72
+ end
73
+ end
74
+
75
+ # Used to set a callback hook for when a bucket
76
+ # reaches the threshold size. It is IMPORTANT
77
+ # to note that the bucket will not automatically
78
+ # be emptied you must call empty_bucket if you
79
+ # want the bucket to be emptied. Also the callback
80
+ # will be called every time a item is added
81
+ # until the bucket is emptied.
82
+ #
83
+ # @yield [String] The bucket id of the full bucket
84
+ def on_bucket_full(&blk)
85
+ @on_bucket_full_callbacks << blk
86
+ end
87
+
88
+ # Used to set a callback hook for when a bucket
89
+ # reaches the time limit. It is IMPORTANT
90
+ # to note that the bucket will not automatically
91
+ # be emptied you must call empty_bucket if you
92
+ # want the bucket to be emptied.
93
+ #
94
+ # This timer is started once the bucket gets its
95
+ # first item and is cleared only when the
96
+ # bucket is emptied. The callback will only be
97
+ # called once at this time and then not again
98
+ # unless you empty the bucket and add something
99
+ # again.
100
+ #
101
+ # @yield [String] The bucket id of the full bucket
102
+ def on_bucket_timeout(&blk)
103
+ @on_bucket_timeout_callbacks << blk
104
+ end
105
+
106
+ # Get the contents of a bucket.
107
+ #
108
+ # @param bucket_id [String] the bucket id
109
+ # of the bucket you want to get
110
+ # @yield [Array] the items you put
111
+ # into the bucket
112
+ def get_bucket(bucket_id, &blk)
113
+ get_bucket_from_db(bucket_id, &blk)
114
+ end
115
+
116
+ # Empty a bucket
117
+ #
118
+ # @param bucket_id [String] the bucket id
119
+ # of the bucket you want to empty
120
+ def empty_bucket(bucket_id, &blk)
121
+ EM::Completion.new.tap do |c|
122
+ c.callback(&blk) if block_given?
123
+ empty_bucket_in_db(bucket_id).callback do
124
+ clear_timer(bucket_id)
125
+ c.succeed
126
+ end.errback do |e|
127
+ c.fail e
128
+ end
129
+ end
130
+ end
131
+
132
+ private
133
+
134
+ def get_and_remove_iterator(bucket_id, count, values, completion)
135
+ proc do |tuple, iter|
136
+ key, val = tuple[0], tuple[1]
137
+ if values.count < count
138
+ values << val
139
+ iter.next
140
+ else
141
+ add_item(bucket_id, key, val).callback do
142
+ iter.next
143
+ end.errback do |e|
144
+ completion.fail e
145
+ end
146
+ end
147
+ end
148
+ end
149
+
150
+ def bucket_full?(bucket_id, &blk)
151
+ bucket_size_from_db(bucket_id).callback do |size|
152
+ blk.call size >= @bucket_threshold_size
153
+ end
154
+ end
155
+
156
+ def check_bucket_full(bucket_id)
157
+ bucket_full?(bucket_id) do |is_full|
158
+ if is_full
159
+ @on_bucket_full_callbacks.each do |callback|
160
+ callback.call bucket_id
161
+ end
162
+ end
163
+ end
164
+ end
165
+
166
+ def add_timer_if_first(bucket_id)
167
+ return unless @bucket_max_age
168
+ @timers[bucket_id] ||= EM::Timer.new(@bucket_max_age, timeout_callback(bucket_id))
169
+ end
170
+
171
+ def timeout_callback(bucket_id)
172
+ proc do |bar|
173
+ @on_bucket_timeout_callbacks.each do |callback|
174
+ callback.call bucket_id
175
+ end
176
+ end
177
+ end
178
+
179
+ def clear_timer(bucket_id)
180
+ return unless @bucket_max_age
181
+ timer = @timers.delete(bucket_id)
182
+ timer.cancel if timer
183
+ end
184
+
185
+ def reset_timer(bucket_id)
186
+ return unless @bucket_max_age
187
+ clear_timer(bucket_id)
188
+ @timers[bucket_id] = EM::Timer.new(@bucket_max_age, timeout_callback(bucket_id))
189
+ end
190
+ end
191
+ end
@@ -0,0 +1,6 @@
1
+ module EventMachine::Bucketer
2
+ module Ordered
3
+ module Database
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,64 @@
1
+ require 'em-bucketer/ordered/database'
2
+
3
+ module EventMachine::Bucketer
4
+ module Ordered
5
+ module Database
6
+ module Hash
7
+ private
8
+
9
+ def pop_all_from_db(bucket_id, &blk)
10
+ EM::Completion.new.tap do |c|
11
+ c.callback(&blk) if block_given?
12
+ all = bucket_by_id(bucket_id)
13
+ @buckets[bucket_id] = []
14
+ c.succeed all
15
+ end
16
+ end
17
+
18
+ def pop_count_from_db(bucket_id, count, &blk)
19
+ EM::Completion.new.tap do |c|
20
+ c.callback(&blk) if block_given?
21
+ all = bucket_by_id(bucket_id)
22
+ result = all.first(count)
23
+ @buckets[bucket_id] = all[count..-1]
24
+ c.succeed result
25
+ end
26
+ end
27
+
28
+ def bucket_size_from_db(bucket_id, &blk)
29
+ EM::Completion.new.tap do |c|
30
+ c.callback(&blk) if block_given?
31
+ c.succeed bucket_by_id(bucket_id).count
32
+ end
33
+ end
34
+
35
+ def add_item_to_db(bucket_id, item, &blk)
36
+ EM::Completion.new.tap do |c|
37
+ c.callback(&blk) if block_given?
38
+ bucket_by_id(bucket_id) << item
39
+ c.succeed
40
+ end
41
+ end
42
+
43
+ def get_bucket_from_db(bucket_id, &blk)
44
+ EM::Completion.new.tap do |c|
45
+ c.callback(&blk) if block_given?
46
+ c.succeed bucket_by_id(bucket_id)
47
+ end
48
+ end
49
+
50
+ def empty_bucket_in_db(bucket_id, &blk)
51
+ EM::Completion.new.tap do |c|
52
+ c.callback(&blk) if block_given?
53
+ @buckets[bucket_id] = []
54
+ c.succeed
55
+ end
56
+ end
57
+
58
+ def bucket_by_id(bucket_id)
59
+ @buckets[bucket_id] ||= []
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,24 @@
1
+ require 'eventmachine'
2
+ require 'em-bucketer/ordered/database/hash'
3
+ require 'em-bucketer/ordered/base'
4
+
5
+ module EventMachine::Bucketer
6
+ class Ordered::InMemory
7
+ include Ordered::Database::Hash
8
+ include Ordered::Base
9
+
10
+ BUCKET_THRESHOLD_SIZE_DEFAULT = 1000
11
+ BUCKET_MAX_AGE_DEFAULT = 3600
12
+
13
+ # Creates a new in memory Bucketer with the requested
14
+ # configurations
15
+ #
16
+ # @param bucket_threshold_size [Integer] the max size of the bucket
17
+ # after which the on_bucket_full callback is called
18
+ # @param bucket_max_age [Integer] max number of seconds a bucket
19
+ # can remain before the on_bucket_timed_out is called
20
+ def initialize(bucket_threshold_size: BUCKET_THRESHOLD_SIZE_DEFAULT, bucket_max_age: BUCKET_MAX_AGE_DEFAULT)
21
+ setup(bucket_threshold_size, bucket_max_age)
22
+ end
23
+ end
24
+ end
@@ -1,5 +1,5 @@
1
1
  module EventMachine
2
2
  module Bucketer
3
- VERSION = "0.1.1"
3
+ VERSION = "0.2.0"
4
4
  end
5
5
  end
@@ -0,0 +1,133 @@
1
+ require 'spec_helper'
2
+ require 'em-bucketer/ordered'
3
+
4
+ shared_examples "an ordered bucketer" do
5
+ describe '#add_item' do
6
+ it 'adds a item to the bucket' do
7
+ EM.run do
8
+ EM.add_timer(0.1) { fail "didn't reach EM.stop" }
9
+ bucketer.add_item("1", {:foo => :bar}) do
10
+ bucketer.get_bucket("1") do |bucket|
11
+ expect(bucket).to eq([{:foo => :bar}])
12
+ EM.stop
13
+ end
14
+ end
15
+ end
16
+ end
17
+
18
+ it 'handles multiple buckets' do
19
+ EM.run do
20
+ EM.add_timer(0.1) { fail "didn't reach EM.stop" }
21
+ bucketer.on_bucket_full do |bucket_id|
22
+ fail "shouldn't have called full"
23
+ end
24
+
25
+ add_n_items_ordered(bucketer, "1", 3) do
26
+ add_n_items_ordered(bucketer, "2", 3) do
27
+
28
+ bucketer.get_bucket("1") do |bucket|
29
+ expect(bucket).to eq([{:id => 0}, {:id => 1}, {:id => 2}])
30
+ EM.stop
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+
37
+ it 'calls on_bucket_full when a bucket fills up' do
38
+ EM.run do
39
+ EM.add_timer(0.1) { fail "didn't reach EM.stop" }
40
+ bucketer.on_bucket_full do |bucket_id|
41
+ expect(bucket_id).to eq("1")
42
+ EM.stop
43
+ end
44
+
45
+ add_n_items_ordered(bucketer, "1", 5)
46
+ end
47
+ end
48
+ end
49
+
50
+ describe '#empty_bucket' do
51
+ it 'emptys a bucket' do
52
+ EM.run do
53
+ EM.add_timer(0.1) { fail "didn't reach EM.stop" }
54
+ add_n_items_ordered(bucketer, "1", 3) do
55
+ bucketer.empty_bucket("1") do
56
+ bucketer.get_bucket("1") do |bucket|
57
+ expect(bucket).to eq([])
58
+ EM.stop
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
65
+
66
+ describe '#on_bucket_timeout' do
67
+ it 'calls the block when the timer times out' do
68
+ ran = false
69
+ EM.run do
70
+ EM.add_timer(0.05) { EM.stop }
71
+ # Stub out the timer that the bucketer uses
72
+ allow(EM).to receive(:add_timer) { |age, callback| callback.call }
73
+ bucketer.on_bucket_timeout do |bucket_id|
74
+ ran = true
75
+ expect(bucket_id).to eq("1")
76
+ end
77
+ bucketer.add_item("1", :foo => :bar)
78
+ end
79
+ fail "didn't call timeout" unless ran
80
+ end
81
+
82
+ it 'doesnt call the block when the timer doesnt time out' do
83
+ EM.run do
84
+ EM.add_timer(0.1) { EM.stop }
85
+ allow(EM).to receive(:add_timer)
86
+ bucketer.on_bucket_timeout do |bucket_id|
87
+ fail "shouldn't have called timeout"
88
+ end
89
+ bucketer.add_item("1", :foo => :bar)
90
+ end
91
+ end
92
+ end
93
+
94
+ describe '#pop_all' do
95
+ it 'gets and emptys the bucket' do
96
+ EM.run do
97
+ EM.add_timer(0.1) { fail "didn't reach EM.stop" }
98
+ add_n_items_ordered(bucketer, "1", 3) do
99
+
100
+ bucketer.pop_all("1") do |bucket|
101
+ expect(bucket).to eq([{:id => 0}, {:id => 1}, {:id => 2}])
102
+
103
+ bucketer.get_bucket("1") do |empty_bucket|
104
+ expect(empty_bucket).to eq([])
105
+ EM.stop
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
112
+
113
+ describe '#pop_count' do
114
+ it 'gets 2 items and removes them' do
115
+ EM.run do
116
+ EM.add_timer(0.1) { fail "didn't reach EM.stop" }
117
+ add_n_items_ordered(bucketer, "1", 3) do
118
+ bucketer.get_bucket("1") do |total_bucket|
119
+ expect(total_bucket.count).to eq(3)
120
+ bucketer.pop_count("1", 2) do |bucket|
121
+ expect(bucket.count).to eq(2)
122
+
123
+ bucketer.get_bucket("1") do |remaining_bucket|
124
+ expect(remaining_bucket.count).to eq(1)
125
+ EM.stop
126
+ end
127
+ end
128
+ end
129
+ end
130
+ end
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,8 @@
1
+ require 'spec_helper'
2
+ require 'em-bucketer/ordered'
3
+
4
+ describe EventMachine::Bucketer::Ordered::InMemory do
5
+ it_behaves_like "an ordered bucketer" do
6
+ let(:bucketer) { EM::Bucketer::Ordered::InMemory.new(:bucket_threshold_size => 5) }
7
+ end
8
+ end
@@ -4,6 +4,7 @@ require 'rspec'
4
4
  require 'pry'
5
5
  require 'spec_methods'
6
6
  require 'em_bucketer_examples'
7
+ require 'em_bucketer_ordered_examples'
7
8
 
8
9
  RSpec.configure do |config|
9
10
  config.order = :rand
@@ -7,4 +7,13 @@ module SpecMethods
7
7
  end
8
8
  EM::Iterator.new(0...n).each(worker, blk)
9
9
  end
10
+
11
+ def add_n_items_ordered(bucketer, bucket, n, &blk)
12
+ worker = proc do |i, iter|
13
+ bucketer.add_item(bucket, {:id => i}) do
14
+ iter.next
15
+ end
16
+ end
17
+ EM::Iterator.new(0...n).each(worker, blk)
18
+ end
10
19
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: em-bucketer
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Richard Heycock
@@ -143,9 +143,16 @@ files:
143
143
  - lib/em-bucketer/database/hash.rb
144
144
  - lib/em-bucketer/database/redis.rb
145
145
  - lib/em-bucketer/in_memory.rb
146
+ - lib/em-bucketer/ordered.rb
147
+ - lib/em-bucketer/ordered/base.rb
148
+ - lib/em-bucketer/ordered/database.rb
149
+ - lib/em-bucketer/ordered/database/hash.rb
150
+ - lib/em-bucketer/ordered/in_memory.rb
146
151
  - lib/em-bucketer/redis.rb
147
152
  - lib/em-bucketer/version.rb
148
153
  - spec/em_bucketer_examples.rb
154
+ - spec/em_bucketer_ordered_examples.rb
155
+ - spec/in_memory_ordered_spec.rb
149
156
  - spec/in_memory_spec.rb
150
157
  - spec/redis_spec.rb
151
158
  - spec/spec_helper.rb
@@ -177,6 +184,8 @@ summary: A generic eventmachine library for storing arbitrary objects in buckets
177
184
  callbacks on threshold reached
178
185
  test_files:
179
186
  - spec/em_bucketer_examples.rb
187
+ - spec/em_bucketer_ordered_examples.rb
188
+ - spec/in_memory_ordered_spec.rb
180
189
  - spec/in_memory_spec.rb
181
190
  - spec/redis_spec.rb
182
191
  - spec/spec_helper.rb