em-bucketer 0.1.1 → 0.2.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.
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