queue-bus 0.10.0 → 0.13.1

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
  SHA256:
3
- metadata.gz: 315c7263eb40e8d6165b96b8f50738ff5861127f35f1ce282f7b1d8a20914290
4
- data.tar.gz: 067034c67ce6232de23fe552c1fc3bac50ca64dd898dec9b4958009ce6067fe9
3
+ metadata.gz: 5383fd5290386ad57b383532a59406032e960ee344dc218db4c6c2e83a8fc8df
4
+ data.tar.gz: 6e695fc97a76265c6fa98984278e517b5e21dc7142c7d7b9ff646e02e52af5ef
5
5
  SHA512:
6
- metadata.gz: dfea248252d2b17eca07eb5dae5e7feb4e32ac8673f154f959f0a622221dd466470f0b243f2e8c07191c951897b646c8341e5df3933556361dc1fd831de483a6
7
- data.tar.gz: 6585cc65f3bb494161fc18e45d421203278db133b3471747caa8d526da7a551b524a6ba6289320f4a6f7fe80566f6bb7436dadda267d69d96497c7e2f14e64a2
6
+ metadata.gz: 7a238383614d2505facac1c2003d8b55cff3d3c68c1bd0a136ce5444cbae92377df9fd9316feb912180bc2a9272f30257dcc028c345178cc56c18f6c9fee8aa5
7
+ data.tar.gz: 507b3514db5c2290e4c2cedf60d77cfed61f8fc9b4b0ae1c647de40350fdd52342144b4bdba32ad94fc5f734fd4413d80f7517ff083ebda0d8f1b28984c28421
data/CHANGELOG.md CHANGED
@@ -6,6 +6,33 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.13.1]
10
+
11
+ ### Fixes
12
+
13
+ - Allows matching on 0 via the `on_heartbeat` subscription
14
+
15
+ ### Added
16
+
17
+ - Allows matching on `wday` via the `on_heartbeat` subscription
18
+
19
+ ## [0.13.0]
20
+
21
+ ### Added
22
+
23
+ - Adds `Dispatch#on_heartbeat` which is a helper function for specifying heartbeat subscriptions.
24
+
25
+ ## [0.12.0]
26
+
27
+ ### Changed
28
+ - Pipelines fetching all queue subscriptions when using `QueueBus::Application.all`
29
+
30
+ ## [0.11.0]
31
+
32
+ ### Added
33
+
34
+ - Adds `QueueBus.in_context` method. Useful when working with a multithreaded environment to add a description for all events published within this scope.
35
+
9
36
  ## [0.10.0]
10
37
 
11
38
  ### Added
data/README.mdown CHANGED
@@ -63,6 +63,16 @@ QueueBus.dispatch("app_b") do
63
63
  subscribe "my_key", { "user_id" => :present, "page" => "homepage"} do
64
64
  Mixpanel.homepage_action!(attributes["action"])
65
65
  end
66
+
67
+ # You may also declare a subscription to heartbeat events. This is a helper function
68
+ # that works along with subscribe to make scheduling regular events easier.
69
+ #
70
+ # minute_interval: Executes every n minutes
71
+ # hour_interval: Executes every n hours
72
+ # minute: Executes on this minute
73
+ # hour: Executes on this hour
74
+ on_heartbeat "my_heartbeat_event", minute_interval: 5 do |attributes|
75
+ end
66
76
  end
67
77
  ```
68
78
 
@@ -141,13 +151,16 @@ event to the appropriate code block.
141
151
  You can also say `QueueBus.local_mode = :suppress` to turn off publishing altogether.
142
152
  This can be helpful inside some sort of migration, for example.
143
153
 
144
- #### Thread Safe Local Modes
154
+ #### Thread Safe Options
145
155
 
146
156
  **!! This is important if you are using workers that utilize multiple threads, such as Sidekiq !!**
147
157
 
148
158
  The above setting is global to the ruby process and modifying it will impact all threads that are
149
159
  currently using QueueBus. If you want to isolate a thread or block of code from QueueBus, you can
150
- use the method `with_local_mode`:
160
+ use the methods `with_local_mode` or `in_context`:
161
+
162
+
163
+ With local mode
151
164
 
152
165
  ```ruby
153
166
  QueueBus.with_local_mode(:suppress) do
@@ -155,7 +168,16 @@ QueueBus.with_local_mode(:suppress) do
155
168
  end
156
169
  ```
157
170
 
158
- The previous value of `local_mode` will be restored after the block exits.
171
+ In context
172
+
173
+ ```ruby
174
+ QueueBus.in_context('some_context') do
175
+ # Context attribute will be set for all events published within this scope.
176
+ end
177
+ ```
178
+
179
+
180
+ The previous values will be restored after the block exits.
159
181
 
160
182
  ### TODO
161
183
 
data/lib/queue-bus.rb CHANGED
@@ -43,7 +43,8 @@ module QueueBus
43
43
  :hostname=, :hostname,
44
44
  :adapter=, :adapter, :has_adapter?,
45
45
  :incoming_queue=, :incoming_queue,
46
- :redis, :worker_middleware_stack
46
+ :redis, :worker_middleware_stack,
47
+ :context=, :context, :in_context
47
48
 
48
49
  def_delegators :_dispatchers, :dispatch, :dispatchers, :dispatcher_by_key, :dispatcher_execute
49
50
 
@@ -7,7 +7,22 @@ module QueueBus
7
7
  class << self
8
8
  def all
9
9
  # note the names arent the same as we started with
10
- ::QueueBus.redis { |redis| redis.smembers(app_list_key).collect { |val| new(val) } }
10
+ ::QueueBus.redis do |redis|
11
+ app_keys = redis.smembers(app_list_key)
12
+ apps = app_keys.collect { |val| new(val) }
13
+
14
+ hashes = redis.pipelined do
15
+ apps.each do |app|
16
+ redis.hgetall(app.redis_key)
17
+ end
18
+ end
19
+
20
+ apps.zip(hashes).each do |app, hash|
21
+ app._hydrate_redis_hash(hash)
22
+ end
23
+
24
+ apps
25
+ end
11
26
  end
12
27
  end
13
28
 
@@ -90,6 +105,10 @@ module QueueBus
90
105
  out
91
106
  end
92
107
 
108
+ def _hydrate_redis_hash(hash)
109
+ @raw_redis_hash = hash
110
+ end
111
+
93
112
  protected
94
113
 
95
114
  def self.normalize(val)
@@ -114,16 +133,24 @@ module QueueBus
114
133
 
115
134
  def read_redis_hash
116
135
  out = {}
117
- ::QueueBus.redis do |redis|
118
- redis.hgetall(redis_key).each do |key, val|
119
- begin
120
- out[key] = ::QueueBus::Util.decode(val)
121
- rescue ::QueueBus::Util::DecodeException
122
- out[key] = val
123
- end
136
+ raw_redis_hash.each do |key, val|
137
+ begin
138
+ out[key] = ::QueueBus::Util.decode(val)
139
+ rescue ::QueueBus::Util::DecodeException
140
+ out[key] = val
124
141
  end
125
142
  end
126
143
  out
127
144
  end
145
+
146
+ private
147
+
148
+ def raw_redis_hash
149
+ return @raw_redis_hash if @raw_redis_hash
150
+
151
+ ::QueueBus.redis do |redis|
152
+ redis.hgetall(redis_key)
153
+ end
154
+ end
128
155
  end
129
156
  end
@@ -9,7 +9,7 @@ module QueueBus
9
9
  attr_accessor :default_queue, :hostname, :incoming_queue, :logger
10
10
 
11
11
  attr_reader :worker_middleware_stack
12
- attr_writer :local_mode
12
+ attr_writer :local_mode, :context
13
13
 
14
14
  def initialize
15
15
  @worker_middleware_stack = QueueBus::Middleware::Stack.new
@@ -24,6 +24,7 @@ module QueueBus
24
24
  Wrap = Struct.new(:value)
25
25
 
26
26
  LOCAL_MODE_VAR = :queue_bus_local_mode
27
+ CONTEXT_VAR = :queue_bus_context
27
28
 
28
29
  # Returns the current local mode of QueueBus
29
30
  def local_mode
@@ -34,6 +35,15 @@ module QueueBus
34
35
  end
35
36
  end
36
37
 
38
+ # Returns the current context of QueueBus
39
+ def context
40
+ if Thread.current.thread_variable_get(CONTEXT_VAR).is_a?(Wrap)
41
+ Thread.current.thread_variable_get(CONTEXT_VAR).value
42
+ else
43
+ @context
44
+ end
45
+ end
46
+
37
47
  # Overrides the current local mode for the duration of a block. This is a threadsafe
38
48
  # implementation. After, the global setting will be resumed.
39
49
  #
@@ -46,6 +56,17 @@ module QueueBus
46
56
  Thread.current.thread_variable_set(LOCAL_MODE_VAR, previous)
47
57
  end
48
58
 
59
+ # Overrides the current bus context (if any) for the duration of a block, adding a
60
+ # `bus_context` attribute set to this value for all events published in this scope.
61
+ # This is a threadsafe implementation. After, the global setting will be resumed.
62
+ def in_context(context)
63
+ previous = Thread.current.thread_variable_get(CONTEXT_VAR)
64
+ Thread.current.thread_variable_set(CONTEXT_VAR, Wrap.new(context))
65
+ yield if block_given?
66
+ ensure
67
+ Thread.current.thread_variable_set(CONTEXT_VAR, previous)
68
+ end
69
+
49
70
  def adapter=(val)
50
71
  raise "Adapter already set to #{@adapter_instance.class.name}" if has_adapter?
51
72
 
@@ -16,6 +16,45 @@ module QueueBus
16
16
  @subscriptions.size
17
17
  end
18
18
 
19
+ def on_heartbeat(key, minute: nil, hour: nil, wday: nil, minute_interval: nil, hour_interval: nil, &block) # rubocop:disable Metrics/PerceivedComplexity, Metrics/MethodLength, Metrics/ParameterLists, Metrics/CyclomaticComplexity, Metrics/AbcSize, Metrics/LineLength
20
+ if minute_interval && !minute_interval.positive?
21
+ raise ArgumentError, 'minute_interval must be a positive integer'
22
+ end
23
+
24
+ if hour_interval && !hour_interval.positive?
25
+ raise ArgumentError, 'hour_interval must be a positive integer'
26
+ end
27
+
28
+ matcher = { bus_event_type: :heartbeat_minutes }
29
+
30
+ if minute
31
+ raise ArgumentError, 'minute must not be negative' if minute.negative?
32
+
33
+ matcher['minute'] = minute
34
+ end
35
+
36
+ if hour
37
+ raise ArgumentError, 'hour must not be negative' if hour.negative?
38
+
39
+ matcher['hour'] = hour
40
+ end
41
+
42
+ if wday
43
+ raise ArgumentError, 'wday must not be negative' if wday.negative?
44
+
45
+ matcher['wday'] = wday
46
+ end
47
+
48
+ subscribe(key, matcher) do |event|
49
+ if (minute_interval.nil? || (event['minute'] % minute_interval).zero?) &&
50
+ (hour_interval.nil? || (event['hour'] % hour_interval).zero?)
51
+
52
+ # Yield the block passed in.
53
+ block.call
54
+ end
55
+ end
56
+ end
57
+
19
58
  def subscribe(key, matcher_hash = nil, &block)
20
59
  dispatch_event('default', key, matcher_hash, block)
21
60
  end
@@ -30,6 +30,7 @@ module QueueBus
30
30
  bus_attr = { 'bus_published_at' => Time.now.to_i, 'bus_event_type' => event_type }
31
31
  bus_attr['bus_id'] = "#{Time.now.to_i}-#{generate_uuid}"
32
32
  bus_attr['bus_app_hostname'] = ::QueueBus.hostname
33
+ bus_attr['bus_context'] = ::QueueBus.context unless ::QueueBus.context.nil?
33
34
  if defined?(I18n) && I18n.respond_to?(:locale) && I18n.locale
34
35
  bus_attr['bus_locale'] = I18n.locale.to_s
35
36
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module QueueBus
4
- VERSION = '0.10.0'
4
+ VERSION = '0.13.1'
5
5
  end
data/spec/config_spec.rb CHANGED
@@ -61,6 +61,41 @@ describe 'QueueBus config' do
61
61
  end
62
62
  end
63
63
 
64
+ describe '#in_context' do
65
+ it 'sets the context on the thread' do
66
+ QueueBus.in_context(:batch_processing) do
67
+ expect(QueueBus.context).to eq(:batch_processing)
68
+ Thread.new { expect(QueueBus.context).to eq nil }.join
69
+ end
70
+ end
71
+
72
+ it 'supports nesting' do
73
+ QueueBus.in_context(:batch_processing) do
74
+ expect(QueueBus.context).to eq :batch_processing
75
+ QueueBus.in_context(:processing) do
76
+ expect(QueueBus.context).to eq :processing
77
+ end
78
+ expect(QueueBus.context).to eq :batch_processing
79
+ end
80
+ end
81
+
82
+ it 'respects an override of nil' do
83
+ QueueBus.context = :batch_processing
84
+ QueueBus.in_context(nil) do
85
+ expect(QueueBus.context).to eq nil
86
+ end
87
+ QueueBus.context = :batch_processing
88
+ end
89
+
90
+ it 'resets to the original context after the block' do
91
+ expect(QueueBus.context).to eq nil
92
+ QueueBus.in_context(:batch_processing) do
93
+ expect(QueueBus.context).to eq :batch_processing
94
+ end
95
+ expect(QueueBus.context).to eq nil
96
+ end
97
+ end
98
+
64
99
  it 'sets the hostname' do
65
100
  expect(QueueBus.hostname).not_to eq(nil)
66
101
  QueueBus.hostname = 'whatever'
@@ -28,6 +28,152 @@ module QueueBus
28
28
  end.not_to raise_error
29
29
  end
30
30
 
31
+ describe '#on_heartbeat' do
32
+ let(:dispatch) { Dispatch.new('heartbeat') }
33
+ let(:event) { { bus_event_type: :heartbeat_minutes } }
34
+ let(:event_name) { 'my-event' }
35
+
36
+ context 'when not declaring anything' do
37
+ before do
38
+ dispatch.on_heartbeat event_name do |_event|
39
+ Runner2.run({})
40
+ end
41
+ end
42
+
43
+ it 'runs on every heart beat' do
44
+ (0..24).each do |hour|
45
+ (0..60).each do |minute|
46
+ expect do
47
+ dispatch.execute(
48
+ event_name, event.merge('hour' => hour, 'minute' => minute)
49
+ )
50
+ end.to change(Runner2, :value).by(1)
51
+ end
52
+ end
53
+ end
54
+ end
55
+
56
+ context 'when running on hour 8' do
57
+ before do
58
+ dispatch.on_heartbeat event_name, hour: 8 do |_event|
59
+ Runner2.run({})
60
+ end
61
+ end
62
+
63
+ it 'subscribes to hour 8' do
64
+ expect(dispatch.subscriptions.all.first.matcher.filters)
65
+ .to eq('bus_event_type' => 'heartbeat_minutes', 'hour' => '8')
66
+ end
67
+ end
68
+
69
+ context 'when running on minute 4' do
70
+ before do
71
+ dispatch.on_heartbeat event_name, minute: 4 do |_event|
72
+ Runner2.run({})
73
+ end
74
+ end
75
+
76
+ it 'subscribes to minute 4' do
77
+ expect(dispatch.subscriptions.all.first.matcher.filters)
78
+ .to eq('bus_event_type' => 'heartbeat_minutes', 'minute' => '4')
79
+ end
80
+ end
81
+
82
+ context 'when running on minute 4 and hour 8' do
83
+ before do
84
+ dispatch.on_heartbeat event_name, hour: 8, minute: 4 do |_event|
85
+ Runner2.run({})
86
+ end
87
+ end
88
+
89
+ it 'subscribes to minute 4 and hour 8' do
90
+ expect(dispatch.subscriptions.all.first.matcher.filters)
91
+ .to eq('bus_event_type' => 'heartbeat_minutes', 'minute' => '4', 'hour' => '8')
92
+ end
93
+ end
94
+
95
+ context 'when running on wday 2' do
96
+ before do
97
+ dispatch.on_heartbeat event_name, wday: 2 do |_event|
98
+ Runner2.run({})
99
+ end
100
+ end
101
+
102
+ it 'subscribes to wday 2' do
103
+ expect(dispatch.subscriptions.all.first.matcher.filters)
104
+ .to eq('bus_event_type' => 'heartbeat_minutes', 'wday' => '2')
105
+ end
106
+ end
107
+
108
+ context 'when declaring minute intervals' do
109
+ before do
110
+ dispatch.on_heartbeat event_name, minute_interval: 5 do |_event|
111
+ Runner2.run({})
112
+ end
113
+ end
114
+
115
+ it 'runs the runner when the minute buzzes (modulos to 5)' do
116
+ (0..60).each do |minute|
117
+ if minute % 5 == 0
118
+ expect { dispatch.execute(event_name, event.merge('minute' => minute)) }
119
+ .to change(Runner2, :value).by(1)
120
+ else
121
+ expect { dispatch.execute(event_name, event.merge('minute' => minute)) }
122
+ .not_to change(Runner2, :value)
123
+ end
124
+ end
125
+ end
126
+ end
127
+
128
+ context 'when declaring hour intervals' do
129
+ before do
130
+ dispatch.on_heartbeat event_name, hour_interval: 3 do |_event|
131
+ Runner2.run({})
132
+ end
133
+ end
134
+
135
+ it 'runs the runner when the hour fizzes (modulos to 3)' do
136
+ (0..60).each do |hour|
137
+ if hour % 3 == 0
138
+ expect { dispatch.execute(event_name, event.merge('hour' => hour)) }
139
+ .to change(Runner2, :value).by(1)
140
+ else
141
+ expect { dispatch.execute(event_name, event.merge('hour' => hour)) }
142
+ .not_to change(Runner2, :value)
143
+ end
144
+ end
145
+ end
146
+ end
147
+
148
+ context 'when declaring hour and minute intervals' do
149
+ before do
150
+ dispatch.on_heartbeat event_name, minute_interval: 5, hour_interval: 3 do |_event|
151
+ Runner2.run({})
152
+ end
153
+ end
154
+
155
+ it 'runs the runner when the time fizzbuzzes (modulos to 3 and 5)' do
156
+ (0..24).each do |hour|
157
+ (0..60).each do |minute|
158
+ if hour % 3 == 0 && minute % 5 == 0
159
+ expect do
160
+ dispatch.execute(
161
+ event_name, event.merge('hour' => hour, 'minute' => minute)
162
+ )
163
+ end.to change(Runner2, :value).by(1)
164
+ else
165
+ expect do
166
+ dispatch.execute(
167
+ event_name, event.merge('hour' => hour, 'minute' => minute)
168
+ )
169
+ end.not_to change(Runner2, :value)
170
+ end
171
+ end
172
+ end
173
+ end
174
+ end
175
+ end
176
+
31
177
  describe 'Top Level' do
32
178
  before(:each) do
33
179
  QueueBus.dispatch('testit') do
data/spec/publish_spec.rb CHANGED
@@ -71,6 +71,28 @@ describe 'Publishing an event' do
71
71
  expect(myval).to eq(1)
72
72
  end
73
73
 
74
+ it 'should add context metadata if wrapping publisher with in_context' do
75
+ expect(QueueBus.context).to be_nil
76
+
77
+ bus_context = 'batch_processing'
78
+ hash = { :one => 1, 'two' => 'here', 'bus_id' => 'app-given' }
79
+
80
+ event_name = 'event_name'
81
+
82
+ QueueBus.in_context(:batch_processing) do
83
+ QueueBus.publish(event_name, hash)
84
+ end
85
+
86
+ val = QueueBus.redis { |redis| redis.lpop('queue:bus_incoming') }
87
+ hash = JSON.parse(val)
88
+
89
+ att = JSON.parse(hash['args'].first)
90
+ expect(att['bus_context']).to eq(bus_context)
91
+
92
+ expect(QueueBus.context).to be_nil
93
+
94
+ end
95
+
74
96
  it 'should set the timezone and locale if available' do
75
97
  expect(defined?(I18n)).to be_nil
76
98
  expect(Time.respond_to?(:zone)).to eq(false)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: queue-bus
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.10.0
4
+ version: 0.13.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brian Leonard
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-12-04 00:00:00.000000000 Z
11
+ date: 2021-11-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: multi_json