arturo 2.5.0 → 2.5.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a2f5b242fbe7537f790c6e07e36c7cfe6bc993b7ca1862c4abae72731d7139ae
4
- data.tar.gz: 4e7fbb2a7eb1e21acd51a270474f76a45ba03291051929111e6326ae6f7702c5
3
+ metadata.gz: bbc3960e60444e79f01132e5ce7aa3a52ec0a95e45218bf69a7d215b5e4b1af8
4
+ data.tar.gz: 1b999b93f95b7f4a388b50fad8f029727fccf7cf1a599b24ca1b15c2b293f000
5
5
  SHA512:
6
- metadata.gz: 21f44a384cb1fc1a27965425322ed1f5013ffb522ac8ac413eea49ea2249921b31a8ad3bd4221b39b3fae5cf5e77151e497cdb84b10f753a72fd256ff87ff79f
7
- data.tar.gz: 57101d806ab3645e49efb7c575a491081999343b452c74fbb956b9fee0e93e9015259821e2ffe71f2f230a5e881423cdce98c7bd2750825a366ae33b46e15074
6
+ metadata.gz: c6d4d6fc203bc4b40a01170a19b0a155b929f137af63c0fef84cf98f0a543ac1a4cea34c23d042c3872611621910ad0a6481570f78e0f012abeba041c815abaa
7
+ data.tar.gz: d73e30ce3fb43583ca4d17aba91a24d9fb796117e8c1b3cad57d8afdf284a4904ec0b95b4a05b371c7e9ef1a201d54ad6fa5e4425da6b406ea5ce49d6680e828
data/README.md CHANGED
@@ -102,6 +102,7 @@ rake db:migrate
102
102
  Open up the newly-generated `config/initializers/arturo_initializer.rb`.
103
103
  There are configuration options for the following:
104
104
 
105
+ * logging capabilities (see [logging](#logging))
105
106
  * the method that determines whether a user has permission to manage features
106
107
  (see [admin permissions](#adminpermissions))
107
108
  * the method that returns the object that has features
@@ -126,6 +127,15 @@ work with you on support for your favorite framework.
126
127
 
127
128
  ## Deep-Dive
128
129
 
130
+ ### <span id='logging'>Logging</span>
131
+
132
+ You can provide a logger in order to inspect Arturo usage.
133
+ A potential implementation for Rails would be:
134
+
135
+ ```Ruby
136
+ Arturo.logger = Rails.logger
137
+ ```
138
+
129
139
  ### <span id='adminpermissions'>Admin Permissions</span>
130
140
 
131
141
  `Arturo::FeatureManagement#may_manage_features?` is a method that is run in
@@ -314,6 +324,11 @@ Arturo::Feature.warm_cache!
314
324
 
315
325
  This will pre-fetch all `Feature`s and put them in the cache.
316
326
 
327
+ To use the current cache state when you can't fetch updates from origin:
328
+
329
+ ```Ruby
330
+ Arturo::Feature.extend_cache_on_failure = true
331
+ ```
317
332
 
318
333
  The following is the **intended** support for integration with view caching:
319
334
 
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
  module Arturo
3
-
3
+ require 'arturo/null_logger'
4
4
  require 'arturo/special_handling'
5
5
  require 'arturo/feature_availability'
6
6
  require 'arturo/feature_management'
@@ -16,5 +16,13 @@ module Arturo
16
16
  f = self::Feature.to_feature(feature_name)
17
17
  f && f.enabled_for?(recipient)
18
18
  end
19
+
20
+ def logger=(logger)
21
+ @logger = logger
22
+ end
23
+
24
+ def logger
25
+ @logger || NullLogger.new
26
+ end
19
27
  end
20
28
  end
@@ -15,7 +15,7 @@ module Arturo
15
15
  attr_readonly :symbol
16
16
 
17
17
  validates_presence_of :symbol, :deployment_percentage
18
- validates_uniqueness_of :symbol, :allow_blank => true
18
+ validates_uniqueness_of :symbol, :allow_blank => true, :case_sensitive => false
19
19
  validates_numericality_of :deployment_percentage,
20
20
  :only_integer => true,
21
21
  :allow_blank => true,
@@ -37,15 +37,21 @@ module Arturo
37
37
  class << base
38
38
  prepend PrependMethods
39
39
  attr_accessor :cache_ttl, :feature_cache, :feature_caching_strategy
40
+ attr_writer :extend_cache_on_failure
40
41
  end
41
42
  base.send(:after_save) do |f|
42
43
  f.class.feature_caching_strategy.expire(f.class.feature_cache, f.symbol.to_sym) if f.class.caches_features?
43
44
  end
44
45
  base.cache_ttl = 0
46
+ base.extend_cache_on_failure = false
45
47
  base.feature_cache = Arturo::FeatureCaching::Cache.new
46
48
  base.feature_caching_strategy = AllStrategy
47
49
  end
48
50
 
51
+ def extend_cache_on_failure?
52
+ !!@extend_cache_on_failure
53
+ end
54
+
49
55
  def caches_features?
50
56
  self.cache_ttl.to_i > 0
51
57
  end
@@ -56,13 +62,20 @@ module Arturo
56
62
 
57
63
  class AllStrategy
58
64
  class << self
65
+ ##
66
+ # @param cache [Arturo::Cache] cache backend
67
+ # @param symbol [Symbol] arturo identifier
68
+ # @return [Arturo::Feature, Arturo::NoSuchFeature]
69
+ #
59
70
  def fetch(cache, symbol, &block)
60
- features = cache.read("arturo.all")
61
-
62
- unless cache_is_current?(cache, features)
63
- features = Hash[Arturo::Feature.all.map { |f| [f.symbol.to_sym, f] }]
64
- mark_as_current!(cache)
65
- cache.write("arturo.all", features, :expires_in => Arturo::Feature.cache_ttl * 10)
71
+ existing_features = cache.read("arturo.all")
72
+
73
+ features = if cache_is_current?(cache, existing_features)
74
+ existing_features
75
+ else
76
+ arturos_from_origin(fallback: existing_features).tap do |updated_features|
77
+ update_and_extend_cache!(cache, updated_features)
78
+ end
66
79
  end
67
80
 
68
81
  features[symbol] || Arturo::NoSuchFeature.new(symbol)
@@ -74,15 +87,85 @@ module Arturo
74
87
 
75
88
  private
76
89
 
90
+ ##
91
+ # @param fallback [Hash] features to use on database failure
92
+ # @return [Hash] updated features from origin or fallback
93
+ # @raise [ActiveRecord::ActiveRecordError] on database failure
94
+ # without cache extension option
95
+ #
96
+ def arturos_from_origin(fallback:)
97
+ Hash[Arturo::Feature.all.map { |f| [f.symbol.to_sym, f] }]
98
+ rescue ActiveRecord::ActiveRecordError
99
+ raise unless Arturo::Feature.extend_cache_on_failure?
100
+
101
+ if fallback.blank?
102
+ log_empty_cache
103
+ raise
104
+ else
105
+ log_stale_cache
106
+ fallback
107
+ end
108
+ end
109
+
110
+ ##
111
+ # @return [Boolean] whether the current cache has to be updated from origin
112
+ # @raise [ActiveRecord::ActiveRecordError] on database failure
113
+ # without cache extension option
114
+ #
77
115
  def cache_is_current?(cache, features)
78
116
  return unless features
79
117
  return true if cache.read("arturo.current")
80
- return false if features.values.map(&:updated_at).compact.max != Arturo::Feature.maximum(:updated_at)
118
+
119
+ begin
120
+ return false if origin_changed?(features)
121
+ rescue ActiveRecord::ActiveRecordError
122
+ raise unless Arturo::Feature.extend_cache_on_failure?
123
+
124
+ if features.blank?
125
+ log_empty_cache
126
+ raise
127
+ else
128
+ log_stale_cache
129
+ update_and_extend_cache!(cache, features)
130
+ end
131
+
132
+ return true
133
+ end
81
134
  mark_as_current!(cache)
82
135
  end
83
136
 
137
+ def formatted_log(namespace, msg)
138
+ "[Arturo][#{namespace}] #{msg}"
139
+ end
140
+
141
+ def log_empty_cache
142
+ Arturo.logger.error(formatted_log('extend_cache_on_failure', 'Fallback cache is empty'))
143
+ end
144
+
145
+ def log_stale_cache
146
+ Arturo.logger.warn(formatted_log('extend_cache_on_failure', 'Falling back to stale cache'))
147
+ end
148
+
149
+ ##
150
+ # @return [True]
151
+ #
84
152
  def mark_as_current!(cache)
85
- cache.write("arturo.current", true, :expires_in => Arturo::Feature.cache_ttl)
153
+ cache.write("arturo.current", true, expires_in: Arturo::Feature.cache_ttl)
154
+ end
155
+
156
+ ##
157
+ # The Arturo origin might return a big payload, so checking for the latest
158
+ # update is a cheaper operation.
159
+ #
160
+ # @return [Boolean] if origin has been updated since the last cache update.
161
+ #
162
+ def origin_changed?(features)
163
+ features.values.map(&:updated_at).compact.max != Arturo::Feature.maximum(:updated_at)
164
+ end
165
+
166
+ def update_and_extend_cache!(cache, features)
167
+ mark_as_current!(cache)
168
+ cache.write("arturo.all", features, expires_in: Arturo::Feature.cache_ttl * 10)
86
169
  end
87
170
  end
88
171
  end
@@ -92,7 +175,7 @@ module Arturo
92
175
  if feature = cache.read("arturo.#{symbol}")
93
176
  feature
94
177
  else
95
- cache.write("arturo.#{symbol}", yield || Arturo::NoSuchFeature.new(symbol), :expires_in => Arturo::Feature.cache_ttl)
178
+ cache.write("arturo.#{symbol}", yield || Arturo::NoSuchFeature.new(symbol), expires_in: Arturo::Feature.cache_ttl)
96
179
  end
97
180
  end
98
181
 
@@ -0,0 +1,8 @@
1
+ module Arturo
2
+ class NullLogger
3
+ %w[info debug error fatal warn].each do |level|
4
+ define_method level do |_message|
5
+ end
6
+ end
7
+ end
8
+ end
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module Arturo
3
- VERSION = '2.5.0'
3
+ VERSION = '2.5.1'
4
4
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: arturo
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.5.0
4
+ version: 2.5.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - James A. Rosen
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-03-18 00:00:00.000000000 Z
11
+ date: 2020-03-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -168,6 +168,7 @@ files:
168
168
  - lib/arturo/feature_params_support.rb
169
169
  - lib/arturo/middleware.rb
170
170
  - lib/arturo/no_such_feature.rb
171
+ - lib/arturo/null_logger.rb
171
172
  - lib/arturo/special_handling.rb
172
173
  - lib/arturo/test_support.rb
173
174
  - lib/arturo/version.rb