arturo 2.5.0 → 2.5.1

Sign up to get free protection for your applications and to get access to all the features.
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