startback 0.11.4 → 0.12.1

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: 929e1ab2d7ab916eadc828ecf2d1695c14c9a43d117198457e03c70c8b479ba4
4
- data.tar.gz: 2f3d212acd31e00b1910aca3c3c1154a5e8a130962aeb002c184c6a70da17ab2
3
+ metadata.gz: '085ac55f8fc68a8b8806570359e68ae9cba48a0c0cef83645bfb9f7133fb38ce'
4
+ data.tar.gz: 231685b5e7c8c53a3cb086bbdc8ef6728132a942a47b59e6ac5c1ca6560b2725
5
5
  SHA512:
6
- metadata.gz: 90d448f505406727db9cd5c1db1d2fdb14b5df6ed546566ae9f3db4e573dbe43e43968f5885c9602c5ca198ceb8a7801aca769e499ef4d10acaaf2be318f779f
7
- data.tar.gz: 01b21d439cc5b7d1c8ff218620b7eec13a8496919c941863df25d8d01f4d2c6d11b727a6c4269015ef61d1d8063ebe1487909c3b238c84c06c5211eccc568423
6
+ metadata.gz: 3c12830f86d5e0a067b801ebc752c894d7abec27c9745bb8eb307eee010a049275b49f3bfa1f55a1b7bf168c9fc3222b7039f6dc243f96c5268d369b625eabb3
7
+ data.tar.gz: dedb51337c356d9208ea727155c978dd6e937c2d8016189c17c606820975e81d1ede4e004a22878e8060dd58cd84280a4119f1a1996dae5c26bbdfc660009029
@@ -18,7 +18,7 @@ module Startback
18
18
  #
19
19
  # # Use a user defined context class
20
20
  # Rack::Builder.new do
21
- # use Startback::Context::Middleware, context_class: MyContextClass
21
+ # use Startback::Context::Middleware, MyContextClass.new
22
22
  #
23
23
  # run ->(env){
24
24
  # ctx = env[Startback::Context::Middleware::RACK_ENV_KEY]
@@ -31,18 +31,14 @@ module Startback
31
31
 
32
32
  RACK_ENV_KEY = 'SAMBACK_CONTEXT'
33
33
 
34
- DEFAULT_OPTIONS = {
35
- context_class: Context
36
- }
37
-
38
- def initialize(app, options = {})
34
+ def initialize(app, context = Context.new)
39
35
  @app = app
40
- @options = DEFAULT_OPTIONS.merge(options || {})
36
+ @context = context
41
37
  end
42
- attr_reader :options
38
+ attr_reader :context
43
39
 
44
40
  def call(env)
45
- env[RACK_ENV_KEY] ||= options[:context_class].h({}).tap{|c|
41
+ env[RACK_ENV_KEY] ||= context.dup.tap{|c|
46
42
  c.original_rack_env = env.dup
47
43
  }
48
44
  @app.call(env)
@@ -0,0 +1,73 @@
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_data|
42
+ event = engine.factor_event(event_data)
43
+ dup.call(event)
44
+ end
45
+ end
46
+
47
+ # Synchronously listen to a specific event.
48
+ #
49
+ # See Bus#listen
50
+ def sync(exchange, queue)
51
+ bus.listen(exchange, queue) do |event_data|
52
+ event = engine.factor_event(event_data)
53
+ dup.call(event)
54
+ end
55
+ end
56
+
57
+ # Reacts to a specific event.
58
+ #
59
+ # This method must be implemented by subclasses and raises
60
+ # an error by default.
61
+ def call(event = nil)
62
+ log(:fatal, {
63
+ op: self.class,
64
+ op_data: event,
65
+ error: %Q{Unexpected call to Startback::Event::Agent#call},
66
+ backtrace: caller
67
+ })
68
+ raise NotImplementedError
69
+ end
70
+
71
+ end # class Agent
72
+ end # class Event
73
+ end # module Starback
@@ -0,0 +1,162 @@
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
+ stop_errors(self, "listen") do
127
+ (listener || bl).call(body)
128
+ end
129
+ end
130
+ end
131
+
132
+ protected
133
+
134
+ def consumer_pool_size
135
+ options[:consumer_pool_size]
136
+ end
137
+
138
+ def abort_on_exception?
139
+ options[:abort_on_exception]
140
+ end
141
+
142
+ def fanout_options
143
+ options[:fanout_options]
144
+ end
145
+
146
+ def queue_options
147
+ options[:queue_options]
148
+ end
149
+
150
+ def factor_event(body)
151
+ if options[:event_factory]
152
+ options[:event_factory].call(body)
153
+ else
154
+ Event.json(body, options)
155
+ end
156
+ end
157
+
158
+ end # class Async
159
+ end # module Bunny
160
+ end # class Bus
161
+ end # class Event
162
+ 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,176 @@
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
+ @context.engine = self
40
+ end
41
+ attr_reader :options, :context
42
+
43
+ class << self
44
+ def auto_create_agents?
45
+ !!@auto_create_agents
46
+ end
47
+
48
+ # Register a base class which will be used to discover
49
+ # the agents to start when the engine is ran.
50
+ def auto_create_agents(base_class = nil)
51
+ @auto_create_agents ||= base_class
52
+ @auto_create_agents
53
+ end
54
+ end
55
+
56
+ # This method is executed on health check and can be
57
+ # overriden by subclasses to perform specific checks.
58
+ def on_health_check
59
+ "Ok"
60
+ end
61
+
62
+ def bus
63
+ ::Startback::Event::Bus.new
64
+ end
65
+
66
+ def connect
67
+ log(:info, self, "Connecting to the bus now!")
68
+ bus.connect
69
+ end
70
+
71
+ def run(options = {})
72
+ connect
73
+
74
+ log(:info, self, "Running agents and server engine!")
75
+ create_agents
76
+ Runner.new(self, options[:server_engine] || {}).run
77
+ end
78
+
79
+ def create_agents
80
+ return unless parent = self.class.auto_create_agents
81
+
82
+ ObjectSpace
83
+ .each_object(Class)
84
+ .select { |klass| klass < parent }
85
+ .each { |klass| klass.new(self) }
86
+ end
87
+
88
+ def factor_event(event_data)
89
+ Event.json(event_data, context.dup)
90
+ end
91
+
92
+ class Runner
93
+
94
+ DEFAULT_SERVER_ENGINE_OPTIONS = {
95
+ daemonize: false,
96
+ worker_type: 'process',
97
+ workers: 1
98
+ }
99
+
100
+ def initialize(engine, options = {})
101
+ raise ArgumentError if engine.nil?
102
+
103
+ @engine = engine
104
+ @options = DEFAULT_SERVER_ENGINE_OPTIONS.merge(options)
105
+ require 'serverengine'
106
+ end
107
+ attr_reader :engine, :options
108
+
109
+ def run(options = {})
110
+ health = self.class.build_health_check(engine)
111
+ worker = self.class.build_worker(engine, health)
112
+ se = ServerEngine.create(nil, worker, options)
113
+ se.run
114
+ se
115
+ end
116
+
117
+ class << self
118
+ def run(*args, &bl)
119
+ new.run(*args, &bl)
120
+ end
121
+
122
+ def build_health_check(engine)
123
+ Rack::Builder.new do
124
+ map '/health-check' do
125
+ health = Startback::Web::HealthCheck.new {
126
+ engine.on_health_check
127
+ }
128
+ run(health)
129
+ end
130
+ end
131
+ end
132
+
133
+ def build_worker(engine, health)
134
+ Module.new do
135
+ include Support::Env
136
+
137
+ def initialize
138
+ @stop_flag = ServerEngine::BlockingFlag.new
139
+ end
140
+
141
+ define_method(:health) do
142
+ health
143
+ end
144
+
145
+ define_method(:engine) do
146
+ engine
147
+ end
148
+
149
+ def run
150
+ ran = false
151
+ until @stop_flag.set?
152
+ if ran
153
+ engine.send(:log, :warn, engine, "Restarting internal loop")
154
+ else
155
+ engine.send(:log, :info, engine, "Starting internal loop")
156
+ end
157
+ Rack::Handler::WEBrick.run(health, {
158
+ :Port => env('STARTBACK_ENGINE_PORT', '3000').to_i,
159
+ :Host => env('STARTBACK_ENGINE_LISTEN', '0.0.0.0')
160
+ })
161
+ ran = true
162
+ end
163
+ end
164
+
165
+ def stop
166
+ engine.send(:log, :info, engine, "Stopping internal loop")
167
+ @stop_flag.set!
168
+ Rack::Handler::WEBrick.shutdown
169
+ end
170
+ end
171
+ end
172
+ end # class << self
173
+ end # class Runner
174
+ end # class Engine
175
+ end # class Event
176
+ end # module Startback
@@ -0,0 +1,5 @@
1
+ module Startback
2
+ class Context
3
+ attr_accessor :engine
4
+ end # class Context
5
+ end # module Startback
@@ -0,0 +1,13 @@
1
+ module Startback
2
+ class Operation
3
+
4
+ def self.emits(type, &bl)
5
+ after_call do
6
+ event_data = instance_exec(&bl)
7
+ event = type.new(type.to_s, event_data, context)
8
+ context.engine.bus.emit(event)
9
+ end
10
+ end
11
+
12
+ end # class Operation
13
+ end # module Startback
@@ -20,14 +20,10 @@ module Startback
20
20
  end
21
21
  attr_reader :context, :type, :data
22
22
 
23
- def self.json(src, world = {})
23
+ def self.json(src, context)
24
24
  parsed = JSON.parse(src)
25
- context = if world[:context]
26
- world[:context]
27
- elsif world[:context_factory]
28
- world[:context_factory].call(parsed)
29
- end
30
- Event.new(parsed['type'], parsed['data'], context)
25
+ klass = Kernel.const_get(parsed['type'])
26
+ klass.new(parsed['type'], parsed['data'], context)
31
27
  end
32
28
 
33
29
  def to_json(*args, &bl)
@@ -41,3 +37,8 @@ module Startback
41
37
 
42
38
  end # class Event
43
39
  end # module Startback
40
+ require_relative 'event/ext/context'
41
+ require_relative 'event/ext/operation'
42
+ require_relative 'event/agent'
43
+ require_relative 'event/bus'
44
+ require_relative 'event/engine'