determinator 0.8.0 → 0.9.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
  SHA1:
3
- metadata.gz: b61e11a6498a8925acd9afe2f044ee454a0b155d
4
- data.tar.gz: 033893ae1248afd025503413daebc2a0dd953a66
3
+ metadata.gz: 0a7faec5d84de6789a94db7db9ff1de4fe22f947
4
+ data.tar.gz: 7c6f139261beea20a8f5c64f5071582a2314e4f1
5
5
  SHA512:
6
- metadata.gz: 3f1abc324ad0a963d67d82fabdc35a49ea8149bd6a8718e2ece520a5b07ae7d101a4234455ae82e1ede5c76f1db8b5d96760440330c6be3dbe41836a5253759e
7
- data.tar.gz: 500c7ec60e1dd4e08764dc3d0d30dc60cd0be635e4b42b8188b5363a0f37690a3984a6a0c109ee3ada8976931c5551678f48f4e2dabc52d82c97dfac65d2f56d
6
+ metadata.gz: cda841470760af3175a63d5d6e4a464bce9b71f6c3df6953e140fbae31dd8b1c6cdeae89504e71ac66dab8298b364c0eab641cca6cc98e39ed915e626ac7dd06
7
+ data.tar.gz: e3a5d769418dc6be72b97df333f8feafbb662fcb78331381863c6cb21193ad5d4b67d68c1b2bf0f513b660347f0676ec02f122f674cd821d4b44c29886eb6a04
data/CHANGELOG.md ADDED
@@ -0,0 +1,16 @@
1
+ # v0.9.1 (2017-09-14)
2
+
3
+ Bug fix:
4
+ - Fixes an issue where PR #27 missed one instance of syntactic sugar where `constraints` needed to be switched to `properties`. (#30)
5
+
6
+ # v0.9.0 (2017-09-14)
7
+
8
+ This version of Determiantor introduces some breaking changes as we move to getting the Florence ecosystem more ready for a wider audience. A 1.0.0 release will follow shortly with few additional features, but with significantly more documentation.
9
+
10
+ Breaking changes:
11
+ - When asking for a determination, you must specify the `properties` of the given actor, not the `constraints` (as `constraints` makes less sense) (#27)
12
+ - Remove siphon drain step, determinator now requires just a drain that expires caches. Also Removes the caching step for ID lookup (#24)
13
+
14
+ Features:
15
+ - Added Determinator RSpec helper (#26)
16
+ - Allows Determinator to accept both legacy and new style contraint and override specifications, for upcoming Florence API migration (#25)
data/README.md CHANGED
@@ -25,7 +25,7 @@ when 'velociraptors'
25
25
  end
26
26
  ```
27
27
 
28
- Feature flags and Experiments can be configured to have string based constraints. When the strings required for the experiment do not match, the user will _never_ see the flag or the experiment, when they match, then the rollout specified by the feature will be applied.
28
+ Feature flags and Experiments can be configured to have string based constraints. When the experiment's _constraints_ do not match the given actor's _properties_, the flag or experiment will always be off. When they match the rollout specified by the feature will be applied.
29
29
 
30
30
  Constraints must be strings, what matches and doesn't is configurable after-the-fact within Florence.
31
31
 
@@ -33,7 +33,7 @@ Constraints must be strings, what matches and doesn't is configurable after-the-
33
33
  # Constraints
34
34
  variant = determinator.which_variant(
35
35
  :my_experiment_name,
36
- constraints: {
36
+ properties: {
37
37
  country_of_first_order: current_user.orders.first.country.tld,
38
38
  }
39
39
  )
@@ -41,13 +41,34 @@ variant = determinator.which_variant(
41
41
 
42
42
  ## Installation
43
43
 
44
+ Determinator requires your application to be subscribed to the a `features` topic via Routemaster.
45
+
46
+ The drain must expire the routemaster cache on receipt of events, `Routemaster::Drain::CacheBusting.new` or better.
47
+
44
48
  Check the example Rails app in `examples` for more information on how to make use of this gem.
45
49
 
50
+ ```
51
+ # config/initializers/determinator.rb
52
+
53
+ require 'determinator/retrieve/routemaster'
54
+ Determinator.configure(
55
+ retrieval: Determinator::Retrieve::Routemaster.new(
56
+ discovery_url: 'https://florence.dev/'
57
+ retrieval_cache: ActiveSupport::Cache::MemoryStore.new(expires_in: 1.minute)
58
+ )
59
+ )
60
+ ```
61
+
62
+ ### Retrieval Cache
63
+
64
+ Determinator will function fully without a retrieval_cache set, although Determinator will produce 1 Redis query for every determination. By setting a `retrieval_cache` as an instance of `ActiveSupport::Cache::MemoryStore` (or equivalent) this can be reduced per application instance. This cache is not expired so *must* have a `expires_in` set, ideally to a short amount of time.
65
+
46
66
  ## Contributing
47
67
 
48
68
  Bug reports and pull requests are welcome on GitHub at https://github.com/deliveroo/determinator. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
49
69
 
70
+ Any PR should include a new section at the top of the `CHANGELOG.md` (if it doesn't exist) called 'Unreleased' of a similar format to the lines below. Upon release, this will be used to detail what has been added.
71
+
50
72
  ## License
51
73
 
52
74
  The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
53
-
@@ -1,2 +1 @@
1
1
  web: bundle exec rails s
2
- worker: bundle exec sidekiq
@@ -8,13 +8,15 @@ This example Rails app has been configured so that Determinator is correctly con
8
8
 
9
9
  This file sets up the singleton Determinator instance for the application.
10
10
 
11
- ### `config/routes.rb`
11
+ ### `config/initializers/routemaster.rb`
12
+
13
+ Uses Routemaster::Client to subscribe to the `features` topic, with a calback url matching what's set up in the routes file
12
14
 
13
- Using Determinator with Routemaster means that you must expose an endpoint to be informed of changes to Features. Determinator makes it easy to set this up with the `#configure_rails_router` helper method.
15
+ ### `config/routes.rb`
14
16
 
15
- ### `Procfile`
17
+ Sets up a simple routemaster drain endpoint, using the `CacheBusting`. Other Drains are available with additional features
16
18
 
17
- Bear in mind that, because routemaster depends on background workers to populate the cache, Sidekiq (or Resque) must be running alongside the app.
19
+ https://github.com/deliveroo/routemaster-drain
18
20
 
19
21
  ### `app/controllers/index_controller.rb`
20
22
 
@@ -29,7 +31,3 @@ The `determinator` method memoizes the instance of the `ActorControl` helper cla
29
31
  ### `config/application.rb`
30
32
 
31
33
  Ensure you've required the job runner backend appropriate for your set up. Routemaster Drain currently supports Sidekiq and Resque.
32
-
33
- ### `config/sidekiq.yml`
34
-
35
- This example uses Sidekiq as the background processor, ensure you've set it up correctly for notifications to cache in the background.
@@ -0,0 +1,9 @@
1
+ #Sets up the routemaster client and subscribes to the 'features' topic
2
+ Routemaster::Client.subscribe(
3
+ topics: %w[
4
+ features
5
+ ],
6
+ callback: ENV.fetch('ROUTEMASTER_CALLBACK_URL'),
7
+ uuid: ENV.fetch('ROUTEMASTER_DRAIN_TOKENS'),
8
+ max: ENV.fetch('ROUTEMASTER_DRAIN_BATCH_SIZE', 1).to_i
9
+ )
@@ -1,6 +1,5 @@
1
+ require 'routemaster/drain/cache_busting'
1
2
  Rails.application.routes.draw do
2
3
  root to: 'index#show'
3
-
4
- # DETERMINATOR: Make sure the routemaster routes are mapped to the containing app.
5
- Determinator.instance.retrieval.configure_rails_router(self)
4
+ mount Routemaster::Drain::CacheBusting.new, at: ENV.fetch('ROUTEMASTER_CALLBACK_URL')
6
5
  end
@@ -3,30 +3,33 @@ module Determinator
3
3
  # Useful for contexts where the actor remains constant (eg. inside
4
4
  # the request cycle in a webapp)
5
5
  class ActorControl
6
- attr_reader :id, :guid, :default_constraints
6
+ attr_reader :id, :guid, :default_properties
7
7
 
8
- def initialize(controller, id: nil, guid: nil, default_constraints: {})
8
+ # @see Determinator::Control#for_actor
9
+ def initialize(controller, id: nil, guid: nil, default_properties: {})
9
10
  @id = id
10
11
  @guid = guid
11
- @default_constraints = default_constraints
12
+ @default_properties = default_properties
12
13
  @controller = controller
13
14
  end
14
15
 
15
- def which_variant(name, constraints: {})
16
+ # @see Determinator::Control#which_variant
17
+ def which_variant(name, properties: {})
16
18
  controller.which_variant(
17
19
  name,
18
20
  id: id,
19
21
  guid: guid,
20
- constraints: default_constraints.merge(constraints)
22
+ properties: default_properties.merge(properties)
21
23
  )
22
24
  end
23
25
 
24
- def feature_flag_on?(name, constraints: {})
26
+ # @see Determinator::Control#feature_flag_on?
27
+ def feature_flag_on?(name, properties: {})
25
28
  controller.feature_flag_on?(
26
29
  name,
27
30
  id: id,
28
31
  guid: guid,
29
- constraints: default_constraints.merge(constraints)
32
+ properties: default_properties.merge(properties)
30
33
  )
31
34
  end
32
35
 
@@ -9,25 +9,40 @@ module Determinator
9
9
  @retrieval = retrieval
10
10
  end
11
11
 
12
+ # Creates a new determinator instance which assumes the actor id, guid and properties given
13
+ # are always specified. This is useful for within a before filter in a webserver, for example,
14
+ # so that the determinator instance made available has the logged-in user's credentials prefilled.
15
+ #
16
+ # @param :id [#to_s] The ID of the actor being specified
17
+ # @param :guid [#to_s] The Anonymous ID of the actor being specified
18
+ # @param :default_properties [Hash<Symbol,String>] The default properties for the determinator being created
12
19
  # @return [ActorControl] A helper object removing the need to know id and guid everywhere
13
- def for_actor(id: nil, guid: nil, default_constraints: {})
14
- ActorControl.new(self, id: id, guid: guid, default_constraints: default_constraints)
20
+ def for_actor(id: nil, guid: nil, default_properties: {})
21
+ ActorControl.new(self, id: id, guid: guid, default_properties: default_properties)
15
22
  end
16
23
 
17
24
  # Determines whether a specific feature is on or off for the given actor
18
25
  #
26
+ # @param name [#to_s] The name of the feature flag being checked
27
+ # @param :id [#to_s] The id of the actor being determinated for
28
+ # @param :guid [#to_s] The Anonymous id of the actor being determinated for
29
+ # @param :properties [Hash<Symbol,String>] The properties of this actor which will be used for including this actor or not
19
30
  # @return [true,false] Whether the feature is on (true) or off (false) for this actor
20
- def feature_flag_on?(name, id: nil, guid: nil, constraints: {})
21
- determinate(name, id: id, guid: guid, constraints: constraints) do |feature|
31
+ def feature_flag_on?(name, id: nil, guid: nil, properties: {})
32
+ determinate(name, id: id, guid: guid, properties: properties) do |feature|
22
33
  feature.feature_flag?
23
34
  end
24
35
  end
25
36
 
26
37
  # Determines what an actor should see for a specific experiment
27
38
  #
39
+ # @param name [#to_s] The name of the experiment being checked
40
+ # @param :id [#to_s] The id of the actor being determinated for
41
+ # @param :guid [#to_s] The Anonymous id of the actor being determinated for
42
+ # @param :properties [Hash<Symbol,String>] The properties of this actor which will be used for including this actor or not
28
43
  # @return [false,String] Returns false, if the actor is not in this experiment, or otherwise the variant name.
29
- def which_variant(name, id: nil, guid: nil, constraints: {})
30
- determinate(name, id: id, guid: guid, constraints: constraints) do |feature|
44
+ def which_variant(name, id: nil, guid: nil, properties: {})
45
+ determinate(name, id: id, guid: guid, properties: properties) do |feature|
31
46
  feature.experiment?
32
47
  end
33
48
  end
@@ -40,7 +55,7 @@ module Determinator
40
55
 
41
56
  Indicators = Struct.new(:rollout, :variant)
42
57
 
43
- def determinate(name, id:, guid:, constraints:)
58
+ def determinate(name, id:, guid:, properties:)
44
59
  feature = retrieval.retrieve(name)
45
60
  return false unless feature
46
61
 
@@ -52,7 +67,7 @@ module Determinator
52
67
 
53
68
  return false unless feature.active?
54
69
 
55
- target_group = choose_target_group(feature, constraints)
70
+ target_group = choose_target_group(feature, properties)
56
71
  # Given constraints have excluded this actor from this experiment
57
72
  return false unless target_group
58
73
 
@@ -70,10 +85,10 @@ module Determinator
70
85
  variant_for(feature, indicators.variant)
71
86
  end
72
87
 
73
- def choose_target_group(feature, constraints)
88
+ def choose_target_group(feature, properties)
74
89
  feature.target_groups.select { |tg|
75
90
  tg.constraints.reduce(true) do |fit, (scope, *required)|
76
- present = [*constraints[scope]]
91
+ present = [*properties[scope]]
77
92
  fit && (required.flatten & present.flatten).any?
78
93
  end
79
94
  # Must choose target group deterministically, if more than one match
@@ -1,7 +1,6 @@
1
1
  require 'uri'
2
2
  require 'routemaster/drain/caching'
3
3
  require 'routemaster/responses/hateoas_response'
4
- require 'determinator/retrieve/routemaster_feature_id_cache_warmer'
5
4
  require 'determinator/retrieve/null_cache'
6
5
 
7
6
  module Determinator
@@ -11,12 +10,8 @@ module Determinator
11
10
  # To use this correctly you will need the following environment variables set to appropriate values
12
11
  # for your instance of Routemaster:
13
12
  #
14
- # ROUTEMASTER_DRAIN_TOKENS
15
- # ROUTEMASTER_DRAIN_REDIS
16
13
  # ROUTEMASTER_CACHE_REDIS
17
14
  # ROUTEMASTER_CACHE_AUTH
18
- # ROUTEMASTER_QUEUE_NAME
19
- # ROUTEMASTER_CALLBACK_URL
20
15
  class Routemaster
21
16
  attr_reader :routemaster_app
22
17
 
@@ -29,16 +24,11 @@ module Determinator
29
24
  )
30
25
  @retrieval_cache = retrieval_cache
31
26
  @actor_service = client.discover(discovery_url)
32
- @routemaster_app = ::Routemaster::Drain::Caching.new(
33
- siphon_events: { 'features' => RoutemasterFeatureIdCacheWarmer }
34
- )
27
+
35
28
  end
36
29
 
37
- def retrieve(feature_name)
38
- cached_feature_lookup(feature_name) do
39
- key = self.class.index_cache_key(feature_name)
40
- feature_id = ::Routemaster::Config.cache_redis.get(key)
41
- return unless feature_id
30
+ def retrieve(feature_id)
31
+ cached_feature_lookup(feature_id) do
42
32
  @actor_service.feature.show(feature_id)
43
33
  end
44
34
  rescue ::Routemaster::Errors::ResourceNotFound
@@ -52,6 +42,10 @@ module Determinator
52
42
  route_mapper.mount routemaster_app, at: CALLBACK_PATH
53
43
  end
54
44
 
45
+ def routemaster_app
46
+ @routemaster_app ||= ::Routemaster::Drain::Caching.new
47
+ end
48
+
55
49
  def self.index_cache_key(feature_name)
56
50
  "determinator_index:#{feature_name}"
57
51
  end
@@ -74,19 +68,49 @@ module Determinator
74
68
  identifier: obj.body.identifier,
75
69
  bucket_type: obj.body.bucket_type,
76
70
  active: obj.body.active,
77
- target_groups: obj.body.target_groups.map { |tg|
78
- TargetGroup.new(
79
- rollout: tg.rollout,
80
- constraints: tg.constraints.first.to_h
81
- )
82
- },
71
+ target_groups: target_groups_attribute(obj.body.target_groups),
83
72
  variants: obj.body.variants.to_h,
84
- overrides: obj.body.overrides.each_with_object({}) { |override, hash|
85
- hash[override.user_id] = override.variant
86
- },
73
+ overrides: overrides_attribute(obj.body.overrides),
87
74
  winning_variant: obj.body.winning_variant,
88
75
  )
89
76
  end
77
+
78
+ def target_groups_attribute(target_groups)
79
+ target_groups.map do |tg|
80
+ TargetGroup.new(
81
+ rollout: tg.rollout,
82
+ constraints: constraints_attribute(tg.constraints)
83
+ )
84
+ end
85
+ end
86
+
87
+ def constraints_attribute(constraints)
88
+ # TODO when FLO-3 closed: no need for legacy guard
89
+ return legacy_constraints_attribute(constraints) unless constraints.respond_to?(:each_pair)
90
+ constraints
91
+ end
92
+
93
+ def legacy_constraints_attribute(constraints)
94
+ constraints.each_with_object({}) do |constraint, h|
95
+ if h[constraint.scope]
96
+ h[constraint.scope] = [*h[constraint.scope], constraint.identifier]
97
+ else
98
+ h[constraint.scope] = constraint.identifier
99
+ end
100
+ end
101
+ end
102
+
103
+ def overrides_attribute(overrides)
104
+ # TODO when FLO-3 closed: no need for legacy guard
105
+ return legacy_overrides_attribute(overrides) unless overrides.respond_to?(:each_pair)
106
+ overrides
107
+ end
108
+
109
+ def legacy_overrides_attribute(overrides)
110
+ overrides.each_with_object({}) do |override, h|
111
+ h[override[:user_id]] = override[:variant]
112
+ end
113
+ end
90
114
  end
91
115
  end
92
116
  end
@@ -1,3 +1,3 @@
1
1
  module Determinator
2
- VERSION = "0.8.0"
2
+ VERSION = '0.9.1'
3
3
  end
@@ -1,3 +1,5 @@
1
+ require 'determinator'
2
+
1
3
  module RSpec
2
4
  module Determinator
3
5
  def self.included(by)
@@ -5,12 +7,12 @@ module RSpec
5
7
 
6
8
  by.let(:fake_determinator) { FakeControl.new }
7
9
  by.before do
8
- allow(Determinator::Control).to receive(:new).and_return(fake_determinator)
10
+ allow(::Determinator::Control).to receive(:new).and_return(fake_determinator)
9
11
  end
10
12
  end
11
13
 
12
14
  module DSL
13
- def forced_determination(name, result, only_for: nil)
15
+ def forced_determination(name, result, only_for: {})
14
16
  before do
15
17
  fake_determinator.mock_result(
16
18
  name,
@@ -23,22 +25,33 @@ module RSpec
23
25
 
24
26
  class FakeControl
25
27
  def initialize
26
- @mocked_results = {}
28
+ @mocked_results = Hash.new { |h, k| h[k] = {} }
27
29
  end
28
30
 
29
- def mock_result(name, result, only_for: nil)
30
- @mocked_results[name.to_s][only_for || {}] = result
31
+ def mock_result(name, result, only_for: {})
32
+ @mocked_results[name.to_s][only_for] = result
31
33
  end
32
34
 
33
- def fake_determinate(name, id: nil, guid: nil, constraints: {})
34
- constraints[:id] = id if id
35
- constraints[:guid] = guid if guid
35
+ def fake_determinate(name, id: nil, guid: nil, properties: {})
36
+ properties[:id] = id if id
37
+ properties[:guid] = guid if guid
36
38
 
37
- return false unless @mocked_results[name.to_s].has_key?(constraints)
38
- @mocked_results[name.to_s][constraints]
39
+ outcome_for_feature_given_properties(name.to_s, properties)
39
40
  end
40
41
  alias_method :feature_flag_on?, :fake_determinate
41
42
  alias_method :which_variant, :fake_determinate
43
+
44
+ private
45
+
46
+ def outcome_for_feature_given_properties(feature_name, requirements)
47
+ req_array = requirements.to_a
48
+
49
+ _, forced = @mocked_results[feature_name].find do |given, outcome|
50
+ (given.to_a - req_array).empty?
51
+ end
52
+
53
+ forced || false
54
+ end
42
55
  end
43
56
  end
44
57
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: determinator
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.0
4
+ version: 0.9.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - JP Hastings-Spital
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2017-09-04 00:00:00.000000000 Z
11
+ date: 2017-09-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: routemaster-drain
@@ -132,6 +132,7 @@ files:
132
132
  - ".gitignore"
133
133
  - ".rspec"
134
134
  - ".travis.yml"
135
+ - CHANGELOG.md
135
136
  - CODE_OF_CONDUCT.md
136
137
  - Gemfile
137
138
  - Guardfile
@@ -168,9 +169,9 @@ files:
168
169
  - examples/determinator-rails/config/initializers/new_framework_defaults.rb
169
170
  - examples/determinator-rails/config/initializers/wrap_parameters.rb
170
171
  - examples/determinator-rails/config/puma.rb
172
+ - examples/determinator-rails/config/routemaster.rb
171
173
  - examples/determinator-rails/config/routes.rb
172
174
  - examples/determinator-rails/config/secrets.yml
173
- - examples/determinator-rails/config/sidekiq.yml
174
175
  - examples/determinator-rails/public/favicon.ico
175
176
  - examples/determinator-rails/public/robots.txt
176
177
  - lib/determinator.rb
@@ -180,7 +181,6 @@ files:
180
181
  - lib/determinator/retrieve/null_cache.rb
181
182
  - lib/determinator/retrieve/null_retriever.rb
182
183
  - lib/determinator/retrieve/routemaster.rb
183
- - lib/determinator/retrieve/routemaster_feature_id_cache_warmer.rb
184
184
  - lib/determinator/target_group.rb
185
185
  - lib/determinator/version.rb
186
186
  - lib/rspec/determinator.rb
@@ -1,2 +0,0 @@
1
- :queues:
2
- - routemaster
@@ -1,29 +0,0 @@
1
- module Determinator
2
- module Retrieve
3
- class RoutemasterFeatureIdCacheWarmer
4
- def initialize(payload)
5
- @payload = payload
6
- end
7
-
8
- def call
9
- response = client.get(@payload['url']).body
10
- if valid_feature_response?(response)
11
- key = Routemaster.index_cache_key(response['name'])
12
- ::Routemaster::Config.cache_redis.set(key, response['id'])
13
- end
14
- end
15
-
16
- private
17
-
18
- def client
19
- @client ||= ::Routemaster::APIClient.new(
20
- response_class: ::Routemaster::Responses::HateoasResponse
21
- )
22
- end
23
-
24
- def valid_feature_response?(response)
25
- response['id'] && response['name'] && response['bucket_type']
26
- end
27
- end
28
- end
29
- end