blood_contracts-instrumentation 0.1.0
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 +7 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/.rubocop.yml +31 -0
- data/.travis.yml +7 -0
- data/CHANGELOG.md +12 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +6 -0
- data/LICENSE.txt +21 -0
- data/README.md +51 -0
- data/Rakefile +8 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/blood_contracts-instrumentation.gemspec +27 -0
- data/lib/blood_contracts-instrumentation.rb +3 -0
- data/lib/blood_contracts/instrumentation.rb +72 -0
- data/lib/blood_contracts/instrumentation/config.rb +126 -0
- data/lib/blood_contracts/instrumentation/failed_match.rb +41 -0
- data/lib/blood_contracts/instrumentation/instrument.rb +88 -0
- data/lib/blood_contracts/instrumentation/session.rb +121 -0
- data/lib/blood_contracts/instrumentation/session_finalizer.rb +60 -0
- data/lib/blood_contracts/instrumentation/session_finalizer/basic.rb +27 -0
- data/lib/blood_contracts/instrumentation/session_finalizer/fibers.rb +75 -0
- data/lib/blood_contracts/instrumentation/session_finalizer/threads.rb +33 -0
- data/lib/blood_contracts/instrumentation/session_recording.rb +134 -0
- data/spec/blood_contracts/instrumentation_spec.rb +82 -0
- data/spec/spec_helper.rb +22 -0
- metadata +158 -0
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module BloodContracts
|
4
|
+
module Instrumentation
|
5
|
+
# Wrapper for exception happend during the match instrumentation
|
6
|
+
# Should not be used in the app, to distinguish between expected and
|
7
|
+
# unexpected failures
|
8
|
+
class FailedMatch < ::BC::ContractFailure
|
9
|
+
# Initialize failure type with exception
|
10
|
+
#
|
11
|
+
# @param value [Exception] rescued exception from the type match
|
12
|
+
# @option context [Hash] shared context of matching pipeline
|
13
|
+
#
|
14
|
+
# @return [FailedMatch]
|
15
|
+
#
|
16
|
+
def initialize(exception, context: {})
|
17
|
+
@errors = []
|
18
|
+
@context = context
|
19
|
+
@value = exception
|
20
|
+
@context[:exception] = exception
|
21
|
+
end
|
22
|
+
|
23
|
+
# Predicate, whether the data is valid or not
|
24
|
+
# (for the ExceptionCaught it is always False)
|
25
|
+
#
|
26
|
+
# @return [Boolean]
|
27
|
+
#
|
28
|
+
def valid?
|
29
|
+
false
|
30
|
+
end
|
31
|
+
|
32
|
+
# Reader for the exception caught
|
33
|
+
#
|
34
|
+
# @return [Exception]
|
35
|
+
#
|
36
|
+
def exception
|
37
|
+
@context[:exception]
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module BloodContracts
|
4
|
+
module Instrumentation
|
5
|
+
# Base class for instrumentation tooling
|
6
|
+
class Instrument
|
7
|
+
class << self
|
8
|
+
# Builds an Instrument class from the proto and before/after callbacks
|
9
|
+
#
|
10
|
+
# When `proto` is just a Proc - we create new Instrument class around
|
11
|
+
# Otherwise - use the `proto` object as an instrument
|
12
|
+
#
|
13
|
+
# Also if before/after is defined we add the definition to the `proto`
|
14
|
+
#
|
15
|
+
# @param proto [#call, Proc] callable object that is used as an
|
16
|
+
# instrumentation tool
|
17
|
+
# @option before [#call, Proc] definition of before callback, it runs
|
18
|
+
# right after Session#start in the matching pipeline, the argument
|
19
|
+
# is Session instance for current BC::Refined#match call
|
20
|
+
# @option before [#call, Proc] definition of before callback, it runs
|
21
|
+
# right after Session#finish in the matching pipeline, the argument
|
22
|
+
# is Session instance for current BC::Refined#match call
|
23
|
+
#
|
24
|
+
# @return [Instrument, #call]
|
25
|
+
#
|
26
|
+
def build(proto, before: nil, after: nil)
|
27
|
+
raise ArgumentError unless proto.respond_to?(:call)
|
28
|
+
|
29
|
+
instance = instrument_from_proc(proto)
|
30
|
+
|
31
|
+
if before.respond_to?(:call)
|
32
|
+
inst.define_singleton_method(:before, &before)
|
33
|
+
end
|
34
|
+
|
35
|
+
define_stub(instance, :before)
|
36
|
+
|
37
|
+
if after.respond_to?(:call)
|
38
|
+
instance.define_singleton_method(:after, &after)
|
39
|
+
end
|
40
|
+
|
41
|
+
define_stub(instance, :after)
|
42
|
+
|
43
|
+
instance
|
44
|
+
end
|
45
|
+
|
46
|
+
private def define_stub(instance, name)
|
47
|
+
return if instance.respond_to?(name)
|
48
|
+
|
49
|
+
instance.define_singleton_method(name) { |_| }
|
50
|
+
end
|
51
|
+
|
52
|
+
# @private
|
53
|
+
private def instrument_from_proc(proto)
|
54
|
+
return proto unless proto.is_a?(Proc)
|
55
|
+
|
56
|
+
inst_klass = Class.new(self)
|
57
|
+
inst_klass.define_method(:call, &proto)
|
58
|
+
const_set(:"I_#{SecureRandom.hex(4)}", inst_klass)
|
59
|
+
inst_klass.new
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# Predefined interface for Instrument before callback, do-no
|
64
|
+
#
|
65
|
+
# @param _session [Session] to use in callback
|
66
|
+
#
|
67
|
+
# @return [Nothing]
|
68
|
+
#
|
69
|
+
def before(_session); end
|
70
|
+
|
71
|
+
# Predefined interface for Instrument after callback, do-no
|
72
|
+
#
|
73
|
+
# @param _session [Session] to use in callback
|
74
|
+
#
|
75
|
+
# @return [Nothing]
|
76
|
+
#
|
77
|
+
def after(_session); end
|
78
|
+
|
79
|
+
# Predefined interface for Instrument finalization call, do-no
|
80
|
+
#
|
81
|
+
# @param _session [Session] to use in callback
|
82
|
+
#
|
83
|
+
# @return [Nothing]
|
84
|
+
#
|
85
|
+
def call(_session); end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,121 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module BloodContracts
|
4
|
+
module Instrumentation
|
5
|
+
# Basic class to hold data about matching process
|
6
|
+
# Start date, finish date, result type name and the validation context
|
7
|
+
class Session
|
8
|
+
# Unique ID of the session
|
9
|
+
#
|
10
|
+
# @return [String]
|
11
|
+
#
|
12
|
+
attr_reader :id
|
13
|
+
|
14
|
+
# Time when session started
|
15
|
+
#
|
16
|
+
# @return [Time]
|
17
|
+
#
|
18
|
+
attr_reader :started_at
|
19
|
+
|
20
|
+
# Time when session finished
|
21
|
+
#
|
22
|
+
# @return [Time]
|
23
|
+
#
|
24
|
+
attr_reader :finished_at
|
25
|
+
|
26
|
+
# Frozen hash of matching pipeline context
|
27
|
+
#
|
28
|
+
# @return [Hash]
|
29
|
+
#
|
30
|
+
attr_reader :context
|
31
|
+
|
32
|
+
# Additional text about scope of the mathc (e.g. "User:12")
|
33
|
+
#
|
34
|
+
# @return [String]
|
35
|
+
#
|
36
|
+
attr_reader :scope
|
37
|
+
|
38
|
+
# Name of the type which owns the session
|
39
|
+
#
|
40
|
+
# @return [String]
|
41
|
+
#
|
42
|
+
attr_reader :matcher_type_name
|
43
|
+
|
44
|
+
# Name of the matching result type
|
45
|
+
#
|
46
|
+
# @return [String]
|
47
|
+
#
|
48
|
+
attr_reader :result_type_name
|
49
|
+
|
50
|
+
# List of matches in the pipeline run
|
51
|
+
#
|
52
|
+
# @return [Array<String>]
|
53
|
+
#
|
54
|
+
attr_reader :path
|
55
|
+
|
56
|
+
# Additional data for instrumentaion stores here
|
57
|
+
#
|
58
|
+
# @return [Hash]
|
59
|
+
#
|
60
|
+
attr_reader :extras
|
61
|
+
|
62
|
+
# Whether the result was valid or not
|
63
|
+
#
|
64
|
+
# @return [Boolean]
|
65
|
+
#
|
66
|
+
def valid?
|
67
|
+
!!@valid
|
68
|
+
end
|
69
|
+
|
70
|
+
# Initialize the session with matcher type name with defaults
|
71
|
+
#
|
72
|
+
# @param type_name [String] name of the type which owns the session
|
73
|
+
#
|
74
|
+
# @return [Nothing]
|
75
|
+
#
|
76
|
+
def initialize(type_name)
|
77
|
+
@id = SecureRandom.hex(10)
|
78
|
+
@matcher_type_name = type_name
|
79
|
+
@extras = {}
|
80
|
+
@context = {}
|
81
|
+
end
|
82
|
+
|
83
|
+
# Marks the session as started
|
84
|
+
# If you inherit the Session this method runs right BEFORE matching start
|
85
|
+
#
|
86
|
+
# @return [Nothing]
|
87
|
+
#
|
88
|
+
def start
|
89
|
+
@started_at = Time.now
|
90
|
+
end
|
91
|
+
|
92
|
+
# Session scope fallback
|
93
|
+
NO_SCOPE = "unscoped"
|
94
|
+
|
95
|
+
# Session validation path fallback
|
96
|
+
NO_VALIDATION_PATH = "undefined"
|
97
|
+
|
98
|
+
# Session result type name fallback
|
99
|
+
NO_TYPE_MATCH = "unmatched"
|
100
|
+
|
101
|
+
# Marks the session as finished (with the type)
|
102
|
+
# If you inherit the Session this method runs right AFTER matching
|
103
|
+
# finished (even if an exception raised the method would be called)
|
104
|
+
#
|
105
|
+
# @param type_match [BC::Refined] result type of matching pipeline
|
106
|
+
#
|
107
|
+
# @return [Nothing]
|
108
|
+
#
|
109
|
+
def finish(type_match)
|
110
|
+
@finished_at = Time.now
|
111
|
+
@context = type_match.context.dup.freeze if type_match
|
112
|
+
@valid = type_match&.valid?
|
113
|
+
|
114
|
+
@result_type_name = type_match&.class&.name || NO_TYPE_MATCH
|
115
|
+
@id = @context.fetch(:session_id) { @id }
|
116
|
+
@scope = @context.fetch(:scope) { NO_SCOPE }
|
117
|
+
@path = @context.fetch(:steps) { NO_VALIDATION_PATH }
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module BloodContracts
|
4
|
+
module Instrumentation
|
5
|
+
# Top-level interface for Instrument finalizers
|
6
|
+
module SessionFinalizer
|
7
|
+
module_function
|
8
|
+
|
9
|
+
require_relative "./session_finalizer/basic.rb"
|
10
|
+
require_relative "./session_finalizer/fibers.rb"
|
11
|
+
require_relative "./session_finalizer/threads.rb"
|
12
|
+
|
13
|
+
# Names of finalizers
|
14
|
+
#
|
15
|
+
# @return [Array<Symbol>]
|
16
|
+
#
|
17
|
+
FINALIZERS = %i[basic fibers threads].freeze
|
18
|
+
|
19
|
+
# @private
|
20
|
+
WRONG_FINALIZER_MSG = "Choose finalizer wisely: #{FINALIZERS.join(', ')}"
|
21
|
+
|
22
|
+
# @private
|
23
|
+
DEFAULT_POOL_SIZE = 13
|
24
|
+
|
25
|
+
# Current thread instance of the Session finalizer
|
26
|
+
#
|
27
|
+
# @return [#finalize!]
|
28
|
+
#
|
29
|
+
def instance
|
30
|
+
Thread.current[:bc_session_finalizer] ||=
|
31
|
+
Instrumentation.reset_session_finalizer!
|
32
|
+
end
|
33
|
+
|
34
|
+
# Reset the finalizer by name
|
35
|
+
#
|
36
|
+
# @param name [Symbol] finalizer to find
|
37
|
+
# @param **opts [Hash] options passed to finalizer constructor
|
38
|
+
#
|
39
|
+
# @return [#finalize!]
|
40
|
+
#
|
41
|
+
def init(name, **opts)
|
42
|
+
Thread.current[:bc_session_finalizer] = find_finalizer_by(name, **opts)
|
43
|
+
end
|
44
|
+
|
45
|
+
# @private
|
46
|
+
private def find_finalizer_by(name, pool_size: DEFAULT_POOL_SIZE)
|
47
|
+
case name
|
48
|
+
when :basic
|
49
|
+
Basic
|
50
|
+
when :fibers
|
51
|
+
Fibers.new(pool_size)
|
52
|
+
when :threads
|
53
|
+
Threads
|
54
|
+
else
|
55
|
+
raise ArgumentError, WRONG_FINALIZER_MSG
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module BloodContracts
|
4
|
+
module Instrumentation
|
5
|
+
module SessionFinalizer
|
6
|
+
# Basic implementation of Session finaliazer
|
7
|
+
module Basic
|
8
|
+
# Run the instruments against the session in a loop
|
9
|
+
# Pros:
|
10
|
+
# - simplest, obvious logic
|
11
|
+
# Cons:
|
12
|
+
# - failure in one instrument affects the others
|
13
|
+
#
|
14
|
+
# @param instruments [Array<Instrument>] list of Instruments to run
|
15
|
+
# against the session
|
16
|
+
# @param session [Session] object that hold information about matching
|
17
|
+
# process, argument for Instrument#call
|
18
|
+
#
|
19
|
+
# @return [Nothing]
|
20
|
+
#
|
21
|
+
def self.finalize!(instruments, session)
|
22
|
+
instruments.each { |i| i.call(session) }
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "fiber"
|
4
|
+
|
5
|
+
module BloodContracts
|
6
|
+
module Instrumentation
|
7
|
+
module SessionFinalizer
|
8
|
+
# Threads over fibers implementation of Session finaliazer
|
9
|
+
class Fibers
|
10
|
+
# Error message when fibers pool is not enough to run all the
|
11
|
+
# instruments
|
12
|
+
STARVATION_MSG = "WARNING! BC::Instrumentation fiber starvation!"
|
13
|
+
|
14
|
+
# Pool of Fibers to finalize instrumentation session
|
15
|
+
#
|
16
|
+
# @return [Array<Fiber>]
|
17
|
+
#
|
18
|
+
attr_reader :fibers
|
19
|
+
|
20
|
+
# Initialize the fibers pool
|
21
|
+
#
|
22
|
+
# @param pool_size [Integer] number of fibers to use in a single run
|
23
|
+
def initialize(pool_size)
|
24
|
+
@fibers = pool_size.times.map { create_fiber_with_a_thread }
|
25
|
+
end
|
26
|
+
|
27
|
+
# Run the instruments against the session in a loop (each in a separate
|
28
|
+
# fiber on separate thread)
|
29
|
+
#
|
30
|
+
# Pros:
|
31
|
+
# - Each instrument call don't affect the others
|
32
|
+
# - Each instrument run in parallel (up to GIL)
|
33
|
+
# - Runs in parallel to the matching process, so should have
|
34
|
+
# minimum impact on BC::Refined#match speed
|
35
|
+
# Cons:
|
36
|
+
# - thread creation have costs
|
37
|
+
# - the pool size is limited, so if you use number of instruments
|
38
|
+
# more the pool size you have to update Config#finalizer_pool_size
|
39
|
+
# properly
|
40
|
+
#
|
41
|
+
# @param instruments [Array<Instrument>] list of Instruments to run
|
42
|
+
# against the session
|
43
|
+
# @param session [Session] object that hold information about matching
|
44
|
+
# process, argument for Instrument#call
|
45
|
+
#
|
46
|
+
# @return [Nothing]
|
47
|
+
#
|
48
|
+
def finalize!(instruments, session)
|
49
|
+
instruments.each do |instrument|
|
50
|
+
raise STARVATION_MSG unless (fiber = @fibers.shift)
|
51
|
+
|
52
|
+
fiber.resume instrument, session if fiber.alive?
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# @private
|
57
|
+
#
|
58
|
+
# Create a fiber which holds a Thread to run next Instrument#call
|
59
|
+
# against the session
|
60
|
+
#
|
61
|
+
# @return [Fiber]
|
62
|
+
protected def create_fiber_with_a_thread
|
63
|
+
Fiber.new do |instrument, session|
|
64
|
+
loop do
|
65
|
+
thread = Thread.new(instrument, session) { |i, s| i.call(s) }
|
66
|
+
@fibers.unshift Fiber.current
|
67
|
+
instrument, session = Fiber.yield
|
68
|
+
thread.join
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module BloodContracts
|
4
|
+
module Instrumentation
|
5
|
+
module SessionFinalizer
|
6
|
+
# Multi-threaded implementation of Session finalizer
|
7
|
+
module Threads
|
8
|
+
# Run the instruments against the session in a Thread in a loop
|
9
|
+
# Pros:
|
10
|
+
# - parallel execution of instruments (up to GIL)
|
11
|
+
# - one failed instrument do not affect the others
|
12
|
+
# Cons:
|
13
|
+
# - creating a thread has costs
|
14
|
+
# - do not parallel with the BC::Refined matching, as we need to
|
15
|
+
# finish the threads, join them to main Thread
|
16
|
+
#
|
17
|
+
# @param instruments [Array<Instrument>] list of Instruments to run
|
18
|
+
# against the session
|
19
|
+
# @param session [Session] object that hold information about matching
|
20
|
+
# process, argument for Instrument#call
|
21
|
+
#
|
22
|
+
# @return [Nothing]
|
23
|
+
#
|
24
|
+
def self.finalize!(instruments, session)
|
25
|
+
threads = instruments.map do |i|
|
26
|
+
Thread.new { i.call(session) }
|
27
|
+
end
|
28
|
+
threads.map(&:join)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|