sbmt-strangler 0.9.1

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.
Files changed (45) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +54 -0
  4. data/.rubocop_todo.yml +0 -0
  5. data/Appraisals +20 -0
  6. data/CHANGELOG.md +106 -0
  7. data/Gemfile +5 -0
  8. data/LICENSE +21 -0
  9. data/README.md +86 -0
  10. data/Rakefile +12 -0
  11. data/config/initializers/strangler.rb +5 -0
  12. data/config/initializers/yabeda.rb +40 -0
  13. data/dip.yml +67 -0
  14. data/docker-compose.yml +19 -0
  15. data/docs/img/01-proxy_mode.png +0 -0
  16. data/docs/img/02-mirror_mode.png +0 -0
  17. data/docs/img/03-replace_mode.png +0 -0
  18. data/lefthook-local.dip_example.yml +4 -0
  19. data/lefthook.yml +6 -0
  20. data/lib/sbmt/strangler/action.rb +37 -0
  21. data/lib/sbmt/strangler/action_invoker.rb +43 -0
  22. data/lib/sbmt/strangler/builder.rb +66 -0
  23. data/lib/sbmt/strangler/configurable.rb +32 -0
  24. data/lib/sbmt/strangler/configuration.rb +25 -0
  25. data/lib/sbmt/strangler/const_definer.rb +36 -0
  26. data/lib/sbmt/strangler/controller.rb +30 -0
  27. data/lib/sbmt/strangler/engine.rb +11 -0
  28. data/lib/sbmt/strangler/error_tracker.rb +34 -0
  29. data/lib/sbmt/strangler/errors.rb +7 -0
  30. data/lib/sbmt/strangler/feature_flags.rb +59 -0
  31. data/lib/sbmt/strangler/flipper.rb +38 -0
  32. data/lib/sbmt/strangler/http/client.rb +41 -0
  33. data/lib/sbmt/strangler/http/transport.rb +89 -0
  34. data/lib/sbmt/strangler/http.rb +89 -0
  35. data/lib/sbmt/strangler/logger.rb +48 -0
  36. data/lib/sbmt/strangler/metric_tracker.rb +69 -0
  37. data/lib/sbmt/strangler/mixin.rb +56 -0
  38. data/lib/sbmt/strangler/version.rb +7 -0
  39. data/lib/sbmt/strangler/work_modes/base.rb +22 -0
  40. data/lib/sbmt/strangler/work_modes/mirror.rb +73 -0
  41. data/lib/sbmt/strangler/work_modes/proxy.rb +20 -0
  42. data/lib/sbmt/strangler/work_modes/replace.rb +65 -0
  43. data/lib/sbmt/strangler.rb +84 -0
  44. data/sbmt-strangler.gemspec +59 -0
  45. metadata +473 -0
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sbmt
4
+ module Strangler
5
+ class Configuration
6
+ extend Sbmt::Strangler::Configurable
7
+
8
+ option :params_tracking_allowlist, :headers_allowlist, default: []
9
+ option :action_controller_base_class, default: "ActionController::Base"
10
+ option :error_tracker, default: "Sbmt::Strangler::ErrorTracker"
11
+ option :flipper_actor, default: ->(_http_params, _headers) {}
12
+
13
+ attr_reader :controllers, :http
14
+
15
+ def initialize(options = {})
16
+ @controllers = []
17
+ @http = ActiveSupport::InheritableOptions.new(Sbmt::Strangler::Http::DEFAULT_HTTP_OPTIONS)
18
+ end
19
+
20
+ def controller(name, &)
21
+ controllers.push(Sbmt::Strangler::Controller.new(name, self, &))
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sbmt
4
+ module Strangler
5
+ class ConstDefiner
6
+ class << self
7
+ def call!(name, klass)
8
+ const_names = name.split("::")
9
+ class_name = const_names.pop
10
+ module_name = if const_names.any?
11
+ define_modules(const_names)
12
+ else
13
+ Object
14
+ end
15
+
16
+ module_name.const_set(class_name, klass)
17
+ end
18
+
19
+ private
20
+
21
+ def define_modules(module_names)
22
+ module_names.reduce(Object) do |parent_module_name, module_name|
23
+ define_module(module_name, parent_module_name)
24
+ "#{parent_module_name}::#{module_name}".constantize
25
+ end
26
+ end
27
+
28
+ def define_module(module_name, parent_module_name)
29
+ return if parent_module_name.const_defined?(module_name, false)
30
+
31
+ parent_module_name.const_set(module_name, Module.new)
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sbmt
4
+ module Strangler
5
+ class Controller
6
+ extend Sbmt::Strangler::Configurable
7
+
8
+ option :params_tracking_allowlist, :headers_allowlist, :flipper_actor, default_from: :configuration
9
+
10
+ attr_reader :name, :class_name, :actions, :configuration
11
+
12
+ def initialize(name, configuration, &)
13
+ @name = name
14
+ @class_name = "#{name.camelize}Controller"
15
+ @actions = []
16
+ @configuration = configuration
17
+
18
+ yield(self)
19
+ end
20
+
21
+ def action(name, &)
22
+ @actions.push(Sbmt::Strangler::Action.new(name, self, &))
23
+ end
24
+
25
+ def http
26
+ @http ||= ActiveSupport::InheritableOptions.new(configuration.http)
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/engine"
4
+
5
+ module Sbmt
6
+ module Strangler
7
+ class Engine < Rails::Engine
8
+ isolate_namespace Sbmt::Strangler
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sbmt
4
+ module Strangler
5
+ class ErrorTracker
6
+ class << self
7
+ def error(message, params = {})
8
+ unless defined?(Sentry)
9
+ Sbmt::Strangler.logger.log_error(message, params)
10
+ return
11
+ end
12
+
13
+ logging(:error, message, params)
14
+ end
15
+
16
+ private
17
+
18
+ def logging(level, message, params)
19
+ params = {message: params} if params.is_a?(String)
20
+
21
+ Sentry.with_scope do |scope|
22
+ scope.set_contexts(contexts: params)
23
+
24
+ if message.is_a?(Exception)
25
+ Sentry.capture_exception(message, level: level)
26
+ else
27
+ Sentry.capture_message(message, level: level)
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sbmt
4
+ module Strangler
5
+ class ConfigurationError < StandardError; end
6
+ end
7
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sbmt
4
+ module Strangler
5
+ class FeatureFlags
6
+ FLAGS = %i[
7
+ mirror
8
+ replace
9
+ ]
10
+ FEATURES_HEADER_NAME = "HTTP_STRANGLER_FEATURES"
11
+
12
+ attr_reader :strangler_action, :rails_controller
13
+
14
+ def initialize(strangler_action:, rails_controller: nil)
15
+ @strangler_action = strangler_action
16
+ @rails_controller = rails_controller
17
+ end
18
+
19
+ FLAGS.each do |flag_name|
20
+ define_method(:"#{flag_name}?") { feature_enabled?(feature_name(flag_name)) }
21
+ end
22
+
23
+ def add_all!
24
+ FLAGS.each { |flag_name| add(feature_name(flag_name)) }
25
+ end
26
+
27
+ private
28
+
29
+ delegate :add, :enabled?, :enabled_on_time?, to: "Sbmt::Strangler::Flipper"
30
+
31
+ FEATURE_NAME_SANITIZER = -> { _1.to_s.gsub(/[^A-Za-z0-9]+/, "-") }
32
+
33
+ def feature_name(flag_name)
34
+ sanitized_controller_name = FEATURE_NAME_SANITIZER.call(strangler_action.controller.name)
35
+ sanitized_action_name = FEATURE_NAME_SANITIZER.call(strangler_action.name)
36
+ sanitized_flag_name = FEATURE_NAME_SANITIZER.call(flag_name)
37
+
38
+ "#{sanitized_controller_name}__#{sanitized_action_name}--#{sanitized_flag_name}"
39
+ end
40
+
41
+ def feature_enabled?(feature_name)
42
+ enabled?(feature_name, flipper_actor) ||
43
+ enabled_on_time?(feature_name) ||
44
+ enabled_by_header?(feature_name)
45
+ end
46
+
47
+ def flipper_actor
48
+ @flipper_actor ||= strangler_action.flipper_actor.call(rails_controller.http_params, rails_controller.request.headers)
49
+ end
50
+
51
+ def enabled_by_header?(feature_name)
52
+ features = rails_controller.request.headers[FEATURES_HEADER_NAME]
53
+ return false unless features.present? && features.is_a?(String)
54
+
55
+ features.split(",").map(&:strip).include?(feature_name)
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sbmt
4
+ module Strangler
5
+ module Flipper
6
+ FLIPPER_ID_STRUCT = Struct.new(:flipper_id)
7
+ ONTIME_ACTOR_REGEXP = /^ONTIME:(\d{2})-(\d{2})$/
8
+
9
+ class << self
10
+ delegate :add, to: ::Flipper
11
+
12
+ def enabled?(feature_name, *actors)
13
+ raise "feature name is blank" if feature_name.blank?
14
+
15
+ actors = Array(actors).flatten.compact
16
+ ::Flipper.enabled?(feature_name, *actors.map { FLIPPER_ID_STRUCT.new(_1) })
17
+ end
18
+
19
+ def enabled_on_time?(feature_name)
20
+ raise "feature name is blank" if feature_name.blank?
21
+
22
+ hours_ranges =
23
+ ::Flipper[feature_name]
24
+ .actors_value
25
+ .filter_map { |e|
26
+ e.match(ONTIME_ACTOR_REGEXP) {
27
+ $LAST_MATCH_INFO.captures.map(&:to_i)
28
+ }
29
+ }
30
+ .compact
31
+
32
+ hour_now = DateTime.now.in_time_zone.hour
33
+ hours_ranges.any? { |range| (range.first..range.last).cover?(hour_now) }
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sbmt
4
+ module Strangler
5
+ module Http
6
+ class Client
7
+ include Dry::Monads::Result::Mixin
8
+
9
+ def initialize(http_options: nil)
10
+ @http_options = http_options
11
+ end
12
+
13
+ def call(url, http_verb, payload: {}, headers: {})
14
+ case http_verb.downcase
15
+ when :get
16
+ transport.get_request(url, params: payload, headers: prepare_headers(headers))
17
+ when :post
18
+ transport.post_request(url, body: payload, headers: prepare_headers(headers))
19
+ when :put
20
+ transport.put_request(url, body: payload, headers: prepare_headers(headers))
21
+
22
+ else
23
+ raise "unsupported http verb - #{http_verb}"
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ attr_reader :http_options
30
+
31
+ def transport
32
+ @transport ||= Sbmt::Strangler::Http::Transport.new(http_options: http_options)
33
+ end
34
+
35
+ def prepare_headers(headers)
36
+ headers&.transform_keys { |key| key.sub("HTTP_", "").tr("_", "-") }
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sbmt
4
+ module Strangler
5
+ module Http
6
+ class Transport
7
+ include Dry::Monads::Do
8
+ include Dry::Monads::Result::Mixin
9
+
10
+ def initialize(http_options: nil)
11
+ @http_options = http_options
12
+ end
13
+
14
+ def get_request(url, params: {}, headers: {})
15
+ with_error_handling(url) do
16
+ response = connection.get(url, params, headers)
17
+ Success(body: response.body, status: response.status, headers: response.headers)
18
+ end
19
+ end
20
+
21
+ def post_request(url, body: {}, headers: {})
22
+ with_error_handling(url) do
23
+ response = connection.post(url, body, headers)
24
+ Success(body: response.body, status: response.status, headers: response.headers)
25
+ end
26
+ end
27
+
28
+ def put_request(url, body: {}, headers: {})
29
+ with_error_handling(url) do
30
+ response = connection.put(url, body, headers)
31
+ Success(body: response.body, status: response.status, headers: response.headers)
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ attr_reader :http_options
38
+
39
+ def connection
40
+ @connection ||= Faraday.new do |conn|
41
+ conn.response :raise_error
42
+ Sbmt::Strangler::Http.configure_faraday(conn, name: "strangler_http_client", http_options: http_options)
43
+ # Skip JSON parsing because
44
+ # 1. it speeds up proxy mode and
45
+ # 2. allows us to duplicate proxy response easily.
46
+ # conn.response :json
47
+ conn.request :json
48
+ end
49
+ end
50
+
51
+ def with_error_handling(url)
52
+ retry_count ||= 0
53
+
54
+ yield
55
+ rescue Faraday::ConnectionFailed => error
56
+ Sbmt::Strangler.logger.error(
57
+ message: "Sbmt::Strangler::Http::Transport ConnectionFailed",
58
+ url: url,
59
+ attempt: retry_count + 1,
60
+ retries_count: http_options.retries_count
61
+ )
62
+
63
+ retry if (retry_count += 1) && retry_count <= http_options.retries_count
64
+
65
+ Failure(status: :bad_gateway)
66
+ rescue Faraday::UnprocessableEntityError, Faraday::ForbiddenError => error
67
+ Failure(body: error.response_body, status: error.response_status, headers: error.response_headers)
68
+ rescue Faraday::TimeoutError
69
+ Sbmt::Strangler.logger.error(
70
+ message: "Sbmt::Strangler::Http::Transport TimeoutError",
71
+ url: url
72
+ )
73
+ Failure(status: :gateway_timeout)
74
+ rescue Faraday::Error => error
75
+ response = error.response
76
+ Sbmt::Strangler.logger.error(error.message)
77
+ Sbmt::Strangler.error_tracker.error(error)
78
+ return Failure(status: :internal_server_error) unless response
79
+
80
+ Failure(body: response[:body], status: response[:status], headers: response[:headers])
81
+ rescue => error
82
+ Sbmt::Strangler.logger.error(error.message)
83
+ Sbmt::Strangler.error_tracker.error(error)
84
+ Failure(status: :internal_server_error)
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "http/client"
4
+ require_relative "http/transport"
5
+
6
+ module Sbmt
7
+ module Strangler
8
+ module Http
9
+ DEFAULT_HTTP_OPTIONS = {
10
+ keepalive_pool_size: 256,
11
+ keepalive_idle_timeout: 30,
12
+ timeout: 5,
13
+ read_timeout: 5,
14
+ write_timeout: 5,
15
+ open_timeout: 1,
16
+ retries_count: 1
17
+ }.freeze
18
+ REQUEST_PATH_FILTER_REGEX = %r{(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})|(/\d+)|(/[A-Z]\d{9,11}(-\d{1})?)}
19
+
20
+ # Configures Faraday connection. Sets default options and adds default middlewares into chain.
21
+ # Accepts an optional block to configure net-http-persistent-adapter
22
+ #
23
+ # @example
24
+ #
25
+ # @conn ||= Faraday.new(@base_url) do |f|
26
+ # Sbmt::Strangler::Http.configure_faraday(f, name: "http-client") do |http|
27
+ # http.idle_timeout = 42
28
+ # end
29
+ # f.timeout = 5
30
+ # f.response :json
31
+ # end
32
+ #
33
+ # @param [Faraday::Connection] conn
34
+ # @param [Hash] opts faraday & middlewares options
35
+ # @option opts [String] :name client name for tracing and instrumentation. Required.
36
+ # @option opts [Hash] :adapter_opts net_http_persistent adapter options
37
+ # @option opts [ActiveSupport::InheritableOptions] :http_options timeout options
38
+ # @option opts [Regexp] :request_path_filter_regex (REQUEST_PATH_FILTER_REGEX) regex for filtering out
39
+ # variables from http request metric `path` tag. Set to false to add empty value instead.
40
+ #
41
+ # @return [Faraday::Connection]
42
+ def self.configure_faraday(conn, opts = {})
43
+ raise ConfigurationError, "Faraday client :name must be set" unless opts[:name]
44
+
45
+ http_options = opts[:http_options] || Sbmt::Strangler.configuration.http
46
+
47
+ conn.options.timeout = http_options.timeout
48
+ conn.options.read_timeout = http_options.read_timeout
49
+ conn.options.open_timeout = http_options.open_timeout
50
+ conn.options.write_timeout = http_options.write_timeout
51
+
52
+ configure_faraday_metrics(conn, opts.slice(:name, :request_path_filter_regex))
53
+
54
+ adapter_opts = {pool_size: http_options.keepalive_pool_size}.merge(opts[:adapter_opts] || {})
55
+ conn.adapter :net_http_persistent, adapter_opts do |http|
56
+ http.idle_timeout = http_options.keepalive_idle_timeout
57
+ yield http if block_given?
58
+ end
59
+
60
+ conn
61
+ end
62
+
63
+ def self.configure_faraday_metrics(conn, opts = {})
64
+ @subscribers ||= {}
65
+ name = opts.fetch(:name)
66
+ instrument_full_name = ["request.faraday", name].compact.join(".")
67
+ filter = opts.fetch(:request_path_filter_regex, REQUEST_PATH_FILTER_REGEX)
68
+
69
+ conn.request :instrumentation, name: instrument_full_name
70
+ return if @subscribers[instrument_full_name]
71
+
72
+ @subscribers[instrument_full_name] = ActiveSupport::Notifications.subscribe(instrument_full_name) do |*args|
73
+ event = ActiveSupport::Notifications::Event.new(*args)
74
+ env = event.payload
75
+
76
+ tags = {
77
+ name: name,
78
+ method: env.method,
79
+ status: env.status || :error,
80
+ host: env.url.host,
81
+ path: filter ? env.url.path.gsub(filter, "/:id") : ""
82
+ }
83
+
84
+ Yabeda.sbmt_strangler.http_request_duration.measure(tags, event.duration.fdiv(1000))
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sbmt
4
+ module Strangler
5
+ class Logger
6
+ delegate :logger, to: :Rails
7
+ delegate_missing_to :logger
8
+
9
+ def log_debug(message, **params)
10
+ with_tags(**params) do
11
+ logger.debug(message)
12
+ end
13
+ end
14
+
15
+ def log_info(message, **params)
16
+ with_tags(**params) do
17
+ logger.info(message)
18
+ end
19
+ end
20
+
21
+ def log_warn(message, **params)
22
+ with_tags(**params) do
23
+ logger.warn(message)
24
+ end
25
+ end
26
+
27
+ def log_error(message, **params)
28
+ with_tags(**params) do
29
+ logger.error(message)
30
+ end
31
+ end
32
+
33
+ def log_success(message, **params)
34
+ log_info(message, status: "success", **params)
35
+ end
36
+
37
+ def log_failure(message, **params)
38
+ log_error(message, status: "failure", **params)
39
+ end
40
+
41
+ def with_tags(**params)
42
+ logger.tagged(**params) do
43
+ yield
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sbmt
4
+ module Strangler
5
+ class MetricTracker
6
+ attr_reader :rails_controller
7
+
8
+ def initialize(rails_controller)
9
+ @rails_controller = rails_controller
10
+ end
11
+
12
+ def track_params_usage
13
+ ::Yabeda.sbmt_strangler.params_usage.increment(common_tags)
14
+ end
15
+
16
+ def log_unallowed_params
17
+ unallowed_params = all_request_params - allowed_request_params
18
+ Sbmt::Strangler.logger.log_warn(<<~WARN.strip) if unallowed_params.any?
19
+ Not allowed parameters in #{controller_path}##{action_name}: #{unallowed_params}
20
+ WARN
21
+ end
22
+
23
+ def track_work_mode(mode)
24
+ yabeda_tags = common_tags.merge(mode: mode.to_s)
25
+ ::Yabeda.sbmt_strangler.work_mode.increment(yabeda_tags)
26
+ end
27
+
28
+ def track_mirror_call(success)
29
+ yabeda_tags = common_tags.merge(success: success.to_s)
30
+ ::Yabeda.sbmt_strangler.mirror_call.increment(yabeda_tags)
31
+ end
32
+
33
+ def track_compare_call(success)
34
+ yabeda_tags = common_tags.merge(success: success.to_s)
35
+ ::Yabeda.sbmt_strangler.compare_call.increment(yabeda_tags)
36
+ end
37
+
38
+ def track_compare_call_result(value)
39
+ yabeda_tags = common_tags.merge(value: value.to_s)
40
+ ::Yabeda.sbmt_strangler.compare_call_result.increment(yabeda_tags)
41
+ end
42
+
43
+ def track_render_call(success)
44
+ yabeda_tags = common_tags.merge(success: success.to_s)
45
+ ::Yabeda.sbmt_strangler.render_call.increment(yabeda_tags)
46
+ end
47
+
48
+ private
49
+
50
+ delegate :http_params, :allowed_params, :controller_path, :action_name, to: :rails_controller
51
+
52
+ def common_tags
53
+ {
54
+ params: allowed_request_params.join(","),
55
+ controller: controller_path,
56
+ action: action_name
57
+ }
58
+ end
59
+
60
+ def allowed_request_params
61
+ @allowed_request_params ||= allowed_params.keys.map(&:to_s).sort.uniq
62
+ end
63
+
64
+ def all_request_params
65
+ @all_request_params ||= http_params.keys.map(&:to_s).sort.uniq
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sbmt
4
+ module Strangler
5
+ module Mixin
6
+ attr_reader :strangler_action
7
+
8
+ def http_params
9
+ params.to_unsafe_h.except(:action, :controller, :format)
10
+ end
11
+
12
+ def allowed_params
13
+ return http_params if strangler_action.params_tracking_allowlist.blank?
14
+
15
+ params.permit(*strangler_action.params_tracking_allowlist).to_h
16
+ end
17
+
18
+ def allowed_headers
19
+ if strangler_action.headers_allowlist.blank?
20
+ return request.headers.select { |name, _| name.starts_with?("HTTP_") }.to_h
21
+ end
22
+
23
+ request.headers.select { |name, _| name.in?(strangler_action.headers_allowlist) }.to_h
24
+ end
25
+
26
+ def http_request(payload)
27
+ strangler_action.http_client.call(
28
+ proxy_url,
29
+ strangler_action.proxy_http_method,
30
+ payload: payload,
31
+ headers: allowed_headers
32
+ )
33
+ end
34
+
35
+ def proxy_url
36
+ case strangler_action.proxy_url
37
+ in String => url
38
+ url
39
+ in Proc => proc
40
+ proc.call(http_params, request.headers)
41
+ end
42
+ end
43
+
44
+ def render_origin_response(response)
45
+ if response.success?
46
+ body, status = response.value!.values_at(:body, :status)
47
+ render json: body, status: status
48
+ return
49
+ end
50
+
51
+ body, status = response.failure.values_at(:body, :status)
52
+ render json: body, status: status
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sbmt
4
+ module Strangler
5
+ VERSION = "0.9.1"
6
+ end
7
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sbmt
4
+ module Strangler
5
+ module WorkModes
6
+ class Base
7
+ attr_reader :rails_controller, :strangler_action, :metric_tracker, :feature_flags
8
+
9
+ def initialize(rails_controller:, strangler_action:, metric_tracker:, feature_flags:)
10
+ @rails_controller = rails_controller
11
+ @strangler_action = strangler_action
12
+ @metric_tracker = metric_tracker
13
+ @feature_flags = feature_flags
14
+ end
15
+
16
+ def call
17
+ raise NotImplementedError
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end