startback 0.11.5 → 0.12.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
  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