lhc 12.2.0 → 13.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/rubocop.yml +27 -0
  3. data/.github/workflows/test.yml +27 -0
  4. data/.rubocop.yml +3 -0
  5. data/.ruby-version +1 -1
  6. data/Gemfile.activesupport5 +1 -1
  7. data/Gemfile.activesupport6 +1 -1
  8. data/README.md +67 -6
  9. data/Rakefile +3 -3
  10. data/lhc.gemspec +3 -2
  11. data/lib/lhc/error.rb +3 -1
  12. data/lib/lhc/interceptor.rb +4 -0
  13. data/lib/lhc/interceptors/auth.rb +0 -4
  14. data/lib/lhc/interceptors/caching.rb +65 -44
  15. data/lib/lhc/interceptors/monitoring.rb +39 -10
  16. data/lib/lhc/interceptors/throttle.rb +9 -8
  17. data/lib/lhc/railtie.rb +0 -1
  18. data/lib/lhc/request.rb +7 -3
  19. data/lib/lhc/rspec.rb +1 -2
  20. data/lib/lhc/version.rb +1 -1
  21. data/spec/error/to_s_spec.rb +7 -2
  22. data/spec/formats/multipart_spec.rb +1 -1
  23. data/spec/formats/plain_spec.rb +1 -1
  24. data/spec/interceptors/after_response_spec.rb +1 -1
  25. data/spec/interceptors/caching/main_spec.rb +2 -2
  26. data/spec/interceptors/caching/multilevel_cache_spec.rb +139 -0
  27. data/spec/interceptors/caching/options_spec.rb +0 -11
  28. data/spec/interceptors/monitoring/caching_spec.rb +66 -0
  29. data/spec/interceptors/response_competition_spec.rb +2 -2
  30. data/spec/interceptors/return_response_spec.rb +2 -2
  31. data/spec/interceptors/throttle/main_spec.rb +95 -21
  32. data/spec/spec_helper.rb +1 -0
  33. metadata +27 -20
  34. data/Gemfile.activesupport4 +0 -4
  35. data/cider-ci.yml +0 -6
  36. data/cider-ci/bin/bundle +0 -51
  37. data/cider-ci/bin/ruby_install +0 -8
  38. data/cider-ci/bin/ruby_version +0 -25
  39. data/cider-ci/jobs/rspec-activesupport-4.yml +0 -28
  40. data/cider-ci/jobs/rspec-activesupport-5.yml +0 -27
  41. data/cider-ci/jobs/rspec-activesupport-6.yml +0 -28
  42. data/cider-ci/jobs/rubocop.yml +0 -18
  43. data/cider-ci/task_components/bundle.yml +0 -22
  44. data/cider-ci/task_components/rspec.yml +0 -36
  45. data/cider-ci/task_components/rubocop.yml +0 -29
  46. data/cider-ci/task_components/ruby.yml +0 -15
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1a536d04e90db04fdeca6edd06c3e88782106a660d410a1b908b94147a3e9154
4
- data.tar.gz: d0d89904a00069fdd8f3ef297f296e5da9d1383a4b4239aeebf00b6869be082a
3
+ metadata.gz: b7e042d800751c9fceb6d4f3e01513a1a849a3e00fb8bf7e0987bf1df73b5a45
4
+ data.tar.gz: 6fa4a578f6003a9ef46ca27ed81766ef2c4dbd07ad231d4201624274b7255153
5
5
  SHA512:
6
- metadata.gz: 858291c8b53ce6447df1cc23fabd7c74ca4307ff7590eaebb76f4d538b769f81d9d89055bf79bf564814cf55c6832fccee42918100433061331252c6aafd8042
7
- data.tar.gz: 49356178736094b85a4d9c1eab8dcf390024df5b580cb696e7f02683cbc1f1cf4a3e5bcfc6e03f5e8b9330fed1e02b94dd8bbbe17ec3c58e14b91b7352df9030
6
+ metadata.gz: 80b0d65063c77912df0cf07bc8f25acbb083af34186248689131283cb07e90f9bc2677973b96170d0eeba788b7bcde19ce0f3efddefe349501e33b3825f39b53
7
+ data.tar.gz: 2f0b273133f8fe5311eeaa4c46d11170430c58099c8599f855e303659f299fbab0ababbc5091999a0fc7c433a7eccdd80fbc343e5cac267aeae3aab7595bbdcb
@@ -0,0 +1,27 @@
1
+ name: Rubocop
2
+
3
+ on: push
4
+
5
+ jobs:
6
+ rubocop:
7
+ runs-on: ubuntu-latest
8
+
9
+ steps:
10
+ - uses: actions/checkout@v2
11
+ - uses: actions/setup-ruby@v1
12
+ with:
13
+ ruby-version: 2.7.2
14
+ - name: Cache Ruby Gems
15
+ uses: actions/cache@v2
16
+ with:
17
+ path: /.tmp/vendor/bundle
18
+ key: ${{ runner.os }}-gems-latest-${{ hashFiles('**/Gemfile.lock') }}
19
+ restore-keys: |
20
+ ${{ runner.os }}-gems-latest-
21
+ - name: Bundle Install
22
+ run: |
23
+ bundle config path /.tmp/vendor/bundle
24
+ bundle install --jobs 4 --retry 3
25
+ - name: Run Rubocop
26
+ run: |
27
+ bundle exec rubocop
@@ -0,0 +1,27 @@
1
+ name: Test
2
+
3
+ on: push
4
+
5
+ jobs:
6
+ rspec:
7
+ runs-on: ubuntu-latest
8
+
9
+ steps:
10
+ - uses: actions/checkout@v2
11
+ - uses: actions/setup-ruby@v1
12
+ with:
13
+ ruby-version: 2.7.2
14
+ - name: Cache Ruby Gems
15
+ uses: actions/cache@v2
16
+ with:
17
+ path: /.tmp/vendor/bundle
18
+ key: ${{ runner.os }}-gems-latest-${{ hashFiles('**/Gemfile.lock') }}
19
+ restore-keys: |
20
+ ${{ runner.os }}-gems-latest-
21
+ - name: Bundle Install
22
+ run: |
23
+ bundle config path /.tmp/vendor/bundle
24
+ bundle install --jobs 4 --retry 3
25
+ - name: Run Tests
26
+ run: |
27
+ bundle exec rspec
data/.rubocop.yml CHANGED
@@ -5,6 +5,9 @@ inherit_from:
5
5
 
6
6
  AllCops:
7
7
  TargetRubyVersion: 2.3
8
+ Exclude:
9
+ - vendor/**
10
+ - vendor/**/.*
8
11
 
9
12
  Lint/IneffectiveAccessModifier:
10
13
  Enabled: false
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- ruby-2.6.5
1
+ ruby-2.7.2
@@ -1,4 +1,4 @@
1
1
  source 'https://rubygems.org/'
2
2
 
3
3
  gemspec
4
- gem 'activesupport', '~> 5.0.0'
4
+ gem 'activesupport', '~> 5.2'
@@ -1,4 +1,4 @@
1
1
  source 'https://rubygems.org/'
2
2
 
3
3
  gemspec
4
- gem 'activesupport', '~> 6.0.0'
4
+ gem 'activesupport', '~> 6.0'
data/README.md CHANGED
@@ -73,6 +73,10 @@ use it like:
73
73
  * [Installation](#installation-1)
74
74
  * [Environment](#environment)
75
75
  * [What it tracks](#what-it-tracks)
76
+ * [Before and after request tracking](#before-and-after-request-tracking)
77
+ * [Response tracking](#response-tracking)
78
+ * [Timeout tracking](#timeout-tracking)
79
+ * [Caching tracking](#caching-tracking)
76
80
  * [Configure](#configure-1)
77
81
  * [Prometheus Interceptor](#prometheus-interceptor)
78
82
  * [Retry Interceptor](#retry-interceptor)
@@ -95,6 +99,7 @@ use it like:
95
99
 
96
100
 
97
101
 
102
+
98
103
  ## Basic methods
99
104
 
100
105
  Available are `get`, `post`, `put` & `delete`.
@@ -601,7 +606,6 @@ You can configure your own cache (default Rails.cache) and logger (default Rails
601
606
 
602
607
  ```ruby
603
608
  LHC::Caching.cache = ActiveSupport::Cache::MemoryStore.new
604
- LHC::Caching.logger = Logger.new(STDOUT)
605
609
  ```
606
610
 
607
611
  Caching is not enabled by default, although you added it to your basic set of interceptors.
@@ -632,6 +636,18 @@ Responses served from cache are marked as served from cache:
632
636
  response.from_cache? # true
633
637
  ```
634
638
 
639
+ You can also use a central http cache to be used by the `LHC::Caching` interceptor.
640
+
641
+ If you configure a local and a central cache, LHC will perform multi-level-caching.
642
+ LHC will try to retrieve cached information first from the central, in case of a miss from the local cache, while writing back into both.
643
+
644
+ ```ruby
645
+ LHC::Caching.central = {
646
+ read: 'redis://$PASSWORD@central-http-cache-replica.namespace:6379/0',
647
+ write: 'redis://$PASSWORD@central-http-cache-master.namespace:6379/0'
648
+ }
649
+ ```
650
+
635
651
  ##### Options
636
652
 
637
653
  ```ruby
@@ -644,7 +660,7 @@ Responses served from cache are marked as served from cache:
644
660
 
645
661
  `race_condition_ttl` - very useful in situations where a cache entry is used very frequently and is under heavy load.
646
662
  If a cache expires and due to heavy load several different processes will try to read data natively and then they all will try to write to cache.
647
- To avoid that case the first process to find an expired cache entry will bump the cache expiration time by the value set in `cache_race_condition_ttl`.
663
+ To avoid that case the first process to find an expired cache entry will bump the cache expiration time by the value set in `race_condition_ttl`.
648
664
 
649
665
  `use` - Set an explicit cache to be used for this request. If this option is missing `LHC::Caching.cache` is used.
650
666
 
@@ -733,11 +749,15 @@ It tracks request attempts with `before_request` and `after_request` (counts).
733
749
  In case your workers/processes are getting killed due limited time constraints,
734
750
  you are able to detect deltas with relying on "before_request", and "after_request" counts:
735
751
 
752
+ ###### Before and after request tracking
753
+
736
754
  ```ruby
737
755
  "lhc.<app_name>.<env>.<host>.<http_method>.before_request", 1
738
756
  "lhc.<app_name>.<env>.<host>.<http_method>.after_request", 1
739
757
  ```
740
758
 
759
+ ###### Response tracking
760
+
741
761
  In case of a successful response it reports the response code with a count and the response time with a gauge value.
742
762
 
743
763
  ```ruby
@@ -748,6 +768,17 @@ In case of a successful response it reports the response code with a count and t
748
768
  "lhc.<app_name>.<env>.<host>.<http_method>.time", 43
749
769
  ```
750
770
 
771
+ In case of a unsuccessful response it reports the response code with a count but no time:
772
+
773
+ ```ruby
774
+ LHC.get('http://local.ch')
775
+
776
+ "lhc.<app_name>.<env>.<host>.<http_method>.count", 1
777
+ "lhc.<app_name>.<env>.<host>.<http_method>.500", 1
778
+ ```
779
+
780
+ ###### Timeout tracking
781
+
751
782
  Timeouts are also reported:
752
783
 
753
784
  ```ruby
@@ -756,6 +787,30 @@ Timeouts are also reported:
756
787
 
757
788
  All the dots in the host are getting replaced with underscore, because dot is the default separator in graphite.
758
789
 
790
+ ###### Caching tracking
791
+
792
+ When you want to track caching stats please make sure you have enabled the `LHC::Caching` and the `LHC::Monitoring` interceptor.
793
+
794
+ Make sure that the `LHC::Caching` is listed before `LHC::Monitoring` interceptor when configuring interceptors:
795
+
796
+ ```ruby
797
+ LHC.configure do |c|
798
+ c.interceptors = [LHC::Caching, LHC::Monitoring]
799
+ end
800
+ ```
801
+
802
+ If a response was served from cache it tracks:
803
+
804
+ ```ruby
805
+ "lhc.<app_name>.<env>.<host>.<http_method>.cache.hit", 1
806
+ ```
807
+
808
+ If a response was not served from cache it tracks:
809
+
810
+ ```ruby
811
+ "lhc.<app_name>.<env>.<host>.<http_method>.cache.miss", 1
812
+ ```
813
+
759
814
  ##### Configure
760
815
 
761
816
  It is possible to set the key for Monitoring Interceptor on per request basis:
@@ -895,16 +950,22 @@ LHC.get('http://local.ch', options)
895
950
  LHC.get('http://local.ch', options)
896
951
  # raises LHC::Throttle::OutOfQuota: Reached predefined quota for local.ch
897
952
  ```
953
+
898
954
  **Options Description**
899
955
  * `track`: enables tracking of current limit/remaining requests of rate-limiting
900
956
  * `break`: quota in percent after which errors are raised. Percentage symbol is optional, values will be converted to integer (e.g. '23.5' will become 23)
901
957
  * `provider`: name of the provider under which throttling tracking is aggregated,
902
- * `limit`: either a hard-coded integer, or a hash pointing at the response header containing the limit value
958
+ * `limit`:
959
+ * a hard-coded integer
960
+ * a hash pointing at the response header containing the limit value
961
+ * a proc that receives the response as argument and returns the limit value
903
962
  * `remaining`:
904
963
  * a hash pointing at the response header containing the current amount of remaining requests
905
- * a proc that receives the response as argument and returns the current amount
906
- of remaining requests
907
- * `expires`: a hash pointing at the response header containing the timestamp when the quota will reset
964
+ * a proc that receives the response as argument and returns the current amount of remaining requests
965
+ * `expires`:
966
+ * a hash pointing at the response header containing the timestamp when the quota will reset
967
+ * a proc that receives the response as argument and returns the timestamp when the quota will reset
968
+
908
969
 
909
970
  #### Zipkin
910
971
 
data/Rakefile CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  begin
2
4
  require 'bundler/setup'
3
5
  rescue LoadError
@@ -17,9 +19,7 @@ end
17
19
  begin
18
20
  require 'rspec/core/rake_task'
19
21
  RSpec::Core::RakeTask.new(:spec)
20
- task :default => :spec
21
- rescue LoadError
22
- # no rspec available
22
+ task default: :spec
23
23
  end
24
24
 
25
25
  Bundler::GemHelper.install_tasks
data/lhc.gemspec CHANGED
@@ -21,14 +21,15 @@ Gem::Specification.new do |s|
21
21
 
22
22
  s.requirements << 'Ruby >= 2.0.0'
23
23
 
24
- s.add_dependency 'activesupport', '>= 4.2'
24
+ s.add_dependency 'activesupport', '>= 5.2'
25
25
  s.add_dependency 'addressable'
26
26
  s.add_dependency 'typhoeus', '>= 0.11'
27
27
 
28
28
  s.add_development_dependency 'geminabox'
29
29
  s.add_development_dependency 'prometheus-client', '~> 0.7.1'
30
30
  s.add_development_dependency 'pry'
31
- s.add_development_dependency 'rails', '>= 4.2'
31
+ s.add_development_dependency 'rails', '>= 5.2'
32
+ s.add_development_dependency 'redis'
32
33
  s.add_development_dependency 'rspec-rails', '>= 3.0.0'
33
34
  s.add_development_dependency 'rubocop', '~> 0.57.1'
34
35
  s.add_development_dependency 'rubocop-rspec', '~> 1.26.0'
data/lib/lhc/error.rb CHANGED
@@ -64,8 +64,10 @@ class LHC::Error < StandardError
64
64
  end
65
65
 
66
66
  def to_s
67
- return response if response.is_a?(String)
67
+ return response.to_s unless response.is_a?(LHC::Response)
68
68
  request = response.request
69
+ return unless request.is_a?(LHC::Request)
70
+
69
71
  debug = []
70
72
  debug << [request.method, request.url].map { |str| self.class.fix_invalid_encoding(str) }.join(' ')
71
73
  debug << "Options: #{request.options}"
@@ -29,4 +29,8 @@ class LHC::Interceptor
29
29
  def self.dup
30
30
  self
31
31
  end
32
+
33
+ def all_interceptor_classes
34
+ @all_interceptors ||= LHC::Interceptors.new(request).all.map(&:class)
35
+ end
32
36
  end
@@ -75,10 +75,6 @@ class LHC::Auth < LHC::Interceptor
75
75
  @refresh_client_token_option ||= auth_options[:refresh_client_token] || refresh_client_token
76
76
  end
77
77
 
78
- def all_interceptor_classes
79
- @all_interceptors ||= LHC::Interceptors.new(request).all.map(&:class)
80
- end
81
-
82
78
  def auth_options
83
79
  request.options[:auth] || {}
84
80
  end
@@ -3,69 +3,104 @@
3
3
  class LHC::Caching < LHC::Interceptor
4
4
  include ActiveSupport::Configurable
5
5
 
6
- config_accessor :cache, :logger
6
+ config_accessor :cache, :central
7
7
 
8
+ # to control cache invalidation across all applications in case of
9
+ # breaking changes within this inteceptor
10
+ # that do not lead to cache invalidation otherwise
8
11
  CACHE_VERSION = '1'
9
12
 
10
13
  # Options forwarded to the cache
11
14
  FORWARDED_OPTIONS = [:expires_in, :race_condition_ttl]
12
15
 
16
+ class MultilevelCache
17
+
18
+ def initialize(central: nil, local: nil)
19
+ @central = central
20
+ @local = local
21
+ end
22
+
23
+ def fetch(key)
24
+ central_response = @central[:read].fetch(key) if @central && @central[:read].present?
25
+ if central_response
26
+ puts %Q{[LHC] served from central cache: "#{key}"}
27
+ return central_response
28
+ end
29
+ local_response = @local.fetch(key) if @local
30
+ if local_response
31
+ puts %Q{[LHC] served from local cache: "#{key}"}
32
+ return local_response
33
+ end
34
+ end
35
+
36
+ def write(key, content, options)
37
+ @central[:write].write(key, content, options) if @central && @central[:write].present?
38
+ @local.write(key, content, options) if @local.present?
39
+ end
40
+ end
41
+
13
42
  def before_request
14
43
  return unless cache?(request)
15
- deprecation_warning(request.options)
16
- options = options(request.options)
17
- key = key(request, options[:key])
18
- response_data = cache_for(options).fetch(key)
19
- return unless response_data
20
- logger&.info "Served from cache: #{key}"
44
+ return if response_data.blank?
21
45
  from_cache(request, response_data)
22
46
  end
23
47
 
24
48
  def after_response
25
49
  return unless response.success?
26
- request = response.request
27
50
  return unless cache?(request)
28
- options = options(request.options)
29
- cache_for(options).write(
51
+ return if response_data.present?
52
+ multilevel_cache.write(
30
53
  key(request, options[:key]),
31
54
  to_cache(response),
32
- cache_options(options)
55
+ cache_options
33
56
  )
34
57
  end
35
58
 
36
59
  private
37
60
 
38
- # return the cache for the given options
39
- def cache_for(options)
61
+ # from cache
62
+ def response_data
63
+ # stop calling multi-level cache if it already returned nil for this interceptor instance
64
+ return @response_data if defined? @response_data
65
+ @response_data ||= multilevel_cache.fetch(key(request, options[:key]))
66
+ end
67
+
68
+ # performs read/write (fetch/write) on all configured cache levels (e.g. local & central)
69
+ def multilevel_cache
70
+ MultilevelCache.new(
71
+ central: central_cache,
72
+ local: local_cache
73
+ )
74
+ end
75
+
76
+ # returns the local cache either configured for entire LHC
77
+ # or configured locally for that particular request
78
+ def local_cache
40
79
  options.fetch(:use, cache)
41
80
  end
42
81
 
82
+ def central_cache
83
+ return nil if central.blank? || (central[:read].blank? && central[:write].blank?)
84
+ {}.tap do |options|
85
+ options[:read] = ActiveSupport::Cache::RedisCacheStore.new(url: central[:read]) if central[:read].present?
86
+ options[:write] = ActiveSupport::Cache::RedisCacheStore.new(url: central[:write]) if central[:write].present?
87
+ end
88
+ end
89
+
43
90
  # do we even need to bother with this interceptor?
44
91
  # based on the options, this method will
45
92
  # return false if this interceptor cannot work
46
93
  def cache?(request)
47
94
  return false unless request.options[:cache]
48
- options = options(request.options)
49
- cache_for(options) &&
95
+ (local_cache || central_cache) &&
50
96
  cached_method?(request.method, options[:methods])
51
97
  end
52
98
 
53
- # returns the request_options
54
- # will map deprecated options to the new format
55
- def options(request_options)
56
- options = (request_options[:cache] == true) ? {} : request_options[:cache].dup
57
- map_deprecated_options!(request_options, options)
99
+ def options
100
+ options = (request.options[:cache] == true) ? {} : request.options[:cache].dup
58
101
  options
59
102
  end
60
103
 
61
- # maps `cache_key` -> `key`, `cache_expires_in` -> `expires_in` and so on
62
- def map_deprecated_options!(request_options, options)
63
- deprecated_keys(request_options).each do |deprecated_key|
64
- new_key = deprecated_key.to_s.gsub(/^cache_/, '').to_sym
65
- options[new_key] = request_options[deprecated_key]
66
- end
67
- end
68
-
69
104
  # converts json we read from the cache to an LHC::Response object
70
105
  def from_cache(request, data)
71
106
  raw = Typhoeus::Response.new(data)
@@ -104,24 +139,10 @@ class LHC::Caching < LHC::Interceptor
104
139
 
105
140
  # extracts the options that should be forwarded to
106
141
  # the cache
107
- def cache_options(input = {})
108
- input.each_with_object({}) do |(key, value), result|
142
+ def cache_options
143
+ options.each_with_object({}) do |(key, value), result|
109
144
  result[key] = value if key.in? FORWARDED_OPTIONS
110
145
  result
111
146
  end
112
147
  end
113
-
114
- # grabs the deprecated keys from the request options
115
- def deprecated_keys(request_options)
116
- request_options.keys.select { |k| k =~ /^cache_.*/ }.sort
117
- end
118
-
119
- # emits a deprecation warning if necessary
120
- def deprecation_warning(request_options)
121
- unless deprecated_keys(request_options).empty?
122
- ActiveSupport::Deprecation.warn(
123
- "Cache options have changed! #{deprecated_keys(request_options).join(', ')} are deprecated and will be removed in future versions."
124
- )
125
- end
126
- end
127
148
  end