startback 0.11.5 → 0.12.2

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.
@@ -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