startback 0.4.5 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/lib/startback/audit/trailer.rb +145 -0
  3. data/lib/startback/audit.rb +1 -0
  4. data/lib/startback/bus/bunny/async.rb +117 -0
  5. data/lib/startback/bus/bunny.rb +1 -0
  6. data/lib/startback/bus/memory/async.rb +40 -0
  7. data/lib/startback/bus/memory/sync.rb +30 -0
  8. data/lib/startback/bus/memory.rb +2 -0
  9. data/lib/startback/bus.rb +94 -0
  10. data/lib/startback/caching/entity_cache.rb +80 -0
  11. data/lib/startback/caching/store.rb +34 -0
  12. data/lib/startback/context/middleware.rb +1 -1
  13. data/lib/startback/context.rb +93 -4
  14. data/lib/startback/event.rb +43 -0
  15. data/lib/startback/operation.rb +39 -6
  16. data/lib/startback/support/fake_logger.rb +18 -0
  17. data/lib/startback/support/hooks.rb +48 -0
  18. data/lib/startback/support/operation_runner.rb +150 -0
  19. data/lib/startback/support/robustness.rb +153 -0
  20. data/lib/startback/support.rb +3 -0
  21. data/lib/startback/version.rb +2 -2
  22. data/lib/startback/web/api.rb +3 -4
  23. data/lib/startback/web/catch_all.rb +12 -5
  24. data/lib/startback/web/middleware.rb +13 -0
  25. data/lib/startback.rb +2 -0
  26. data/spec/spec_helper.rb +2 -0
  27. data/spec/unit/audit/test_trailer.rb +88 -0
  28. data/spec/unit/bus/memory/test_async.rb +41 -0
  29. data/spec/unit/bus/memory/test_sync.rb +41 -0
  30. data/spec/unit/caching/test_entity_cache.rb +109 -0
  31. data/spec/unit/context/test_abstraction_factory.rb +64 -0
  32. data/spec/unit/support/hooks/test_after_hook.rb +54 -0
  33. data/spec/unit/support/hooks/test_before_hook.rb +54 -0
  34. data/spec/unit/support/operation_runner/test_around_run.rb +157 -0
  35. data/spec/unit/support/operation_runner/test_before_after_call.rb +48 -0
  36. data/spec/unit/support/test_robusteness.rb +209 -0
  37. data/spec/unit/test_context.rb +51 -0
  38. data/spec/unit/test_event.rb +69 -0
  39. data/spec/unit/test_operation.rb +0 -3
  40. metadata +32 -4
@@ -0,0 +1,43 @@
1
+ module Startback
2
+ #
3
+ # An Event occuring a given context and having a type and attached data.
4
+ #
5
+ # Event instances have String types that are by default unrelated to ruby
6
+ # classes. Also, this Event class has a `json` information contract that
7
+ # allows dumping & reloading them easily. A context or context_factory may
8
+ # be provided in dress world to reload the event context from data, but
9
+ # that logic is opaque to this class.
10
+ #
11
+ # This class is intended to be subclassed if a more specific event protocol
12
+ # is wanted.
13
+ #
14
+ class Event
15
+
16
+ def initialize(type, data, context = nil)
17
+ @type = type.to_s
18
+ @data = OpenStruct.new(data)
19
+ @context = context
20
+ end
21
+ attr_reader :context, :type, :data
22
+
23
+ def self.json(src, world = {})
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)
31
+ end
32
+
33
+ def to_json(*args, &bl)
34
+ h = {
35
+ type: self.type,
36
+ data: data.to_h
37
+ }
38
+ h[:context] = context if context
39
+ h.to_json(*args, &bl)
40
+ end
41
+
42
+ end # class Event
43
+ end # module Startback
@@ -1,16 +1,45 @@
1
1
  module Startback
2
+ #
3
+ # High-level Operation abstraction, that is a piece of code that executes
4
+ # on demand and (generally) changes the state of the software system.
5
+ #
6
+ # An operation is basically an object that respond to `call`, but that
7
+ # executes within a given world (see `bind`). It also has before and
8
+ # after hooks that allows specifying what needs to be done before invoking
9
+ # call and after having invoked it. All this protocol is actually under
10
+ # the responsibility of an `OperationRunner`. Operations should not be
11
+ # called manually by third-party code.
12
+ #
13
+ # Example:
14
+ #
15
+ # class SayHello < Startback::Operation
16
+ #
17
+ # before_call do
18
+ # # e.g. check_some_permissions
19
+ # end
20
+ #
21
+ # def call
22
+ # puts "Hello"
23
+ # end
24
+ #
25
+ # after_call do
26
+ # # e.g. log and/or emit something on a bus
27
+ # end
28
+ #
29
+ # end
30
+ #
2
31
  class Operation
3
32
  include Errors
33
+ include Support::OperationRunner
34
+ include Support::Hooks.new(:call)
4
35
 
5
36
  attr_accessor :world
6
-
7
37
  protected :world=
8
38
 
9
39
  def bind(world)
10
40
  return self unless world
11
- dup.tap{|op|
12
- op.world = world
13
- }
41
+ self.world = world
42
+ self
14
43
  end
15
44
 
16
45
  def method_missing(name, *args, &bl)
@@ -19,10 +48,14 @@ module Startback
19
48
  world.fetch(name){ super }
20
49
  end
21
50
 
51
+ def respond_to?(name, *args)
52
+ super || (world && world.has_key?(name))
53
+ end
54
+
22
55
  protected
23
56
 
24
- def run(operation)
25
- operation.bind(self.world).call
57
+ def operation_world(op)
58
+ self.world
26
59
  end
27
60
 
28
61
  end # class Operation
@@ -0,0 +1,18 @@
1
+ module Startback
2
+ module Support
3
+ class FakeLogger < Logger
4
+
5
+ def initialize(*args)
6
+ @last_msg = nil
7
+ end
8
+ attr_reader :last_msg
9
+
10
+ [:debug, :info, :warn, :error, :fatal].each do |meth|
11
+ define_method(meth) do |msg|
12
+ @last_msg = msg
13
+ end
14
+ end
15
+
16
+ end # class Logger
17
+ end # module Support
18
+ end # module Startback
@@ -0,0 +1,48 @@
1
+ module Startback
2
+ module Support
3
+ class Hooks < Module
4
+
5
+ def initialize(suffix)
6
+ @suffix = suffix
7
+ define_method :"before_#{suffix}" do
8
+ self.class.__befores.each do |bl|
9
+ instance_exec(&bl)
10
+ end
11
+ end
12
+ define_method :"after_#{suffix}" do
13
+ self.class.__afters.each do |bl|
14
+ instance_exec(&bl)
15
+ end
16
+ end
17
+ end
18
+ attr_reader :suffix
19
+
20
+ def included(by)
21
+ by.instance_eval %Q{
22
+ def __befores(create = false)
23
+ if create
24
+ @__befores ||= (superclass.respond_to?(:__befores, false) ? superclass.__befores.dup : [])
25
+ end
26
+ @__befores || (superclass.respond_to?(:__befores, false) ? superclass.__befores : [])
27
+ end
28
+
29
+ def __afters(create = false)
30
+ if create
31
+ @__afters ||= (superclass.respond_to?(:__afters, false) ? superclass.__afters.dup : [])
32
+ end
33
+ @__afters || (superclass.respond_to?(:__afters, false) ? superclass.__afters : [])
34
+ end
35
+
36
+ def before_#{suffix}(&bl)
37
+ __befores(true) << bl
38
+ end
39
+
40
+ def after_#{suffix}(&bl)
41
+ __afters(true) << bl
42
+ end
43
+ }
44
+ end
45
+
46
+ end # class Hooks
47
+ end # module Support
48
+ end # module Startback
@@ -0,0 +1,150 @@
1
+ module Startback
2
+ module Support
3
+ #
4
+ # Support module for high-level architectural components that
5
+ # execute operations as part of their logic, see e.g. Web::Api.
6
+ #
7
+ # This module contributes a `run` instance method that allows
8
+ # binding an operation with a world, and executing it while
9
+ # supporting around runners.
10
+ #
11
+ # Example:
12
+ #
13
+ # class HighLevelComponent
14
+ # include Startback::Support::OperationRunner
15
+ #
16
+ # def some_method
17
+ # # Runs the operation passed after some binding
18
+ # run SomeOperation.new
19
+ # end
20
+ #
21
+ # protected
22
+ #
23
+ # # Overriden to inject some extra world
24
+ # def operation_world(op)
25
+ # super(op).merge({ hello: "world" })
26
+ # end
27
+ #
28
+ # # Execute this around op
29
+ # around_run do |op, then_block|
30
+ # puts "About to run #{op.inspect}"
31
+ # then_block.call
32
+ # end
33
+ #
34
+ # # SomeClass#call will be called with the operation
35
+ # # as first parameter and a block as continuation
36
+ # around_run SomeClass.new
37
+ #
38
+ # end
39
+ #
40
+ module OperationRunner
41
+
42
+ # Contributes the hook DSL methods to classes that include
43
+ # the OperationRunner module
44
+ module ClassMethods
45
+
46
+ # Registers a callable to be executed around operation running.
47
+ #
48
+ # In its block form, the callable is `instance_exec`uted on the
49
+ # runner instance, with the operation passed as first parameter
50
+ # and a then_block callable as second parameter (continuation):
51
+ #
52
+ # around_run do |op,then_block|
53
+ # # do whatever you want with the op (already bounded)
54
+ # puts op.inspect
55
+ #
56
+ # # do not forget to call the continuation block
57
+ # then_block.call
58
+ # end
59
+ #
60
+ # With a parameter responding to `#call`, the latter is invoked
61
+ # with the operation as parameter and a block as continuation:
62
+ #
63
+ # class Arounder
64
+ #
65
+ # def call(op)
66
+ # # do whatever you want with the op (already bounded)
67
+ # puts op.inspect
68
+ #
69
+ # # do not forget to call the continuation block
70
+ # yield
71
+ # end
72
+ #
73
+ # end
74
+ #
75
+ def around_run(arounder = nil, &bl)
76
+ raise ArgumentError, "Arg or block required" unless arounder || bl
77
+ arounds(true) << [arounder || bl, arounder.nil?]
78
+ end
79
+
80
+ private
81
+
82
+ def arounds(create = false)
83
+ if create
84
+ @arounds ||= superclass.respond_to?(:arounds, true) \
85
+ ? superclass.send(:arounds, true).dup \
86
+ : []
87
+ end
88
+ @arounds || (superclass.respond_to?(:arounds, true) ? superclass.send(:arounds, true) : [])
89
+ end
90
+
91
+ end
92
+
93
+ # When included by a class/module, install the DSL methods
94
+ def self.included(by)
95
+ by.extend(ClassMethods)
96
+ end
97
+
98
+ # Runs `operation`, taking care of binding it and executing
99
+ # hooks.
100
+ #
101
+ # This method is NOT intended to be overriden. Use hooks and
102
+ # `operation_world` to impact default behavior.
103
+ def run(operation)
104
+ op_world = operation_world(operation)
105
+ op_bound = operation.bind(op_world)
106
+ _run_befores(op_bound)
107
+ r = _run_with_arounds(op_bound, self.class.send(:arounds))
108
+ _run_afters(op_bound)
109
+ r
110
+ end
111
+
112
+ protected
113
+
114
+ # Returns the world to use to bind an operation.
115
+ #
116
+ # The default implementation returns an empty hash. This is
117
+ # intended to be overriden by classes including this module.
118
+ def operation_world(op)
119
+ {}
120
+ end
121
+
122
+ private
123
+
124
+ def _run_befores(op_bound)
125
+ op_bound.before_call if op_bound.respond_to?(:before_call, true)
126
+ end
127
+
128
+ def _run_with_arounds(operation, arounds = [])
129
+ if arounds.empty?
130
+ operation.call
131
+ else
132
+ arounder, iexec = arounds.first
133
+ after_first = ->() {
134
+ _run_with_arounds(operation, arounds[1..-1])
135
+ }
136
+ if iexec
137
+ self.instance_exec(operation, after_first, &arounder)
138
+ else
139
+ arounder.call(self, operation, &after_first)
140
+ end
141
+ end
142
+ end
143
+
144
+ def _run_afters(op_bound)
145
+ op_bound.after_call if op_bound.respond_to?(:after_call, true)
146
+ end
147
+
148
+ end # module OperationRunner
149
+ end # module Support
150
+ end # module Startback
@@ -0,0 +1,153 @@
1
+ module Startback
2
+ module Support
3
+ #
4
+ # This module provides helper methods for robustness of a software design.
5
+ #
6
+ # It is included by main Startback abstractions, and can be included by
7
+ # specific software components who needs fine-tuning of monitoring, logging
8
+ # and error handling.
9
+ #
10
+ # All public methods here follow the following free args parameters:
11
+ #
12
+ # 1. First (& second) argument(s) form the log message.
13
+ #
14
+ # A full log message is a Hash having :op (required), :op_took (optional),
15
+ # and :op_data (optional) keys.
16
+ #
17
+ # If a String (or two) are used instead, a log message will be built taking
18
+ # the former as the executer (a class or instance) and the second as a method.
19
+ # `{ op: "executer#method" }`
20
+ #
21
+ # 2. The second (or third) argument should be a Logger instance, a Context,
22
+ # or an instance knowing its context. The best logger is extracted from it
23
+ # and used for actual logging.
24
+ #
25
+ # Examples:
26
+ #
27
+ # log(op: "hello", op_data: {foo: 12}) => logged as such on STDOUT
28
+ # log("A simple message") => { op: "A simple message" } on STDOUT
29
+ # log(Startback, "hello") => { op: "Startback#hello" } on STDOUT
30
+ # log(Event.new, "hello") => { op: "Event#hello" } on STDOUT
31
+ # log(self, context) => { op: "..." } on context's logger or STDOUT
32
+ # log(self, event) => { op: "..." } on event context's logger or STDOUT
33
+ # ...
34
+ #
35
+ module Robustness
36
+
37
+ # Included to avoid poluting the space of the including
38
+ # classes.
39
+ module Tools
40
+
41
+ def default_logger
42
+ @@default_logger ||= ::Logger.new(STDOUT)
43
+ from = caller.reject{|x| x =~ /lib\/startback/ }.first
44
+ @@default_logger.debug("Watch out, using default logger from: #{from}")
45
+ @@default_logger
46
+ end
47
+ module_function :default_logger
48
+
49
+ def logger_for(arg)
50
+ return arg if arg.is_a?(::Logger)
51
+ return arg.logger if arg.is_a?(Context) && arg.logger
52
+ return logger_for(arg.context) if arg.respond_to?(:context, false)
53
+ default_logger
54
+ end
55
+ module_function :logger_for
56
+
57
+ def parse_args(log_msg, method = nil, context = nil, extra = nil)
58
+ method, context, extra = nil, method, context unless method.is_a?(String)
59
+ context, extra = nil, context if context.is_a?(Hash) && extra.nil?
60
+ logger = logger_for(context) || logger_for(log_msg)
61
+ log_msg = if log_msg.is_a?(Hash)
62
+ log_msg.dup
63
+ elsif log_msg.is_a?(String)
64
+ log_msg = { op: "#{log_msg}#{method.nil? ? '' : '#'+method.to_s}" }
65
+ else
66
+ log_msg = log_msg.class unless log_msg.is_a?(Module)
67
+ log_msg = { op: "#{log_msg.name}##{method}" }
68
+ end
69
+ log_msg.merge!(extra) if extra
70
+ [ log_msg, logger ]
71
+ end
72
+ module_function :parse_args
73
+
74
+ [:debug, :info, :warn, :error, :fatal].each do |meth|
75
+ define_method(meth) do |args, extra = nil, &bl|
76
+ act_args = args + [extra]
77
+ log_msg, logger = parse_args(*act_args)
78
+ logger.send(meth, log_msg)
79
+ end
80
+ module_function(meth)
81
+ end
82
+
83
+ end # module Tools
84
+
85
+ # Logs a specific message with a given severity.
86
+ #
87
+ # Severity can be :debug, :info, :warn, :error or :fatal.
88
+ # The args must follow module's conventions, see above.
89
+ def log(severity, *args)
90
+ Tools.send(severity, args)
91
+ end
92
+
93
+ # Calls the block and monitors then log its execution time.
94
+ #
95
+ # The args must follow module's conventions, see above.
96
+ def monitor(*args, &bl)
97
+ result = nil
98
+ took = Benchmark.realtime {
99
+ result = bl.call
100
+ }
101
+ Tools.debug(args, op_took: took)
102
+ result
103
+ rescue => ex
104
+ Tools.error(args, error: ex)
105
+ raise
106
+ end
107
+
108
+ # Executes the block without letting errors propagate.
109
+ # Errors are logged, though. Nothing is logged if everything
110
+ # goes fine.
111
+ #
112
+ # The args must follow module's conventions, see above.
113
+ def stop_errors(*args, &bl)
114
+ result = nil
115
+ took = Benchmark.realtime {
116
+ result = bl.call
117
+ }
118
+ result
119
+ rescue => ex
120
+ Tools.error(args, op_took: took, error: ex)
121
+ nil
122
+ end
123
+
124
+ # Tries executing the block up to `n` times, until an attempt
125
+ # succeeds (then returning the result). Logs the first and last
126
+ # fatal error, if any.
127
+ #
128
+ # The args must follow module's conventions, see above.
129
+ def try_max_times(n, *args, &bl)
130
+ retried = 0
131
+ took = 0
132
+ begin
133
+ result = nil
134
+ took += Benchmark.realtime {
135
+ result = bl.call
136
+ }
137
+ result
138
+ rescue => ex
139
+ Tools.error(args + [{op_took: took, error: ex}]) if retried == 0
140
+ retried += 1
141
+ if retried < n
142
+ sleep(retried)
143
+ retry
144
+ else
145
+ Tools.fatal(args + [{op_took: took, error: ex}])
146
+ raise
147
+ end
148
+ end
149
+ end
150
+
151
+ end # module Robustness
152
+ end # module Support
153
+ end # module Startback
@@ -15,3 +15,6 @@ module Startback
15
15
  end # module Support
16
16
  end # module Startback
17
17
  require_relative 'support/logger'
18
+ require_relative 'support/robustness'
19
+ require_relative 'support/hooks'
20
+ require_relative 'support/operation_runner'
@@ -1,8 +1,8 @@
1
1
  module Startback
2
2
  module Version
3
3
  MAJOR = 0
4
- MINOR = 4
5
- TINY = 5
4
+ MINOR = 5
5
+ TINY = 0
6
6
  end
7
7
  VERSION = "#{Version::MAJOR}.#{Version::MINOR}.#{Version::TINY}"
8
8
  end
@@ -20,11 +20,10 @@ module Startback
20
20
  ###
21
21
  ### Facade over third party tools
22
22
  ###
23
+ include Support::OperationRunner
23
24
 
24
- def run(operation)
25
- operation
26
- .bind({ context: context })
27
- .call
25
+ def operation_world(op)
26
+ { context: context }
28
27
  end
29
28
 
30
29
  ###
@@ -19,6 +19,7 @@ module Startback
19
19
  #
20
20
  class CatchAll < Rack::Robustness
21
21
  include Errors
22
+ include Support::Robustness
22
23
 
23
24
  FATAL_ERROR = {
24
25
  code: "Startback::Errors::InternalServerError",
@@ -32,12 +33,18 @@ module Startback
32
33
  self.body FATAL_ERROR
33
34
 
34
35
  self.ensure(true) do |ex|
36
+ STDERR.puts(ex.message)
35
37
  context = env[Context::Middleware::RACK_ENV_KEY]
36
- if context && context.respond_to?(:error_handler) && context.error_handler
37
- context.error_handler.fatal(ex)
38
- else
39
- Startback::LOGGER.fatal(ex.message)
40
- Startback::LOGGER.fatal(ex.backtrace.join("\n"))
38
+ begin
39
+ if context && context.respond_to?(:error_handler, true) && context.error_handler
40
+ context.error_handler.fatal(ex)
41
+ else
42
+ log(:fatal, self, "ensure", error: ex)
43
+ end
44
+ rescue => ex2
45
+ STDERR.puts(ex2.message)
46
+ STDERR.puts(ex2.backtrace[0..10].join("\n"))
47
+ raise
41
48
  end
42
49
  end
43
50
 
@@ -0,0 +1,13 @@
1
+ module Startback
2
+ module Web
3
+ module Middleware
4
+
5
+ protected
6
+
7
+ def context(env = @env)
8
+ ::Startback::Context::Middleware.context(env) || Errors.server_error!("Unable to find context!!")
9
+ end
10
+
11
+ end # module Middleware
12
+ end # module Web
13
+ end # module Startback
data/lib/startback.rb CHANGED
@@ -3,6 +3,8 @@ require 'rack/robustness'
3
3
  require 'finitio'
4
4
  require 'logger'
5
5
  require 'path'
6
+ require 'ostruct'
7
+ require 'benchmark'
6
8
  # Provides a reusable backend framework for backend components written
7
9
  # in ruby.
8
10
  #
data/spec/spec_helper.rb CHANGED
@@ -1,5 +1,7 @@
1
1
  $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
2
2
  require 'startback'
3
+ require 'startback/bus'
4
+ require 'startback/support/fake_logger'
3
5
  require 'rack/test'
4
6
 
5
7
  module SpecHelpers
@@ -0,0 +1,88 @@
1
+ require 'spec_helper'
2
+ require 'startback/audit'
3
+ module Startback
4
+ module Audit
5
+ describe Trailer do
6
+
7
+ let(:trailer) {
8
+ Trailer.new("/tmp/trail.log")
9
+ }
10
+
11
+ describe "op_data" do
12
+
13
+ def op_data(op, trailer = self.trailer)
14
+ trailer.send(:op_data, op)
15
+ end
16
+
17
+ it 'uses to_trail in priority if provided' do
18
+ op = OpenStruct.new(to_trail: { foo: "bar" }, input: 12, request: 13)
19
+ expect(op_data(op)).to eql({ foo: "bar" })
20
+ end
21
+
22
+ it 'uses input then' do
23
+ op = OpenStruct.new(input: { foo: "bar" }, request: 13)
24
+ expect(op_data(op)).to eql({ foo: "bar" })
25
+ end
26
+
27
+ it 'uses request then' do
28
+ op = OpenStruct.new(request: { foo: "bar" })
29
+ expect(op_data(op)).to eql({ foo: "bar" })
30
+ end
31
+
32
+ it 'applies default blacklists for security reasons' do
33
+ op = OpenStruct.new(input: {
34
+ token: "will not be dumped",
35
+ a_token: "will not be dumped",
36
+ AToken: "will not be dumped",
37
+ password: "will not be dumped",
38
+ secret: "will not be dumped",
39
+ credentials: "will not be dumped",
40
+ foo: "bar"
41
+ })
42
+ expect(op_data(op)).to eql({
43
+ foo: "bar"
44
+ })
45
+ end
46
+
47
+ it 'applies default blacklists to data arrays too' do
48
+ op = OpenStruct.new(input: [{
49
+ token: "will not be dumped",
50
+ a_token: "will not be dumped",
51
+ AToken: "will not be dumped",
52
+ password: "will not be dumped",
53
+ secret: "will not be dumped",
54
+ credentials: "will not be dumped",
55
+ foo: "bar"
56
+ }])
57
+ expect(op_data(op)).to eql([{
58
+ foo: "bar"
59
+ }])
60
+ end
61
+
62
+ it 'uses the stop words provided at construction' do
63
+ t = Trailer.new("/tmp/trail.log", blacklist: "hello and world")
64
+ op = OpenStruct.new(request: { Hello: "bar", World: "foo", foo: "bar" })
65
+ expect(op_data(op, t)).to eql({ foo: "bar" })
66
+ end
67
+
68
+ end
69
+
70
+ describe "op_context" do
71
+
72
+ def op_context(op, trailer = self.trailer)
73
+ trailer.send(:op_context, op)
74
+ end
75
+
76
+ it 'applies default blacklists for security reasons' do
77
+ op = OpenStruct.new(context: {
78
+ token: "will not be dumped",
79
+ foo: "bar"
80
+ })
81
+ expect(op_context(op)).to eql({ foo: "bar" })
82
+ end
83
+
84
+ end
85
+
86
+ end
87
+ end
88
+ end