startback 0.11.5 → 0.12.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
  SHA256:
3
- metadata.gz: fa9e440ddba682a1c59c98a5635b0febafe08222e9cee2de68d3262c6da6d8d4
4
- data.tar.gz: '09a4748015aa374687c4016341d7f6a699d80718da7f9b4a6449294d69cf7897'
3
+ metadata.gz: 67ffd7abceaf1b4e048cc8f02f668b682ea81f62fc7de5228a9f0d12abab1df8
4
+ data.tar.gz: 1eb075bdec30a569726bc3f2a74c1ce427aef8ff0c3cbcee401a61c034e71ca6
5
5
  SHA512:
6
- metadata.gz: 6f832ba94f865dfd66c372fcf19623c68f43c76019623fd49da96e88fa2c7355bc78916ae98f3ae32de7e4deb0f08d9ade5d7eb2b3fa9f9bebb5544c0de698d4
7
- data.tar.gz: 6ec4e88b7f2bc2ffd58d6e0cddb5bfffc5411149b71a97d1a51e7195105bd907568b45afec40f47916c17dbc45dfe81a5e6432781fa821dbdb2b1ba47f614c86
6
+ metadata.gz: 8cb4d6f1302a34cab267c836c1e0d81c54267607de7f24d350183ff05a79af7cc216a6f34bc96c0e1204f71cca1de744fde448590e99bd96a1ab45c15c8ceddf
7
+ data.tar.gz: fecf573e39eb04317d756414349d94092f24a07210629a55347191ef74ec94fcc8079f8a4f38b6c58bf94be5c59fbd79a90a168a4925819ce771269ccae6e3da
@@ -1,7 +1,70 @@
1
1
  module Startback
2
2
  class Event
3
+ #
4
+ # An agent listen to specific events and react with its
5
+ # `call` method.
6
+ #
7
+ # This class is intended to be subclasses and the following
8
+ # methods overriden:
9
+ #
10
+ # - install_listeners that installs sync and async listeners
11
+ # - call to create a context and implement reaction behavior
12
+ #
3
13
  class Agent
4
14
  include Support::OperationRunner
15
+ include Support::Robustness
16
+
17
+ def initialize(engine)
18
+ @engine = engine
19
+ install_listeners
20
+ end
21
+ attr_reader :engine
22
+
23
+ protected
24
+
25
+ # Installs the various event handlers by calling `sync`
26
+ # and `async` methods.
27
+ #
28
+ # This method is intended to be overriden.
29
+ def install_listeners
30
+ end
31
+
32
+ # Returns the underlying bus
33
+ def bus
34
+ engine.bus
35
+ end
36
+
37
+ # Asynchronously listen to a specific event.
38
+ #
39
+ # See Bus#listen
40
+ def async(exchange, queue)
41
+ bus.async.listen(exchange, queue) do |event|
42
+ dup.call(event)
43
+ end
44
+ end
45
+
46
+ # Synchronously listen to a specific event.
47
+ #
48
+ # See Bus#listen
49
+ def sync(exchange, queue)
50
+ bus.listen(exchange, queue) do |event|
51
+ dup.call(event)
52
+ end
53
+ end
54
+
55
+ # Reacts to a specific event.
56
+ #
57
+ # This method must be implemented by subclasses and raises
58
+ # an error by default.
59
+ def call(event = nil)
60
+ log(:fatal, {
61
+ op: self.class,
62
+ op_data: event,
63
+ error: %Q{Unexpected call to Startback::Event::Agent#call},
64
+ backtrace: caller
65
+ })
66
+ raise NotImplementedError
67
+ end
5
68
 
6
69
  end # class Agent
7
70
  end # class Event
@@ -0,0 +1,165 @@
1
+ require 'bunny'
2
+ module Startback
3
+ class Event
4
+ class Bus
5
+ module Bunny
6
+ #
7
+ # Asynchronous implementation of the bus abstraction, on top of RabbitMQ
8
+ # and using the 'bunny' gem (you need to include it in your Gemfile
9
+ # yourself: it is NOT a startback official dependency).
10
+ #
11
+ # This bus implementation emits events by dumping them to RabbitMQ using
12
+ # the event type as exchange name. Listeners may use the `processor`
13
+ # parameter to specify the queue name ; otherwise a default "main" queue
14
+ # is used.
15
+ #
16
+ # Examples:
17
+ #
18
+ # # Connects to RabbitMQ using all default options
19
+ # #
20
+ # # Uses the STARTBACK_BUS_BUNNY_ASYNC_URL environment variable for
21
+ # # connection URL if present.
22
+ # Startback::Bus::Bunny::Async.new
23
+ #
24
+ # # Connects to RabbitMQ using a specific URL
25
+ # Startback::Bus::Bunny::Async.new("amqp://rabbituser:rabbitpass@192.168.17.17")
26
+ # Startback::Bus::Bunny::Async.new(url: "amqp://rabbituser:rabbitpass@192.168.17.17")
27
+ #
28
+ # # Connects to RabbitMQ using specific connection options. See Bunny's own
29
+ # # documentation
30
+ # Startback::Bus::Bunny::Async.new({
31
+ # connection_options: {
32
+ # host: "192.168.17.17"
33
+ # }
34
+ # })
35
+ #
36
+ class Async
37
+ include Support::Robustness
38
+
39
+ CHANNEL_KEY = 'Startback::Bus::Bunny::Async::ChannelKey'
40
+
41
+ DEFAULT_OPTIONS = {
42
+ # (optional) The URL to use for connecting to RabbitMQ.
43
+ url: ENV['STARTBACK_BUS_BUNNY_ASYNC_URL'],
44
+
45
+ # (optional) The options has to pass to ::Bunny constructor
46
+ connection_options: nil,
47
+
48
+ # (optional) The options to use for the emitter/listener fanout
49
+ fanout_options: {},
50
+
51
+ # (optional) The options to use for the listener queue
52
+ queue_options: {},
53
+
54
+ # (optional) Default event factory to use, if any
55
+ event_factory: nil,
56
+
57
+ # (optional) A default context to use for general logging
58
+ context: nil,
59
+
60
+ # (optional) Size of consumer pool
61
+ consumer_pool_size: 1,
62
+
63
+ # (optional) Whether the program must be aborted on consumption
64
+ # error
65
+ abort_on_exception: true,
66
+
67
+ # (optional) Whether connection occurs immediately,
68
+ # or on demand later
69
+ autoconnect: false
70
+ }
71
+
72
+ # Creates a bus instance, using the various options provided to
73
+ # fine-tune behavior.
74
+ def initialize(options = {})
75
+ options = { url: options } if options.is_a?(String)
76
+ @options = DEFAULT_OPTIONS.merge(options)
77
+ connect if @options[:autoconnect]
78
+ end
79
+ attr_reader :options
80
+
81
+ def connect
82
+ disconnect
83
+ conn = options[:connection_options] || options[:url]
84
+ try_max_times(10) do
85
+ @bunny = ::Bunny.new(conn)
86
+ @bunny.start
87
+ channel # make sure we already create the channel
88
+ log(:info, {op: "#{self.class.name}#connect", op_data: conn}, options[:context])
89
+ end
90
+ end
91
+
92
+ def disconnect
93
+ if channel = Thread.current[CHANNEL_KEY]
94
+ channel.close
95
+ Thread.current[CHANNEL_KEY] = nil
96
+ end
97
+ @bunny.close if @bunny
98
+ end
99
+
100
+ def channel
101
+ unless @bunny
102
+ raise Startback::Errors::Error, "Please connect your bus first, or use autoconnect: true"
103
+ end
104
+
105
+ Thread.current[CHANNEL_KEY] ||= @bunny.create_channel(
106
+ nil,
107
+ consumer_pool_size, # consumer_pool_size
108
+ abort_on_exception? # consumer_pool_abort_on_exception
109
+ )
110
+ end
111
+
112
+ def emit(event)
113
+ stop_errors(self, "emit", event.context) do
114
+ fanout = channel.fanout(event.type.to_s, fanout_options)
115
+ fanout.publish(event.to_json)
116
+ end
117
+ end
118
+
119
+ def listen(type, processor = nil, listener = nil, &bl)
120
+ raise ArgumentError, "A listener must be provided" unless listener || bl
121
+
122
+ fanout = channel.fanout(type.to_s, fanout_options)
123
+ queue = channel.queue((processor || "main").to_s, queue_options)
124
+ queue.bind(fanout)
125
+ queue.subscribe do |delivery_info, properties, body|
126
+ event = stop_errors(self, "listen") do
127
+ factor_event(body)
128
+ end
129
+ stop_errors(self, "listen", event.context) do
130
+ (listener || bl).call(event)
131
+ end
132
+ end
133
+ end
134
+
135
+ protected
136
+
137
+ def consumer_pool_size
138
+ options[:consumer_pool_size]
139
+ end
140
+
141
+ def abort_on_exception?
142
+ options[:abort_on_exception]
143
+ end
144
+
145
+ def fanout_options
146
+ options[:fanout_options]
147
+ end
148
+
149
+ def queue_options
150
+ options[:queue_options]
151
+ end
152
+
153
+ def factor_event(body)
154
+ if options[:event_factory]
155
+ options[:event_factory].call(body)
156
+ else
157
+ Event.json(body, options)
158
+ end
159
+ end
160
+
161
+ end # class Async
162
+ end # module Bunny
163
+ end # class Bus
164
+ end # class Event
165
+ end # module Startback
File without changes
@@ -0,0 +1,45 @@
1
+ module Startback
2
+ class Event
3
+ class Bus
4
+ module Memory
5
+ #
6
+ # Asynchronous implementation of the Bus abstraction, for use between
7
+ # components sharing the same process.
8
+ #
9
+ # This implementation actually calls listeners synchronously (it mays)
10
+ # but hides error raised by them. See Bus::Bunny::Async for another
11
+ # implementation that is truly asynchronous and relies on RabbitMQ.
12
+ #
13
+ class Async
14
+ include Support::Robustness
15
+
16
+ DEFAULT_OPTIONS = {
17
+ }
18
+
19
+ def initialize(options = {})
20
+ @options = DEFAULT_OPTIONS.merge(options)
21
+ @listeners = {}
22
+ end
23
+
24
+ def connect
25
+ end
26
+
27
+ def emit(event)
28
+ (@listeners[event.type.to_s] || []).each do |l|
29
+ stop_errors(self, "emit", event) {
30
+ l.call(event)
31
+ }
32
+ end
33
+ end
34
+
35
+ def listen(type, processor = nil, listener = nil, &bl)
36
+ raise ArgumentError, "A listener must be provided" unless listener || bl
37
+ @listeners[type.to_s] ||= []
38
+ @listeners[type.to_s] << (listener || bl)
39
+ end
40
+
41
+ end # class Sync
42
+ end # module Memory
43
+ end # class Bus
44
+ end # class Event
45
+ end # module Startback
@@ -0,0 +1,35 @@
1
+ module Startback
2
+ class Event
3
+ class Bus
4
+ module Memory
5
+ #
6
+ # Synchronous implementation of the Bus abstraction, for use between
7
+ # components sharing the same process.
8
+ #
9
+ class Sync
10
+ include Support::Robustness
11
+
12
+ def initialize
13
+ @listeners = {}
14
+ end
15
+
16
+ def connect
17
+ end
18
+
19
+ def emit(event)
20
+ (@listeners[event.type.to_s] || []).each do |l|
21
+ l.call(event)
22
+ end
23
+ end
24
+
25
+ def listen(type, processor = nil, listener = nil, &bl)
26
+ raise ArgumentError, "A listener must be provided" unless listener || bl
27
+ @listeners[type.to_s] ||= []
28
+ @listeners[type.to_s] << (listener || bl)
29
+ end
30
+
31
+ end # class Sync
32
+ end # module Memory
33
+ end # class Bus
34
+ end # class Event
35
+ end # module Startback
File without changes
@@ -0,0 +1,100 @@
1
+ module Startback
2
+ class Event
3
+ #
4
+ # Sync and async bus abstraction allowing to register listeners and
5
+ # emitting events towards them.
6
+ #
7
+ # This bus actually decorates two busses, one in synchronous and the
8
+ # other one is asynchronous (optional).
9
+ #
10
+ # * A synchronous bus MUST call the listeners as part of emitting
11
+ # process, and MUST re-raise any error occuring during that process.
12
+ # See, e.g. Startback::Bus::Memory::Sync
13
+ #
14
+ # * An asynchronous bus MAY call the listeners later, but MUST hide
15
+ # errors to the emitter.
16
+ # See, e.g. Startback::Bus::Memory::Async
17
+ #
18
+ # This bus facade emits events to both sync and async busses (if any),
19
+ # and listen on the sync one by default.
20
+ #
21
+ # For emitters:
22
+ #
23
+ # # This will synchronously call every listeners who `listen`
24
+ # # on the synchronous bus (& reraise exceptions) then call
25
+ # # (possibly later) all listeners who `listen` on the
26
+ # # asynchronous bus if any (& hide exceptions).
27
+ # bus.emit(event)
28
+ #
29
+ # # This only reaches sync listeners
30
+ # bus.sync.emit(event)
31
+ #
32
+ # # This only reaches async listeners (an async bus must be set)
33
+ # bus.async.emit(event)
34
+ #
35
+ # Please note that there is currently no way to reach sync listeners
36
+ # without having to implement error handling on the emitter side.
37
+ #
38
+ # For listeners:
39
+ #
40
+ # # This will listen synchronously and make the emitter fail if
41
+ # # anything goes wrong with the callback:
42
+ # bus.listen(event_type) do |event|
43
+ # ...
44
+ # end
45
+ #
46
+ # # It is a shortcut for:
47
+ # bus.sync.listen(event_type) do |event| ... end
48
+ #
49
+ # This will listen asynchronously and could not make the emitter
50
+ # fail if something goes wrong with the callback.
51
+ # bus.async.listen(event_type) do |event|
52
+ # ...
53
+ # end
54
+ #
55
+ # Feel free to access the sync and async busses directly for specific
56
+ # cases though.
57
+ #
58
+ class Bus
59
+ include Support::Robustness
60
+
61
+ def initialize(sync = Memory::Sync.new, async = nil)
62
+ @sync = sync
63
+ @async = async
64
+ end
65
+ attr_reader :sync, :async
66
+
67
+ def connect
68
+ sync.connect if sync
69
+ async.connect if async
70
+ end
71
+
72
+ # Emits a particular event to the listeners.
73
+ #
74
+ # @arg event an event, should be an Event instance (through duck
75
+ # typing is allowed)
76
+ def emit(event)
77
+ monitor({
78
+ op: "Startback::Bus#emit",
79
+ op_data: {
80
+ event: { type: event.type }
81
+ }
82
+ }, event.context) do
83
+ sync.emit(event)
84
+ async.emit(event) if async
85
+ end
86
+ end
87
+
88
+ # Registers `listener` as being interested in receiving events of
89
+ # a specific type.
90
+ #
91
+ # @arg type: Symbol, the type of event the listener is interested in.
92
+ # @arg listener: Proc, the listener itself.
93
+ def listen(type, processor = nil, listener = nil, &bl)
94
+ sync.listen(type, processor, listener, &bl)
95
+ end
96
+
97
+ end # class Bus
98
+ end # class Event
99
+ end # module Startback
100
+ require_relative 'bus/memory'
@@ -4,10 +4,10 @@ require 'startback'
4
4
  module Startback
5
5
  class Event
6
6
  #
7
- # This class runs an infinite loop using ServerEngine.
8
- # It is intended to be used to run jobs that listen to
9
- # a Startback Bus instance without having the main process
10
- # terminating immediately.
7
+ # This class is the starting point of event handling in
8
+ # Startback. It holds a Bus instance to which emitters
9
+ # and listeners can connect, and the possibility for the
10
+ # the listening part to start an infinite loop (ServerEngine).
11
11
  #
12
12
  # The Engine automatically runs a Webrick small webapp
13
13
  # with a /healthcheck webservice. The class can be extended
@@ -15,86 +15,157 @@ module Startback
15
15
  # checks.
16
16
  #
17
17
  # This class goes hand in hand with the `startback:engine`
18
- # docker image.
18
+ # docker image. It can be extended by subclasses to override
19
+ # the following methods:
19
20
  #
20
- # Example:
21
- #
22
- # # Dockerfile
23
- # FROM enspirit/startback:engine-0.11
24
- #
25
- # # engine.rb
26
- # require 'startback/event/engine'
27
- # Startback::Event::Engine.run
21
+ # - bus to use something else than a simple memory bus
22
+ # - on_health_check to check specific health conditions
23
+ # - create_agents to instantiate all listening agents
24
+ # (unless auto_create_agents is used)
28
25
  #
29
26
  class Engine
27
+ include Support::Robustness
30
28
 
31
29
  DEFAULT_OPTIONS = {
32
- daemonize: false,
33
- worker_type: 'process',
34
- workers: 1
30
+
31
+ # To be passed to ServerEngine
32
+ server_engine: {}
33
+
35
34
  }
36
35
 
37
- def initialize
38
- require 'serverengine'
36
+ def initialize(options = {}, context = Context.new)
37
+ @options = DEFAULT_OPTIONS.merge(options)
38
+ @context = context
39
+ end
40
+ attr_reader :options, :context
41
+
42
+ class << self
43
+ def auto_create_agents?
44
+ !!@auto_create_agents
45
+ end
46
+
47
+ # Register a base class which will be used to discover
48
+ # the agents to start when the engine is ran.
49
+ def auto_create_agents(base_class = nil)
50
+ @auto_create_agents ||= base_class
51
+ @auto_create_agents
52
+ end
39
53
  end
40
54
 
55
+ # This method is executed on health check and can be
56
+ # overriden by subclasses to perform specific checks.
41
57
  def on_health_check
42
58
  "Ok"
43
59
  end
44
60
 
61
+ def bus
62
+ ::Startback::Event::Bus.new
63
+ end
64
+
65
+ def connect
66
+ log(:info, self, "Connecting to the bus now!")
67
+ bus.connect
68
+ end
69
+
45
70
  def run(options = {})
46
- options = DEFAULT_OPTIONS.merge(options)
47
- health = Engine.build_health_check(self)
48
- worker = Engine.build_worker(health)
49
- se = ServerEngine.create(nil, worker, options)
50
- se.run
51
- se
71
+ connect
72
+
73
+ log(:info, self, "Running agents and server engine!")
74
+ create_agents
75
+ Runner.new(self, options[:server_engine] || {}).run
52
76
  end
53
77
 
54
- class << self
55
- def run(*args, &bl)
56
- new.run(*args, &bl)
78
+ def create_agents
79
+ return unless parent = self.class.auto_create_agents
80
+
81
+ ObjectSpace
82
+ .each_object(Class)
83
+ .select { |klass| klass < parent }
84
+ .each { |klass| klass.new(self) }
85
+ end
86
+
87
+ class Runner
88
+
89
+ DEFAULT_SERVER_ENGINE_OPTIONS = {
90
+ daemonize: false,
91
+ worker_type: 'process',
92
+ workers: 1
93
+ }
94
+
95
+ def initialize(engine, options = {})
96
+ raise ArgumentError if engine.nil?
97
+
98
+ @engine = engine
99
+ @options = DEFAULT_SERVER_ENGINE_OPTIONS.merge(options)
100
+ require 'serverengine'
57
101
  end
102
+ attr_reader :engine, :options
58
103
 
59
- def build_health_check(engine)
60
- Rack::Builder.new do
61
- map '/health-check' do
62
- health = Startback::Web::HealthCheck.new {
63
- engine.on_health_check
64
- }
65
- run(health)
66
- end
67
- end
104
+ def run(options = {})
105
+ health = self.class.build_health_check(engine)
106
+ worker = self.class.build_worker(engine, health)
107
+ se = ServerEngine.create(nil, worker, options)
108
+ se.run
109
+ se
68
110
  end
69
111
 
70
- def build_worker(health)
71
- Module.new do
72
- include Support::Env
112
+ class << self
113
+ def run(*args, &bl)
114
+ new.run(*args, &bl)
115
+ end
73
116
 
74
- def initialize
75
- @stop_flag = ServerEngine::BlockingFlag.new
117
+ def build_health_check(engine)
118
+ Rack::Builder.new do
119
+ map '/health-check' do
120
+ health = Startback::Web::HealthCheck.new {
121
+ engine.on_health_check
122
+ }
123
+ run(health)
124
+ end
76
125
  end
126
+ end
77
127
 
78
- define_method(:health) do
79
- health
80
- end
128
+ def build_worker(engine, health)
129
+ Module.new do
130
+ include Support::Env
81
131
 
82
- def run
83
- until @stop_flag.set?
84
- Rack::Handler::WEBrick.run(health, {
85
- :Port => env('STARTBACK_ENGINE_PORT', '3000').to_i,
86
- :Host => env('STARTBACK_ENGINE_LISTEN', '0.0.0.0')
87
- })
132
+ def initialize
133
+ @stop_flag = ServerEngine::BlockingFlag.new
134
+ end
135
+
136
+ define_method(:health) do
137
+ health
138
+ end
139
+
140
+ define_method(:engine) do
141
+ engine
88
142
  end
89
- end
90
143
 
91
- def stop
92
- @stop_flag.set!
93
- Rack::Handler::WEBrick.shutdown
144
+ def run
145
+ ran = false
146
+ until @stop_flag.set?
147
+ if ran
148
+ engine.send(:log, :warn, engine, "Restarting internal loop")
149
+ else
150
+ engine.send(:log, :info, engine, "Starting internal loop")
151
+ end
152
+ Rack::Handler::WEBrick.run(health, {
153
+ :Port => env('STARTBACK_ENGINE_PORT', '3000').to_i,
154
+ :Host => env('STARTBACK_ENGINE_LISTEN', '0.0.0.0')
155
+ })
156
+ ran = true
157
+ end
158
+ end
159
+
160
+ def stop
161
+ engine.send(:log, :info, engine, "Stopping internal loop")
162
+ @stop_flag.set!
163
+ Rack::Handler::WEBrick.shutdown
164
+ end
94
165
  end
95
166
  end
96
- end
97
- end # class << self
167
+ end # class << self
168
+ end # class Runner
98
169
  end # class Engine
99
170
  end # class Event
100
171
  end # module Startback
@@ -42,4 +42,5 @@ module Startback
42
42
  end # class Event
43
43
  end # module Startback
44
44
  require_relative 'event/agent'
45
+ require_relative 'event/bus'
45
46
  require_relative 'event/engine'
@@ -107,8 +107,6 @@ module Startback
107
107
  }
108
108
  Tools.info(args, op_took: took)
109
109
  result
110
- rescue => ex
111
- raise
112
110
  end
113
111
 
114
112
  # Executes the block without letting errors propagate.
@@ -1,8 +1,8 @@
1
1
  module Startback
2
2
  module Version
3
3
  MAJOR = 0
4
- MINOR = 11
5
- TINY = 5
4
+ MINOR = 12
5
+ TINY = 0
6
6
  end
7
7
  VERSION = "#{Version::MAJOR}.#{Version::MINOR}.#{Version::TINY}"
8
8
  end
data/spec/spec_helper.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
2
2
  require 'startback'
3
- require 'startback/bus'
3
+ require 'startback/event'
4
4
  require 'startback/support/fake_logger'
5
5
  require 'rack/test'
6
6
 
@@ -0,0 +1,43 @@
1
+ require 'spec_helper'
2
+ module Startback
3
+ class Event
4
+ describe Bus::Memory do
5
+
6
+ subject{
7
+ Bus::Memory::Async.new
8
+ }
9
+
10
+ it 'allows emiting an receiving' do
11
+ seen = nil
12
+ subject.listen("user_changed") do |evt|
13
+ seen = evt
14
+ end
15
+ subject.emit(Event.new("user_changed", {id: 12}))
16
+ expect(seen).to be_a(Event)
17
+ expect(seen.type).to eql("user_changed")
18
+ expect(seen.data.to_h).to eql({id: 12})
19
+ end
20
+
21
+ it 'allows mixin Symbol vs. String for event type' do
22
+ seen = nil
23
+ subject.listen(:user_changed) do |evt|
24
+ seen = evt
25
+ end
26
+ subject.emit(Event.new(:user_changed, {id: 12}))
27
+ expect(seen).to be_a(Event)
28
+ expect(seen.type).to eql("user_changed")
29
+ expect(seen.data.to_h).to eql({id: 12})
30
+ end
31
+
32
+ it 'does not raise errors synchronously' do
33
+ subject.listen("user_changed") do |evt|
34
+ raise "An error occured"
35
+ end
36
+ expect {
37
+ subject.emit(Event.new("user_changed", {id: 12}))
38
+ }.not_to raise_error
39
+ end
40
+
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,43 @@
1
+ require 'spec_helper'
2
+ module Startback
3
+ class Event
4
+ describe Bus::Memory do
5
+
6
+ subject{
7
+ Bus::Memory::Sync.new
8
+ }
9
+
10
+ it 'allows emiting an receiving' do
11
+ seen = nil
12
+ subject.listen("user_changed") do |evt|
13
+ seen = evt
14
+ end
15
+ subject.emit(Event.new("user_changed", {id: 12}))
16
+ expect(seen).to be_a(Event)
17
+ expect(seen.type).to eql("user_changed")
18
+ expect(seen.data.to_h).to eql({id: 12})
19
+ end
20
+
21
+ it 'allows mixin Symbol vs. String for event type' do
22
+ seen = nil
23
+ subject.listen(:user_changed) do |evt|
24
+ seen = evt
25
+ end
26
+ subject.emit(Event.new(:user_changed, {id: 12}))
27
+ expect(seen).to be_a(Event)
28
+ expect(seen.type).to eql("user_changed")
29
+ expect(seen.data.to_h).to eql({id: 12})
30
+ end
31
+
32
+ it 'raises emit errors synchronously' do
33
+ subject.listen("user_changed") do |evt|
34
+ raise "An error occured"
35
+ end
36
+ expect {
37
+ subject.emit(Event.new("user_changed", {id: 12}))
38
+ }.to raise_error("An error occured")
39
+ end
40
+
41
+ end
42
+ end
43
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: startback
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.11.5
4
+ version: 0.12.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bernard Lambeau
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-05-18 00:00:00.000000000 Z
11
+ date: 2022-05-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rspec
@@ -385,12 +385,6 @@ files:
385
385
  - lib/startback/audit/prometheus.rb
386
386
  - lib/startback/audit/shared.rb
387
387
  - lib/startback/audit/trailer.rb
388
- - lib/startback/bus.rb
389
- - lib/startback/bus/bunny.rb
390
- - lib/startback/bus/bunny/async.rb
391
- - lib/startback/bus/memory.rb
392
- - lib/startback/bus/memory/async.rb
393
- - lib/startback/bus/memory/sync.rb
394
388
  - lib/startback/caching/entity_cache.rb
395
389
  - lib/startback/caching/no_store.rb
396
390
  - lib/startback/caching/store.rb
@@ -399,6 +393,12 @@ files:
399
393
  - lib/startback/errors.rb
400
394
  - lib/startback/event.rb
401
395
  - lib/startback/event/agent.rb
396
+ - lib/startback/event/bus.rb
397
+ - lib/startback/event/bus/bunny.rb
398
+ - lib/startback/event/bus/bunny/async.rb
399
+ - lib/startback/event/bus/memory.rb
400
+ - lib/startback/event/bus/memory/async.rb
401
+ - lib/startback/event/bus/memory/sync.rb
402
402
  - lib/startback/event/engine.rb
403
403
  - lib/startback/ext.rb
404
404
  - lib/startback/ext/date_time.rb
@@ -431,13 +431,13 @@ files:
431
431
  - spec/spec_helper.rb
432
432
  - spec/unit/audit/test_prometheus.rb
433
433
  - spec/unit/audit/test_trailer.rb
434
- - spec/unit/bus/memory/test_async.rb
435
- - spec/unit/bus/memory/test_sync.rb
436
434
  - spec/unit/caching/test_entity_cache.rb
437
435
  - spec/unit/context/test_abstraction_factory.rb
438
436
  - spec/unit/context/test_dup.rb
439
437
  - spec/unit/context/test_h_factory.rb
440
438
  - spec/unit/context/test_middleware.rb
439
+ - spec/unit/event/bus/memory/test_async.rb
440
+ - spec/unit/event/bus/memory/test_sync.rb
441
441
  - spec/unit/support/hooks/test_after_hook.rb
442
442
  - spec/unit/support/hooks/test_before_hook.rb
443
443
  - spec/unit/support/operation_runner/test_around_run.rb
@@ -1,123 +0,0 @@
1
- require 'bunny'
2
- module Startback
3
- class Bus
4
- module Bunny
5
- #
6
- # Asynchronous implementation of the bus abstraction, on top of RabbitMQ
7
- # and using the 'bunny' gem (you need to include it in your Gemfile
8
- # yourself: it is NOT a startback official dependency).
9
- #
10
- # This bus implementation emits events by dumping them to RabbitMQ using
11
- # the event type as exchange name. Listeners may use the `processor`
12
- # parameter to specify the queue name ; otherwise a default "main" queue
13
- # is used.
14
- #
15
- # Examples:
16
- #
17
- # # Connects to RabbitMQ using all default options
18
- # #
19
- # # Uses the STARTBACK_BUS_BUNNY_ASYNC_URL environment variable for
20
- # # connection URL if present.
21
- # Startback::Bus::Bunny::Async.new
22
- #
23
- # # Connects to RabbitMQ using a specific URL
24
- # Startback::Bus::Bunny::Async.new("amqp://rabbituser:rabbitpass@192.168.17.17")
25
- # Startback::Bus::Bunny::Async.new(url: "amqp://rabbituser:rabbitpass@192.168.17.17")
26
- #
27
- # # Connects to RabbitMQ using specific connection options. See Bunny's own
28
- # # documentation
29
- # Startback::Bus::Bunny::Async.new({
30
- # connection_options: {
31
- # host: "192.168.17.17"
32
- # }
33
- # })
34
- #
35
- class Async
36
- include Support::Robustness
37
-
38
- CHANNEL_KEY = 'Startback::Bus::Bunny::Async::ChannelKey'
39
-
40
- DEFAULT_OPTIONS = {
41
- # (optional) The URL to use for connecting to RabbitMQ.
42
- url: ENV['STARTBACK_BUS_BUNNY_ASYNC_URL'],
43
-
44
- # (optional) The options has to pass to ::Bunny constructor
45
- connection_options: nil,
46
-
47
- # (optional) The options to use for the emitter/listener fanout
48
- fanout_options: {},
49
-
50
- # (optional) The options to use for the listener queue
51
- queue_options: {},
52
-
53
- # (optional) Default event factory to use, if any
54
- event_factory: nil,
55
-
56
- # (optional) A default context to use for general logging
57
- context: nil
58
- }
59
-
60
- # Creates a bus instance, using the various options provided to
61
- # fine-tune behavior.
62
- def initialize(options = {})
63
- options = { url: options } if options.is_a?(String)
64
- @options = DEFAULT_OPTIONS.merge(options)
65
- retried = 0
66
- conn = options[:connection_options] || options[:url]
67
- try_max_times(10) do
68
- @bunny = ::Bunny.new(conn)
69
- @bunny.start
70
- channel
71
- log(:info, {op: "#{self.class.name}#connect", op_data: conn}, options[:context])
72
- end
73
- end
74
- attr_reader :options
75
-
76
- def channel
77
- Thread.current[CHANNEL_KEY] ||= @bunny.create_channel
78
- end
79
-
80
- def emit(event)
81
- stop_errors(self, "emit", event.context) do
82
- fanout = channel.fanout(event.type.to_s, fanout_options)
83
- fanout.publish(event.to_json)
84
- end
85
- end
86
-
87
- def listen(type, processor = nil, listener = nil, &bl)
88
- raise ArgumentError, "A listener must be provided" unless listener || bl
89
- fanout = channel.fanout(type.to_s, fanout_options)
90
- queue = channel.queue((processor || "main").to_s, queue_options)
91
- queue.bind(fanout)
92
- queue.subscribe do |delivery_info, properties, body|
93
- event = stop_errors(self, "listen") do
94
- factor_event(body)
95
- end
96
- stop_errors(self, "listen", event.context) do
97
- (listener || bl).call(event)
98
- end
99
- end
100
- end
101
-
102
- protected
103
-
104
- def fanout_options
105
- options[:fanout_options]
106
- end
107
-
108
- def queue_options
109
- options[:queue_options]
110
- end
111
-
112
- def factor_event(body)
113
- if options[:event_factory]
114
- options[:event_factory].call(body)
115
- else
116
- Event.json(body, options)
117
- end
118
- end
119
-
120
- end # class Async
121
- end # module Bunny
122
- end # class Bus
123
- end # module Klaro
@@ -1,40 +0,0 @@
1
- module Startback
2
- class Bus
3
- module Memory
4
- #
5
- # Asynchronous implementation of the Bus abstraction, for use between
6
- # components sharing the same process.
7
- #
8
- # This implementation actually calls listeners synchronously (it mays)
9
- # but hides error raised by them. See Bus::Bunny::Async for another
10
- # implementation that is truly asynchronous and relies on RabbitMQ.
11
- #
12
- class Async
13
- include Support::Robustness
14
-
15
- DEFAULT_OPTIONS = {
16
- }
17
-
18
- def initialize(options = {})
19
- @options = DEFAULT_OPTIONS.merge(options)
20
- @listeners = {}
21
- end
22
-
23
- def emit(event)
24
- (@listeners[event.type.to_s] || []).each do |l|
25
- stop_errors(self, "emit", event) {
26
- l.call(event)
27
- }
28
- end
29
- end
30
-
31
- def listen(type, processor = nil, listener = nil, &bl)
32
- raise ArgumentError, "A listener must be provided" unless listener || bl
33
- @listeners[type.to_s] ||= []
34
- @listeners[type.to_s] << (listener || bl)
35
- end
36
-
37
- end # class Sync
38
- end # module Memory
39
- end # class Bus
40
- end # module Klaro
@@ -1,30 +0,0 @@
1
- module Startback
2
- class Bus
3
- module Memory
4
- #
5
- # Synchronous implementation of the Bus abstraction, for use between
6
- # components sharing the same process.
7
- #
8
- class Sync
9
- include Support::Robustness
10
-
11
- def initialize
12
- @listeners = {}
13
- end
14
-
15
- def emit(event)
16
- (@listeners[event.type.to_s] || []).each do |l|
17
- l.call(event)
18
- end
19
- end
20
-
21
- def listen(type, processor = nil, listener = nil, &bl)
22
- raise ArgumentError, "A listener must be provided" unless listener || bl
23
- @listeners[type.to_s] ||= []
24
- @listeners[type.to_s] << (listener || bl)
25
- end
26
-
27
- end # class Sync
28
- end # module Memory
29
- end # class Bus
30
- end # module Klaro
data/lib/startback/bus.rb DELETED
@@ -1,94 +0,0 @@
1
- require 'startback/event'
2
- module Startback
3
- #
4
- # Sync and async bus abstraction allowing to register listeners and
5
- # emitting events towards them.
6
- #
7
- # This bus actually decorates two busses, one in synchronous and the
8
- # other one is asynchronous (optional).
9
- #
10
- # * A synchronous bus MUST call the listeners as part of emitting
11
- # process, and MUST re-raise any error occuring during that process.
12
- # See, e.g. Startback::Bus::Memory::Sync
13
- #
14
- # * An asynchronous bus MAY call the listeners later, but MUST hide
15
- # errors to the emitter.
16
- # See, e.g. Startback::Bus::Memory::Async
17
- #
18
- # This bus facade emits events to both sync and async busses (if any),
19
- # and listen on the sync one by default.
20
- #
21
- # For emitters:
22
- #
23
- # # This will synchronously call every listeners who `listen`
24
- # # on the synchronous bus (& reraise exceptions) then call
25
- # # (possibly later) all listeners who `listen` on the
26
- # # asynchronous bus if any (& hide exceptions).
27
- # bus.emit(event)
28
- #
29
- # # This only reaches sync listeners
30
- # bus.sync.emit(event)
31
- #
32
- # # This only reaches async listeners (an async bus must be set)
33
- # bus.async.emit(event)
34
- #
35
- # Please note that there is currently no way to reach sync listeners
36
- # without having to implement error handling on the emitter side.
37
- #
38
- # For listeners:
39
- #
40
- # # This will listen synchronously and make the emitter fail if
41
- # # anything goes wrong with the callback:
42
- # bus.listen(event_type) do |event|
43
- # ...
44
- # end
45
- #
46
- # # It is a shortcut for:
47
- # bus.sync.listen(event_type) do |event| ... end
48
- #
49
- # This will listen asynchronously and could not make the emitter
50
- # fail if something goes wrong with the callback.
51
- # bus.async.listen(event_type) do |event|
52
- # ...
53
- # end
54
- #
55
- # Feel free to access the sync and async busses directly for specific
56
- # cases though.
57
- #
58
- class Bus
59
- include Support::Robustness
60
-
61
- def initialize(sync = Memory::Sync.new, async = nil)
62
- @sync = sync
63
- @async = async
64
- end
65
- attr_reader :sync, :async
66
-
67
- # Emits a particular event to the listeners.
68
- #
69
- # @arg event an event, should be an Event instance (through duck
70
- # typing is allowed)
71
- def emit(event)
72
- monitor({
73
- op: "Startback::Bus#emit",
74
- op_data: {
75
- event: { type: event.type }
76
- }
77
- }, event.context) do
78
- sync.emit(event)
79
- async.emit(event) if async
80
- end
81
- end
82
-
83
- # Registers `listener` as being interested in receiving events of
84
- # a specific type.
85
- #
86
- # @arg type: Symbol, the type of event the listener is interested in.
87
- # @arg listener: Proc, the listener itself.
88
- def listen(type, processor = nil, listener = nil, &bl)
89
- sync.listen(type, processor, listener, &bl)
90
- end
91
-
92
- end # class Bus
93
- end # module Klaro
94
- require_relative 'bus/memory'
@@ -1,41 +0,0 @@
1
- require 'spec_helper'
2
- module Startback
3
- describe Bus::Memory do
4
-
5
- subject{
6
- Bus::Memory::Async.new
7
- }
8
-
9
- it 'allows emiting an receiving' do
10
- seen = nil
11
- subject.listen("user_changed") do |evt|
12
- seen = evt
13
- end
14
- subject.emit(Event.new("user_changed", {id: 12}))
15
- expect(seen).to be_a(Event)
16
- expect(seen.type).to eql("user_changed")
17
- expect(seen.data.to_h).to eql({id: 12})
18
- end
19
-
20
- it 'allows mixin Symbol vs. String for event type' do
21
- seen = nil
22
- subject.listen(:user_changed) do |evt|
23
- seen = evt
24
- end
25
- subject.emit(Event.new(:user_changed, {id: 12}))
26
- expect(seen).to be_a(Event)
27
- expect(seen.type).to eql("user_changed")
28
- expect(seen.data.to_h).to eql({id: 12})
29
- end
30
-
31
- it 'does not raise errors synchronously' do
32
- subject.listen("user_changed") do |evt|
33
- raise "An error occured"
34
- end
35
- expect {
36
- subject.emit(Event.new("user_changed", {id: 12}))
37
- }.not_to raise_error
38
- end
39
-
40
- end
41
- end
@@ -1,41 +0,0 @@
1
- require 'spec_helper'
2
- module Startback
3
- describe Bus::Memory do
4
-
5
- subject{
6
- Bus::Memory::Sync.new
7
- }
8
-
9
- it 'allows emiting an receiving' do
10
- seen = nil
11
- subject.listen("user_changed") do |evt|
12
- seen = evt
13
- end
14
- subject.emit(Event.new("user_changed", {id: 12}))
15
- expect(seen).to be_a(Event)
16
- expect(seen.type).to eql("user_changed")
17
- expect(seen.data.to_h).to eql({id: 12})
18
- end
19
-
20
- it 'allows mixin Symbol vs. String for event type' do
21
- seen = nil
22
- subject.listen(:user_changed) do |evt|
23
- seen = evt
24
- end
25
- subject.emit(Event.new(:user_changed, {id: 12}))
26
- expect(seen).to be_a(Event)
27
- expect(seen.type).to eql("user_changed")
28
- expect(seen.data.to_h).to eql({id: 12})
29
- end
30
-
31
- it 'raises emit errors synchronously' do
32
- subject.listen("user_changed") do |evt|
33
- raise "An error occured"
34
- end
35
- expect {
36
- subject.emit(Event.new("user_changed", {id: 12}))
37
- }.to raise_error("An error occured")
38
- end
39
-
40
- end
41
- end