lhc 12.2.0 → 13.2.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.
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