startback 0.11.3 → 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: 83733eb966d9d9679a15840dfa76f717aacaacc5c3eb417c6bf518a3eea1e3cd
4
- data.tar.gz: f13815553606f4567e136beb86fc19a066720a3bcdf520c3781c12a732808f47
3
+ metadata.gz: 67ffd7abceaf1b4e048cc8f02f668b682ea81f62fc7de5228a9f0d12abab1df8
4
+ data.tar.gz: 1eb075bdec30a569726bc3f2a74c1ce427aef8ff0c3cbcee401a61c034e71ca6
5
5
  SHA512:
6
- metadata.gz: 0a2c80c7c65fbe845784a6ee1326bed2a1d6f89eed591580800e1ee9f403fea24b61e92b1f1ad13356f612308a30c6ecc17f6c6a16fadb9c82ae6dec61b68d31
7
- data.tar.gz: 18dc6c715ba6ed1715c6d838421476959942611a120ba04eed7a72d40d505e53c60b64ecbf4e877d2a99d2c46300cfa8e83f6cb70960bb72ad599d7d5802509f
6
+ metadata.gz: 8cb4d6f1302a34cab267c836c1e0d81c54267607de7f24d350183ff05a79af7cc216a6f34bc96c0e1204f71cca1de744fde448590e99bd96a1ab45c15c8ceddf
7
+ data.tar.gz: fecf573e39eb04317d756414349d94092f24a07210629a55347191ef74ec94fcc8079f8a4f38b6c58bf94be5c59fbd79a90a168a4925819ce771269ccae6e3da
@@ -0,0 +1,71 @@
1
+ module Startback
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
+ #
13
+ class Agent
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
68
+
69
+ end # class Agent
70
+ end # class Event
71
+ end # module Starback
@@ -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'
@@ -0,0 +1,171 @@
1
+ require 'rack'
2
+ require 'webrick'
3
+ require 'startback'
4
+ module Startback
5
+ class Event
6
+ #
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
+ #
12
+ # The Engine automatically runs a Webrick small webapp
13
+ # with a /healthcheck webservice. The class can be extended
14
+ # and method `on_health_check` overriden to run specific
15
+ # checks.
16
+ #
17
+ # This class goes hand in hand with the `startback:engine`
18
+ # docker image. It can be extended by subclasses to override
19
+ # the following methods:
20
+ #
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)
25
+ #
26
+ class Engine
27
+ include Support::Robustness
28
+
29
+ DEFAULT_OPTIONS = {
30
+
31
+ # To be passed to ServerEngine
32
+ server_engine: {}
33
+
34
+ }
35
+
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
53
+ end
54
+
55
+ # This method is executed on health check and can be
56
+ # overriden by subclasses to perform specific checks.
57
+ def on_health_check
58
+ "Ok"
59
+ end
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
+
70
+ def run(options = {})
71
+ connect
72
+
73
+ log(:info, self, "Running agents and server engine!")
74
+ create_agents
75
+ Runner.new(self, options[:server_engine] || {}).run
76
+ end
77
+
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'
101
+ end
102
+ attr_reader :engine, :options
103
+
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
110
+ end
111
+
112
+ class << self
113
+ def run(*args, &bl)
114
+ new.run(*args, &bl)
115
+ end
116
+
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
125
+ end
126
+ end
127
+
128
+ def build_worker(engine, health)
129
+ Module.new do
130
+ include Support::Env
131
+
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
142
+ end
143
+
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
165
+ end
166
+ end
167
+ end # class << self
168
+ end # class Runner
169
+ end # class Engine
170
+ end # class Event
171
+ end # module Startback
@@ -41,3 +41,6 @@ module Startback
41
41
 
42
42
  end # class Event
43
43
  end # module Startback
44
+ require_relative 'event/agent'
45
+ require_relative 'event/bus'
46
+ require_relative 'event/engine'
@@ -0,0 +1,41 @@
1
+ module Startback
2
+ module Support
3
+ # This method provides the `env` and `env!` methods that
4
+ # help querying environment variables easily.
5
+ module Env
6
+
7
+ # Returns an environment variable or raise an error if
8
+ # not set.
9
+ #
10
+ # The result is always a String with no leading/trailing
11
+ # spaces.
12
+ #
13
+ # If a block is given, the environment variable is yield
14
+ # and the result of the block returned.
15
+ def env!(key, default = nil, &bl)
16
+ v = ENV[key].to_s.strip
17
+ raise Startback::Error, "Missing ENV var `#{key}`" if v.empty?
18
+
19
+ env(key, default, &bl)
20
+ end
21
+ module_function :env!
22
+
23
+ # Returns an environment variable or the default value
24
+ # passed as second argument.
25
+ #
26
+ # The result is always a String with no leading/trailing
27
+ # spaces.
28
+ #
29
+ # If a block is given, the environment variable is yield
30
+ # and the result of the block returned.
31
+ def env(key, default = nil, &bl)
32
+ v = ENV[key].to_s.strip
33
+ v = v.empty? ? default : v
34
+ v = bl.call(v) if bl && v
35
+ v
36
+ end
37
+ module_function :env
38
+
39
+ end # module Env
40
+ end # module Support
41
+ end # module Startback
@@ -3,6 +3,8 @@ module Startback
3
3
  class LogFormatter
4
4
 
5
5
  def call(severity, time, progname, msg)
6
+ msg = { message: msg } if msg.is_a?(String)
7
+ msg = { error: msg } if msg.is_a?(Exception)
6
8
  {
7
9
  severity: severity,
8
10
  time: time
@@ -43,7 +43,7 @@ module Startback
43
43
  @@default_logger ||= begin
44
44
  l = ::Logger.new(STDOUT)
45
45
  l.formatter = LogFormatter.new
46
- l.warn(op: "#{self}", op_data: { msg: "Using default logger", trace: caller })
46
+ l.warn(op: "#{self}", op_data: { msg: "Using default logger to STDOUT" })
47
47
  @@default_logger = l
48
48
  end
49
49
  @@default_logger
@@ -67,6 +67,8 @@ module Startback
67
67
  log_msg.dup
68
68
  elsif log_msg.is_a?(String)
69
69
  log_msg = { op: "#{log_msg}#{method.nil? ? '' : '#'+method.to_s}" }
70
+ elsif log_msg.is_a?(Exception)
71
+ log_msg = { error: log_msg }
70
72
  else
71
73
  log_msg = log_msg.class unless log_msg.is_a?(Module)
72
74
  log_msg = { op: "#{log_msg.name}##{method}" }
@@ -105,8 +107,6 @@ module Startback
105
107
  }
106
108
  Tools.info(args, op_took: took)
107
109
  result
108
- rescue => ex
109
- raise
110
110
  end
111
111
 
112
112
  # Executes the block without letting errors propagate.