startback 0.11.4 → 0.12.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 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'