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,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module Sbmt
6
+ module Strangler
7
+ module WorkModes
8
+ class Mirror < Base
9
+ include Dry::Monads::Result::Mixin
10
+
11
+ def call
12
+ mirror_task = Concurrent::Promises.future { mirror_call }
13
+ proxy_task = Concurrent::Promises.future { http_request(http_params) }
14
+
15
+ mirror_call_result = mirror_task.value!
16
+ origin_response = proxy_task.value!
17
+
18
+ begin
19
+ track_mirror_call(mirror_call_result.success?)
20
+ if mirror_call_result.success?
21
+ mirror_result = mirror_call_result.value!
22
+ origin_result = copy_of_origin_result(origin_response)
23
+ compare_call_result = compare_call(origin_result, mirror_result)
24
+ track_compare_call(compare_call_result.success?)
25
+ track_compare_call_result(compare_call_result.value!) if compare_call_result.success?
26
+ end
27
+ rescue => err
28
+ handle_error(err)
29
+ end
30
+
31
+ render_origin_response(origin_response)
32
+ end
33
+
34
+ private
35
+
36
+ delegate :http_params, :http_request, :render_origin_response, to: :rails_controller
37
+ delegate :track_mirror_call, :track_compare_call, :track_compare_call_result, to: :metric_tracker
38
+
39
+ def copy_of_origin_result(origin_response)
40
+ if origin_response.success?
41
+ origin_response.value!.deep_dup
42
+ else
43
+ origin_response.failure.deep_dup
44
+ end
45
+ end
46
+
47
+ def mirror_call
48
+ value = strangler_action.mirror.call(rails_controller)
49
+ Success(value)
50
+ rescue => err
51
+ handle_error(err)
52
+ Failure(nil)
53
+ end
54
+
55
+ MATCH_ERROR = :error
56
+
57
+ def compare_call(origin_result, mirror_result)
58
+ cmp = strangler_action.compare.call(origin_result, mirror_result)
59
+ raise "Strangler action compare lambda must return a boolean value instead of #{cmp}!" unless cmp.in?([true, false])
60
+ Success(cmp)
61
+ rescue => err
62
+ handle_error(err)
63
+ Failure(nil)
64
+ end
65
+
66
+ def handle_error(err)
67
+ Sbmt::Strangler.error_tracker.error(err)
68
+ Sbmt::Strangler.logger.error(err)
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module Sbmt
6
+ module Strangler
7
+ module WorkModes
8
+ class Proxy < Base
9
+ def call
10
+ origin_response = http_request(http_params)
11
+ render_origin_response(origin_response)
12
+ end
13
+
14
+ private
15
+
16
+ delegate :http_params, :http_request, :render_origin_response, to: :rails_controller
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module Sbmt
6
+ module Strangler
7
+ module WorkModes
8
+ class Replace < Base
9
+ include Dry::Monads::Result::Mixin
10
+
11
+ def call
12
+ mirror_call_result = mirror_call
13
+ track_mirror_call(mirror_call_result.success?)
14
+
15
+ unless mirror_call_result.success?
16
+ render(
17
+ json: {error: "Mirror lambda call failed!"},
18
+ status: :internal_server_error
19
+ ) # TODO: Возможно стоит сделать фолбэк на проксирование?
20
+ return
21
+ end
22
+
23
+ render_call_result = render_call(mirror_call_result.value!)
24
+ track_render_call(render_call_result.success?)
25
+
26
+ unless render_call_result.success?
27
+ render(
28
+ json: {error: "Render lambda call failed!"},
29
+ status: :internal_server_error
30
+ ) # TODO: Возможно стоит сделать фолбэк на проксирование?
31
+ return
32
+ end
33
+
34
+ render render_call_result.value!
35
+ end
36
+
37
+ private
38
+
39
+ delegate :render, to: :rails_controller
40
+ delegate :track_mirror_call, :track_render_call, to: :metric_tracker
41
+
42
+ def mirror_call
43
+ value = strangler_action.mirror.call(rails_controller)
44
+ Success(value)
45
+ rescue => err
46
+ handle_error(err)
47
+ Failure(nil)
48
+ end
49
+
50
+ def render_call(mirror_result)
51
+ value = strangler_action.render.call(mirror_result)
52
+ Success(value)
53
+ rescue => err
54
+ handle_error(err)
55
+ Failure(nil)
56
+ end
57
+
58
+ def handle_error(err)
59
+ Sbmt::Strangler.error_tracker.error(err)
60
+ Sbmt::Strangler.logger.error(err)
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails"
4
+ require "dry-monads"
5
+ require "yabeda"
6
+ require "faraday"
7
+ require "faraday/net_http_persistent"
8
+ require "flipper"
9
+ require "concurrent"
10
+ require "securerandom"
11
+ require "oj"
12
+
13
+ begin
14
+ require "sentry-rails"
15
+ rescue LoadError
16
+ # optional dependency
17
+ end
18
+
19
+ begin
20
+ require "opentelemetry-instrumentation-concurrent_ruby"
21
+ rescue LoadError
22
+ Warning.warn <<~WARN if Object.const_defined?(:Opentelemetry)
23
+ WARNING! It looks like you're using OpenTelemetry but you didn't install an instrumentation for the concurrent-ruby gem.
24
+ sbmt-strangler runs your code using concurrent-ruby futures in mirror mode, so this is very adviced to install
25
+ the instrumentation (opentelemetry-instrumentation-concurrent_ruby gem) to get traces in mirror mode!
26
+ WARN
27
+ end
28
+
29
+ require_relative "strangler/configurable"
30
+ require_relative "strangler/http"
31
+ require_relative "strangler/action"
32
+ require_relative "strangler/controller"
33
+ require_relative "strangler/configuration"
34
+ require_relative "strangler/mixin"
35
+ require_relative "strangler/builder"
36
+ require_relative "strangler/action_invoker"
37
+ require_relative "strangler/const_definer"
38
+ require_relative "strangler/errors"
39
+ require_relative "strangler/error_tracker"
40
+ require_relative "strangler/logger"
41
+ require_relative "strangler/flipper"
42
+ require_relative "strangler/feature_flags"
43
+ require_relative "strangler/metric_tracker"
44
+ require_relative "strangler/work_modes/proxy"
45
+ require_relative "strangler/work_modes/mirror"
46
+ require_relative "strangler/work_modes/replace"
47
+
48
+ require_relative "strangler/engine"
49
+
50
+ module Sbmt
51
+ module Strangler
52
+ module_function
53
+
54
+ # Public: Configure strangler.
55
+ #
56
+ # Sbmt::Strangler.configure do |config|
57
+ # config.controller(...) do |controller|
58
+ # controller.action(...) do {...}
59
+ # end
60
+ # end
61
+ #
62
+ # Yields Sbmt::Strangler::Configuration instance.
63
+ def configure
64
+ yield configuration if block_given?
65
+ end
66
+
67
+ # Public: Returns Sbmt::Strangler::Configuration instance.
68
+ def configuration
69
+ @configuration ||= Configuration.new
70
+ end
71
+
72
+ def action_controller_base_class
73
+ @action_controller_base_class ||= configuration.action_controller_base_class.constantize
74
+ end
75
+
76
+ def error_tracker
77
+ @error_tracker ||= configuration.error_tracker.constantize
78
+ end
79
+
80
+ def logger
81
+ @logger ||= Sbmt::Strangler::Logger.new
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/sbmt/strangler/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "sbmt-strangler"
7
+ spec.version = Sbmt::Strangler::VERSION
8
+ spec.authors = ["sbermarket team"]
9
+
10
+ spec.summary = "Utility for strangler pattern"
11
+ spec.description = spec.summary
12
+ spec.homepage = "https://github.com/SberMarket-Tech/sbmt-strangler"
13
+ spec.required_ruby_version = ">= 3.1.0"
14
+
15
+ spec.metadata["allowed_push_host"] = "https://rubygems.org"
16
+
17
+ spec.metadata["homepage_uri"] = spec.homepage
18
+ spec.metadata["source_code_uri"] = spec.homepage
19
+ spec.metadata["changelog_uri"] = "#{spec.homepage}/-/blob/master/CHANGELOG.md"
20
+ spec.metadata["rubygems_mfa_required"] = "false" # rubocop:disable Gemspec/RequireMFA
21
+
22
+ spec.files = Dir.chdir(__dir__) do
23
+ `git ls-files -z`.split("\x0").reject do |f|
24
+ (f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|travis|circleci)|appveyor)})
25
+ end
26
+ end
27
+ spec.bindir = "exe"
28
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
29
+ spec.require_paths = ["lib"]
30
+
31
+ spec.add_dependency "faraday", "> 2.0"
32
+ spec.add_dependency "faraday-net_http_persistent", "~> 2.0"
33
+ spec.add_dependency "net-http-persistent", ">= 4.0.1"
34
+ spec.add_dependency "rails", ">= 6.1", "< 8"
35
+ spec.add_dependency "yabeda", ">= 0.11"
36
+ spec.add_dependency "flipper", ">= 1.2.2"
37
+ spec.add_dependency "concurrent-ruby", ">= 1.2.3"
38
+ spec.add_dependency "oj"
39
+ spec.add_dependency "dry-monads"
40
+ spec.add_dependency "dry-struct"
41
+
42
+ spec.add_development_dependency "appraisal"
43
+ spec.add_development_dependency "bundler"
44
+ spec.add_development_dependency "combustion"
45
+ spec.add_development_dependency "rake"
46
+ spec.add_development_dependency "rspec"
47
+ spec.add_development_dependency "rspec-rails"
48
+ spec.add_development_dependency "rspec_junit_formatter"
49
+ spec.add_development_dependency "rswag-specs"
50
+ spec.add_development_dependency "rubocop"
51
+ spec.add_development_dependency "rubocop-rails"
52
+ spec.add_development_dependency "rubocop-rspec"
53
+ spec.add_development_dependency "rubocop-performance"
54
+ spec.add_development_dependency "vcr"
55
+ spec.add_development_dependency "standard", ">= 1.7"
56
+ spec.add_development_dependency "zeitwerk"
57
+ spec.add_development_dependency "sentry-rails", "> 5.2.0"
58
+ spec.add_development_dependency "debug"
59
+ end