startback 0.11.5 → 0.12.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,162 @@ 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
+ @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
39
54
  end
40
55
 
56
+ # This method is executed on health check and can be
57
+ # overriden by subclasses to perform specific checks.
41
58
  def on_health_check
42
59
  "Ok"
43
60
  end
44
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
+
45
71
  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
72
+ connect
73
+
74
+ log(:info, self, "Running agents and server engine!")
75
+ create_agents
76
+ Runner.new(self, options[:server_engine] || {}).run
52
77
  end
53
78
 
54
- class << self
55
- def run(*args, &bl)
56
- new.run(*args, &bl)
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)
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'
57
106
  end
107
+ attr_reader :engine, :options
58
108
 
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
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
68
115
  end
69
116
 
70
- def build_worker(health)
71
- Module.new do
72
- include Support::Env
117
+ class << self
118
+ def run(*args, &bl)
119
+ new.run(*args, &bl)
120
+ end
73
121
 
74
- def initialize
75
- @stop_flag = ServerEngine::BlockingFlag.new
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
76
130
  end
131
+ end
77
132
 
78
- define_method(:health) do
79
- health
80
- end
133
+ def build_worker(engine, health)
134
+ Module.new do
135
+ include Support::Env
81
136
 
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
- })
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
88
147
  end
89
- end
90
148
 
91
- def stop
92
- @stop_flag.set!
93
- Rack::Handler::WEBrick.shutdown
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
94
170
  end
95
171
  end
96
- end
97
- end # class << self
172
+ end # class << self
173
+ end # class Runner
98
174
  end # class Engine
99
175
  end # class Event
100
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,11 @@ 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
+ context = context.fork(parsed['context']) if context
27
+ klass.new(parsed['type'], parsed['data'], context)
31
28
  end
32
29
 
33
30
  def to_json(*args, &bl)
@@ -41,5 +38,8 @@ module Startback
41
38
 
42
39
  end # class Event
43
40
  end # module Startback
41
+ require_relative 'event/ext/context'
42
+ require_relative 'event/ext/operation'
44
43
  require_relative 'event/agent'
44
+ require_relative 'event/bus'
45
45
  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 = 2
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
 
@@ -10,3 +10,28 @@ end
10
10
  RSpec.configure do |c|
11
11
  c.include SpecHelpers
12
12
  end
13
+
14
+ class SubContext < Startback::Context
15
+ attr_accessor :foo
16
+ h_factory do |c,h|
17
+ c.foo = h["foo"]
18
+ end
19
+ h_dump do |h|
20
+ h.merge!("foo" => foo)
21
+ end
22
+ end
23
+
24
+ class SubContext
25
+ attr_accessor :bar
26
+ h_factory do |c,h|
27
+ c.bar = h["bar"]
28
+ end
29
+ h_dump do |h|
30
+ h.merge!("bar" => bar)
31
+ end
32
+ end
33
+
34
+ class User
35
+ class Changed < Startback::Event
36
+ end
37
+ end
@@ -3,12 +3,8 @@ require 'spec_helper'
3
3
  module Startback
4
4
  describe Context, "dup" do
5
5
 
6
- class Subcontext < Context
7
- attr_accessor :foo
8
- end
9
-
10
6
  let(:context) {
11
- Subcontext.new.tap{|s| s.foo = "bar" }
7
+ SubContext.new.tap{|s| s.foo = "bar" }
12
8
  }
13
9
 
14
10
  class ContextRelatedAbstraction
@@ -27,7 +23,7 @@ module Startback
27
23
  expect(x).not_to be(context)
28
24
  }
29
25
  expect(seen).to be(got)
30
- expect(got).to be_a(Subcontext)
26
+ expect(got).to be_a(SubContext)
31
27
  expect(got).not_to be(context)
32
28
  expect(got.foo).to eql("bar")
33
29
  end
@@ -43,4 +39,4 @@ module Startback
43
39
  end
44
40
 
45
41
  end
46
- end
42
+ end
@@ -0,0 +1,38 @@
1
+ require 'spec_helper'
2
+
3
+ module Startback
4
+ describe Context, "fork" do
5
+
6
+ it 'is a simple dup without args' do
7
+ context = SubContext.new
8
+ context.foo = ['hello']
9
+
10
+ forked = context.fork
11
+ puts "Forked: #{forked.inspect}"
12
+ expect(fork).not_to be(context)
13
+ expect(forked.foo).to eql(['hello'])
14
+ expect(forked.foo).to be(context.foo)
15
+ end
16
+
17
+ it 'yields the context if a block is provided' do
18
+ context = SubContext.new
19
+
20
+ seen = false
21
+ context.fork({ 'foo' => 'hello' }) do |forked|
22
+ expect(fork).not_to be(context)
23
+ expect(forked.foo).to eql('hello')
24
+ seen = true
25
+ end
26
+ expect(seen).to eql(true)
27
+ end
28
+
29
+ it 'uses the factory on the hash provided' do
30
+ context = SubContext.new
31
+
32
+ forked = context.fork({ 'foo' => 'hello' })
33
+ expect(fork).not_to be(context)
34
+ expect(forked.foo).to eql('hello')
35
+ end
36
+
37
+ end
38
+ end
@@ -7,26 +7,6 @@ module Startback
7
7
  expect(Context.new.to_json).to eql("{}")
8
8
  end
9
9
 
10
- class SubContext < Context
11
- attr_accessor :foo
12
- h_factory do |c,h|
13
- c.foo = h["foo"]
14
- end
15
- h_dump do |h|
16
- h.merge!("foo" => foo)
17
- end
18
- end
19
-
20
- class SubContext
21
- attr_accessor :bar
22
- h_factory do |c,h|
23
- c.bar = h["bar"]
24
- end
25
- h_dump do |h|
26
- h.merge!("bar" => bar)
27
- end
28
- end
29
-
30
10
  it 'allows installing factories' do
31
11
  expect(Context.h_factories).to be_empty
32
12
  expect(SubContext.h_factories.size).to eql(2)
@@ -10,18 +10,18 @@ module Startback
10
10
  include Rack::Test::Methods
11
11
 
12
12
  def app
13
- opts = middleware_options
13
+ build_args = self.build_args
14
14
  Rack::Builder.new do
15
- use Middleware, opts
15
+ use Middleware, *build_args
16
16
  run ->(env){
17
- ctx = env[Startback::Context::Middleware::RACK_ENV_KEY]
17
+ ctx = env[Startback::Context::Middleware::RACK_ENV_KEY]
18
18
  [200, {}, ctx.class.to_s]
19
19
  }
20
20
  end
21
21
  end
22
22
 
23
- context 'when used without option' do
24
- let(:middleware_options){ nil }
23
+ context 'when used without context' do
24
+ let(:build_args){ [] }
25
25
 
26
26
  it 'sets the default context class' do
27
27
  get '/'
@@ -31,9 +31,7 @@ module Startback
31
31
  end
32
32
 
33
33
  context 'when specifying the context class' do
34
- let(:middleware_options){{
35
- context_class: MyContextSubClass
36
- }}
34
+ let(:build_args){ [MyContextSubClass.new] }
37
35
 
38
36
  it 'sets the default context class' do
39
37
  get '/'
@@ -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
@@ -4,7 +4,7 @@ module Startback
4
4
  describe Event do
5
5
 
6
6
  subject{
7
- Event.new("user_changed", { "foo" => "bar" })
7
+ Event.new("User::Changed", { "foo" => "bar" })
8
8
  }
9
9
 
10
10
  it 'presents an ostruct on top of its data' do
@@ -15,7 +15,7 @@ module Startback
15
15
 
16
16
  JSON_SRC = <<-JSON.gsub(/\s+/, "")
17
17
  {
18
- "type": "user_changed",
18
+ "type": "User::Changed",
19
19
  "data": {
20
20
  "foo": "bar"
21
21
  }
@@ -27,10 +27,10 @@ module Startback
27
27
  end
28
28
 
29
29
  it 'has a to_json that dumps the context if any' do
30
- evt = Event.new("user_changed", { "foo" => "bar" }, { "baz": "context" })
30
+ evt = Event.new("User::Changed", { "foo" => "bar" }, { "baz": "context" })
31
31
  expect(evt.to_json).to eql(<<-JSON.gsub(/\s+/, ""))
32
32
  {
33
- "type": "user_changed",
33
+ "type": "User::Changed",
34
34
  "data": {
35
35
  "foo": "bar"
36
36
  },
@@ -43,26 +43,19 @@ module Startback
43
43
 
44
44
 
45
45
  it 'has a json class method that works as expected' do
46
- evt = Event.json(JSON_SRC)
46
+ evt = Event.json(JSON_SRC, nil)
47
47
  expect(evt).to be_a(Event)
48
- expect(evt.type).to eql("user_changed")
48
+ expect(evt.type).to eql("User::Changed")
49
49
  expect(evt.data).to eql(subject.data)
50
50
  end
51
51
 
52
- it 'accepts an explicit context in the world' do
53
- evt = Event.json(JSON_SRC, context: 12)
54
- expect(evt.context).to eql(12)
52
+ it 'accepts an explicit context as second argument' do
53
+ c = SubContext.new.tap{|x| x.foo = 'hello' }
54
+ evt = Event.json(JSON_SRC, c)
55
+ expect(evt.context).not_to be(c)
56
+ expect(evt.context).to be_a(SubContext)
57
+ expect(evt.context.foo).to eql('hello')
55
58
  end
56
-
57
- it 'accepts an context factory in the world' do
58
- cf = ->(arg) {
59
- expect(arg).to eql(JSON.parse(JSON_SRC))
60
- 12
61
- }
62
- evt = Event.json(JSON_SRC, context_factory: cf)
63
- expect(evt.context).to eql(12)
64
- end
65
-
66
59
  end
67
60
 
68
61
  end
@@ -53,7 +53,7 @@ module Startback
53
53
 
54
54
  def app
55
55
  Rack::Builder.new do
56
- use Context::Middleware, context_class: MyContextWithErrorHandler
56
+ use Context::Middleware, MyContextWithErrorHandler.new
57
57
  use CatchAll
58
58
  run ->(env){ raise AnError, "Hello error" }
59
59
  end