arturo 2.4.1 → 2.5.3

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
- SHA1:
3
- metadata.gz: 79d3affcbb0dd1612baa5eebd1073a4325ab9436
4
- data.tar.gz: 541f2ffa57e8eb6b3a7774789da148bb14bb0d2f
2
+ SHA256:
3
+ metadata.gz: a92dc0b0221d1584220352d9ca7586f50449dd23bb6b4f2bfe9cc26fa30c7d1f
4
+ data.tar.gz: d59e9a44466d5ede3338873993f61ae8b11d009ff32eb5e55b1122090b3ad368
5
5
  SHA512:
6
- metadata.gz: 76d162fc3dd78459ccddcd54902f5f68c09a2a354ea3468a3977b9b13fa213b579c2bad69cae936ea2a996ddf8ae0bca0f949a5760d8972fd47adc1613c62d94
7
- data.tar.gz: bbe4b79c132ac8d9ef6fbf563da43553e85b9666a65383c4deecb6340af3971bde19914151de222acd7229eeb1408e473ce154209e7388e7bceb921b7df1fc20
6
+ metadata.gz: fe24d781cd0137148a5edc2b3f5fb67fb9d29995758fb0cbbcb026c0fd4b40960bf542bf31aa09a555f827c053e8f49d1f5f08553d55e68050758f714d6f53f7
7
+ data.tar.gz: 6df3e48141801e5007a14f7531a7bfea975a09a95ae59dd7ad2bb8e756b66a14e4903a34f3855f35fbda01465ad972e701381ecbdf321071d5f38f8fd3a689d1
data/CHANGELOG.md CHANGED
@@ -1,3 +1,17 @@
1
+ ## Unreleased
2
+
3
+ ## v2.5.3
4
+
5
+ Bug fix: Allow using Arturo with ActiveRecord, but without all of Rails.
6
+
7
+ ## v2.5.2
8
+
9
+ Drop support for Rails 3.2.
10
+
11
+ Add support for Rails 6.1.
12
+
13
+ Switch CI from Travis to GitHub Actions.
14
+
1
15
  ## v2.2.0
2
16
 
3
17
  Bug fix: making a feature-update request that fails strict params checks now returns a sensible error instead of throwing an exception
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
 
@@ -331,6 +346,6 @@ on [Sliders](http://en.wikipedia.org/wiki/Sliders).
331
346
 
332
347
  ## Quality Metrics
333
348
 
334
- [![Build Status](https://travis-ci.org/zendesk/arturo.png?branch=master)](https://travis-ci.org/zendesk/arturo)
349
+ [![Build Status](https://github.com/zendesk/arturo/workflows/CI/badge.svg)](https://github.com/zendesk/arturo/actions?query=workflow%3ACI)
335
350
 
336
351
  [![Code Quality](https://codeclimate.com/github/zendesk/arturo.png)](https://codeclimate.com/github/zendesk/arturo)
@@ -37,7 +37,7 @@ module Arturo
37
37
  feature = Arturo::Feature.find_by_id(id)
38
38
  if feature.blank?
39
39
  errors << t('arturo.features.flash.no_such_feature', :id => id)
40
- elsif feature.update_attributes(attributes)
40
+ elsif feature.update(attributes)
41
41
  updated_count += 1
42
42
  else
43
43
  errors << t('arturo.features.flash.error_updating', :id => id, :errors => feature.errors.full_messages.to_sentence)
@@ -76,7 +76,7 @@ module Arturo
76
76
  end
77
77
 
78
78
  def update
79
- if @feature.update_attributes(feature_params)
79
+ if @feature.update(feature_params)
80
80
  flash[:notice] = t('arturo.features.flash.updated', :name => @feature.to_s)
81
81
  redirect_to arturo_engine.feature_path(@feature)
82
82
  else
data/lib/arturo.rb CHANGED
@@ -1,11 +1,11 @@
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'
7
7
  require 'arturo/feature_caching'
8
- require 'arturo/engine' if defined?(Rails::VERSION::MAJOR) && Rails::VERSION::MAJOR >= 3
8
+ require 'arturo/engine' if defined?(Rails)
9
9
 
10
10
  class << self
11
11
  # Quick check for whether a feature is enabled for a recipient.
@@ -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
@@ -11,11 +11,10 @@ module Arturo
11
11
  Arturo::Feature::SYMBOL_REGEX = /^[a-zA-z][a-zA-Z0-9_]*$/
12
12
  DEFAULT_ATTRIBUTES = { :deployment_percentage => 0 }.with_indifferent_access
13
13
 
14
- attr_accessible :symbol, :deployment_percentage if ActiveRecord::VERSION::MAJOR < 4
15
14
  attr_readonly :symbol
16
15
 
17
16
  validates_presence_of :symbol, :deployment_percentage
18
- validates_uniqueness_of :symbol, :allow_blank => true
17
+ validates_uniqueness_of :symbol, :allow_blank => true, :case_sensitive => false
19
18
  validates_numericality_of :deployment_percentage,
20
19
  :only_integer => true,
21
20
  :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
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
- FactoryGirl.define do
2
+ FactoryBot.define do
3
3
  sequence(:feature_symbol) { |n| "feature_#{n}".to_sym }
4
4
 
5
5
  factory :feature, :class => Arturo::Feature do
@@ -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
@@ -10,7 +10,7 @@ Arturo.instance_eval do
10
10
  def enable_feature!(name)
11
11
  if feature = Arturo::Feature.find_feature(name)
12
12
  feature = feature.class.find(feature.id) if feature.frozen?
13
- feature.update_attributes(:deployment_percentage => 100)
13
+ feature.update(:deployment_percentage => 100)
14
14
  else
15
15
  Arturo::Feature.create!(:symbol => name, :deployment_percentage => 100)
16
16
  end
@@ -25,7 +25,7 @@ Arturo.instance_eval do
25
25
  def disable_feature!(name)
26
26
  if feature = Arturo::Feature.find_feature(name)
27
27
  feature = feature.class.find(feature.id) if feature.frozen?
28
- feature.update_attributes(:deployment_percentage => 0)
28
+ feature.update(:deployment_percentage => 0)
29
29
  end
30
30
  end
31
31
 
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module Arturo
3
- VERSION = '2.4.1'
3
+ VERSION = '2.5.3'
4
4
  end
metadata CHANGED
@@ -1,55 +1,55 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: arturo
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.4.1
4
+ version: 2.5.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - James A. Rosen
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-09-13 00:00:00.000000000 Z
11
+ date: 2021-02-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - ">"
17
+ - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '3.2'
19
+ version: '4.2'
20
20
  - - "<"
21
21
  - !ruby/object:Gem::Version
22
- version: '5.2'
22
+ version: '6.2'
23
23
  type: :runtime
24
24
  prerelease: false
25
25
  version_requirements: !ruby/object:Gem::Requirement
26
26
  requirements:
27
- - - ">"
27
+ - - ">="
28
28
  - !ruby/object:Gem::Version
29
- version: '3.2'
29
+ version: '4.2'
30
30
  - - "<"
31
31
  - !ruby/object:Gem::Version
32
- version: '5.2'
32
+ version: '6.2'
33
33
  - !ruby/object:Gem::Dependency
34
34
  name: rails
35
35
  requirement: !ruby/object:Gem::Requirement
36
36
  requirements:
37
- - - ">"
37
+ - - ">="
38
38
  - !ruby/object:Gem::Version
39
- version: '3.2'
39
+ version: '4.2'
40
40
  - - "<"
41
41
  - !ruby/object:Gem::Version
42
- version: '5.2'
42
+ version: '6.2'
43
43
  type: :development
44
44
  prerelease: false
45
45
  version_requirements: !ruby/object:Gem::Requirement
46
46
  requirements:
47
- - - ">"
47
+ - - ">="
48
48
  - !ruby/object:Gem::Version
49
- version: '3.2'
49
+ version: '4.2'
50
50
  - - "<"
51
51
  - !ruby/object:Gem::Version
52
- version: '5.2'
52
+ version: '6.2'
53
53
  - !ruby/object:Gem::Dependency
54
54
  name: sqlite3
55
55
  requirement: !ruby/object:Gem::Requirement
@@ -79,7 +79,7 @@ dependencies:
79
79
  - !ruby/object:Gem::Version
80
80
  version: '0'
81
81
  - !ruby/object:Gem::Dependency
82
- name: factory_girl
82
+ name: factory_bot
83
83
  requirement: !ruby/object:Gem::Requirement
84
84
  requirements:
85
85
  - - ">="
@@ -106,20 +106,6 @@ dependencies:
106
106
  - - ">="
107
107
  - !ruby/object:Gem::Version
108
108
  version: '0'
109
- - !ruby/object:Gem::Dependency
110
- name: wwtd
111
- requirement: !ruby/object:Gem::Requirement
112
- requirements:
113
- - - ">="
114
- - !ruby/object:Gem::Version
115
- version: '0'
116
- type: :development
117
- prerelease: false
118
- version_requirements: !ruby/object:Gem::Requirement
119
- requirements:
120
- - - ">="
121
- - !ruby/object:Gem::Version
122
- version: '0'
123
109
  - !ruby/object:Gem::Dependency
124
110
  name: bump
125
111
  requirement: !ruby/object:Gem::Requirement
@@ -168,6 +154,7 @@ files:
168
154
  - lib/arturo/feature_params_support.rb
169
155
  - lib/arturo/middleware.rb
170
156
  - lib/arturo/no_such_feature.rb
157
+ - lib/arturo/null_logger.rb
171
158
  - lib/arturo/special_handling.rb
172
159
  - lib/arturo/test_support.rb
173
160
  - lib/arturo/version.rb
@@ -182,7 +169,7 @@ homepage: http://github.com/zendesk/arturo
182
169
  licenses:
183
170
  - APLv2
184
171
  metadata: {}
185
- post_install_message:
172
+ post_install_message:
186
173
  rdoc_options: []
187
174
  require_paths:
188
175
  - lib
@@ -197,9 +184,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
197
184
  - !ruby/object:Gem::Version
198
185
  version: '0'
199
186
  requirements: []
200
- rubyforge_project:
201
- rubygems_version: 2.6.13
202
- signing_key:
187
+ rubygems_version: 3.2.2
188
+ signing_key:
203
189
  specification_version: 4
204
190
  summary: Feature sliders, wrapped up in an engine
205
191
  test_files: []