determinator 0.12.1 → 1.0.0

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: 17f714ab0a57974a7934bdcf86631e65e4e5111d
4
- data.tar.gz: 82caf2b07f4a891cc2621fc1ef72aa8c569347b1
3
+ metadata.gz: 3b01c8573e10fbc219e4cd44c85696a8c34f4283
4
+ data.tar.gz: f7a43c6e60f22ef9947fbe04e5313db4b28982c8
5
5
  SHA512:
6
- metadata.gz: 37b01ffc666efd680e085365b61b4516cf3c5300e7b215e98e0dde2e1472ea494f7fc5748f811420f137452ee163ed86488f47360205631cc825e2c896780948
7
- data.tar.gz: d974d0c38bf2f3963dfa4661252e9bd41b5ef5d5d11ee3e91a2a85658254ca96d90ec5ae8c1d7b5e332065e7254bd0ff79db4cdb7a091a04a76b2e8094f7fd4a
6
+ metadata.gz: 66c567515540a0cc5adc1b9978b2a33b57d7f66135a0d6413847bccc68ee69d7a0954979333dda96bad701eba3879b1ac9e4c38785f18a4bf5c9ddc0f51ac668
7
+ data.tar.gz: 3329ee1b0ea3c1f59dc010675e2e2f89173dfc7c346b06ccb4248cb67e8c29b1ff13041a969f0400911a41fa93e0656b921ec5c9d9984e7d44f4634cec2c1a59
@@ -0,0 +1,3 @@
1
+ [submodule "spec/standard_cases"]
2
+ path = spec/standard_cases
3
+ url = https://github.com/deliveroo/determinator-standard-tests
@@ -1,3 +1,15 @@
1
+ # v1.0.0 (2018-02-05)
2
+
3
+ ⚠️ This release includes breaking changes ⚠️
4
+
5
+ Breaking change:
6
+ - Changes retrieval caching so that 404s are cached as well as positive feature retrievals. This means that an unconfigured feature won't result in a thundering herd of requests to Florence. (#46)
7
+
8
+ Feature:
9
+ - Changes tests to use the [Determinator standard tests](https://github.com/deliveroo/determinator-standard-tests) (#41)
10
+ - Supports the use of the 'single' bucket type for features, which allows (only) feature flags to be on or off, without any `id` or `guid` specified. (#41)
11
+ - Adds Determintion callbacks: trigger a block when a determination is made, allowing for experiment tracking. (#45)
12
+
1
13
  # v0.12.1 (2018-02-01)
2
14
 
3
15
  Bug Fix:
data/Guardfile CHANGED
@@ -3,4 +3,5 @@ guard :rspec, cmd: 'rspec' do
3
3
  watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
4
4
  watch(%r{^spec/factories/.+\.rb}) { "spec" }
5
5
  watch('spec/spec_helper.rb') { "spec" }
6
+ watch(%r{^spec/standard_cases/}) { "spec/determinator/control_spec.rb" }
6
7
  end
data/README.md CHANGED
@@ -81,19 +81,34 @@ Determinator requires a initialiser block somewhere in your application's boot p
81
81
  # config/initializers/determinator.rb
82
82
 
83
83
  require 'determinator/retrieve/routemaster'
84
+ require 'active_support/cache'
85
+
84
86
  Determinator.configure(
85
- retrieval: Determinator::Retrieve::Routemaster.new(
86
- discovery_url: 'https://flo.dev/'
87
- retrieval_cache: ActiveSupport::Cache::MemoryStore.new(expires_in: 1.minute)
88
- ),
89
- errors: -> error { NewRelic::Agent.notice_error(error) },
90
- missing_features: -> feature_name { STATSD.increment 'determinator.missing_feature', tags: ["feature:#{name}"] }
87
+ retrieval: Determinator::Retrieve::Routemaster.new(discovery_url: 'https://flo.dev/'),
88
+ feature_cache: Determinator::Cache::FetchWrapper.new(
89
+ ActiveSupport::Cache::MemoryStore.new(expires_in: 1.minute)
90
+ )
91
91
  )
92
+ Determinator.on_error(NewRelic::Agent.method(:notice_error))
93
+ Determinator.on_missing_feature do |feature_name|
94
+ STATSD.increment 'determinator.missing_feature', tags: ["feature:#{name}"]
95
+ end
96
+
97
+ Determinator.on_determination do |id, guid, feature, determination|
98
+ if feature.experiment? && determination != false
99
+ YourTrackingSolution.record_variant_viewing(
100
+ user_id: id,
101
+ experiment_name: feature.name,
102
+ variant: determination
103
+ )
104
+ end
105
+ end
92
106
  ```
93
107
 
94
108
  This configures the `Determinator.instance` with:
95
109
 
96
110
  - What **retrieval** mechanism should be used to get feature details
111
+ - (recommended) How features should be **cached** as they're retrieved. This mechanism allows caching features _and_ missing features, so when a cache is configured a determination request for a missing feature on busy machines won't result in a thundering herd.
97
112
  - (optional) How **errors** should be reported
98
113
  - (optional) How **missing features** should be monitored (as they indicate something's up with your code or your set up!)
99
114
 
@@ -175,9 +190,13 @@ end
175
190
 
176
191
  * Check out [the specs for `RSpec::Determinator`](spec/rspec/determinator_spec.rb) to find out what you can do!
177
192
 
178
- ### Retrieval Cache
193
+ ## Testing this library
179
194
 
180
- 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.
195
+ This library makes use of the [Determinator Standard Tests](https://github.com/deliveroo/determinator-standard-tests) to ensure that it conforms to the same specification as determinator libraries in other languages. The standard tests can be updated to the latest ones available by updating the submodule:
196
+
197
+ ```bash
198
+ git submodule foreach git pull origin master
199
+ ```
181
200
 
182
201
  ## Contributing
183
202
 
data/circle.yml CHANGED
@@ -0,0 +1,3 @@
1
+ checkout:
2
+ post:
3
+ - git submodule update --init
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ../..
3
3
  specs:
4
- determinator (0.11.1)
4
+ determinator (0.12.1)
5
5
  routemaster-drain (~> 3.0)
6
6
 
7
7
  GEM
@@ -3,10 +3,6 @@ class IndexController < ApplicationController
3
3
  is_colloquial = determinator.feature_flag_on?(:colloquial_welcome)
4
4
  emoji = determinator.which_variant(:welcome_emoji)
5
5
 
6
- if emoji
7
- # TODO: Track that this user saw a variant of this experiment
8
- end
9
-
10
6
  message = [
11
7
  is_colloquial ? "hi world" : "hello world",
12
8
  emoji
@@ -1,10 +1,22 @@
1
1
  require 'determinator/retrieve/routemaster'
2
+ require 'active_support/cache'
2
3
 
3
- Determinator.configure(
4
- retrieval: Determinator::Retrieve::Routemaster.new(
5
- discovery_url: 'https://florence.dev/'
6
- retrieval_cache: ActiveSupport::Cache::MemoryStore.new(expires_in: 1.minute)
7
- )
8
- # The following would allow tracking of errors in NewRelic
9
- # errors: -> error { NewRelic::Agent.notice_error(error) }
4
+ retrieval = Determinator::Retrieve::Routemaster.new(discovery_url: 'https://flo.dev/')
5
+ feature_cache = Determinator::Cache::FetchWrapper.new(
6
+ ActiveSupport::Cache::MemoryStore.new(expires_in: 1.minute)
10
7
  )
8
+ Determinator.configure(retrieval: retrieval, feature_cache: feature_cache)
9
+
10
+ Determinator.on_error do |error|
11
+ # NewRelic::Agent.notice_error(error)
12
+ end
13
+
14
+ Determinator.on_missing_feature do |feature_name|
15
+ # STATSD.increment 'determinator.missing_feature', tags: ["feature:#{name}"]
16
+ end
17
+
18
+ Determinator.on_determination do |id, guid, feature, determination|
19
+ if feature.experiment? && determination != false
20
+ puts "TODO: Track that user #{id}/#{guid} saw the #{determination} variant of '#{feature.name}' for analysis"
21
+ end
22
+ end
@@ -4,40 +4,91 @@ require 'determinator/feature'
4
4
  require 'determinator/target_group'
5
5
  require 'determinator/retrieve/routemaster'
6
6
  require 'determinator/retrieve/null_retriever'
7
+ require 'determinator/cache/fetch_wrapper'
7
8
 
8
9
  module Determinator
9
- # @param :retrieval [Determinator::Retrieve::Routemaster] A retrieval instance for Features
10
- # @param :errors [#call, nil] a proc, accepting an error, which will be called with any errors which occur while determinating
11
- # @param :missing_feature [#call, nil] a proc, accepting a feature name, which will be called any time a feature is requested but isn't available
12
- def self.configure(retrieval:, errors: nil, missing_feature: nil)
13
- @error_logger = errors if errors.respond_to?(:call)
14
- @missing_feature_logger = missing_feature if missing_feature.respond_to?(:call)
15
- @instance = Control.new(retrieval: retrieval)
16
- end
10
+ class << self
11
+ # @param :retrieval [Determinator::Retrieve::Routemaster] A retrieval instance for Features
12
+ # @param :errors [#call, nil] a proc, accepting an error, which will be called with any errors which occur while determinating
13
+ # @param :missing_feature [#call, nil] a proc, accepting a feature name, which will be called any time a feature is requested but isn't available
14
+ # @param :feature_cache [#call, nil] a caching proc, accepting a feature name, which will return the named feature or yield (and store) if not available
15
+ def configure(retrieval:, errors: nil, missing_feature: nil, feature_cache: nil)
16
+ self.on_error_logger(&errors) if errors
17
+ self.on_missing_feature(&missing_feature) if missing_feature
18
+ @feature_cache = feature_cache if feature_cache.respond_to?(:call)
19
+ @instance = Control.new(retrieval: retrieval)
20
+ end
17
21
 
18
- # @return [Determinator::Control] The currently active instance of determinator.
19
- # @raises [RuntimeError] If no Determinator instance is set up (with `.configure`)
20
- def self.instance
21
- raise "No singleton Determinator instance defined" unless @instance
22
- @instance
23
- end
22
+ # Returns the currently configured Determinator::Control instance
23
+ #
24
+ # @return [Determinator::Control] The currently active instance of determinator.
25
+ # @raises [RuntimeError] If no Determinator instance is set up (with `.configure`)
26
+ def instance
27
+ raise "No singleton Determinator instance defined" unless @instance
28
+ @instance
29
+ end
24
30
 
25
- # Returns the feature with the given name as Determinator uses it. This is useful for
26
- # debugging issues with the retrieval mechanism which delivers features to Determinator.
27
- # @returns [Determinator::Feature,nil] The feature details Determinator would use for a determination right now.
28
- def self.feature_details(name)
29
- instance.retrieval.retrieve(name)
30
- end
31
+ # Defines how errors that shouldn't break your application should be logged
32
+ def on_error(&block)
33
+ @error_logger = block
34
+ end
31
35
 
32
- def self.notice_error(error)
33
- return unless @error_logger
36
+ # Defines how to record the moment when a feature which doesn't exist is requested.
37
+ # If this happens a lot it indicates poor set up, so can be useful for tracking.
38
+ def on_missing_feature(&block)
39
+ @missing_feature_logger = block
40
+ end
34
41
 
35
- @error_logger.call(error)
36
- end
42
+ # Defines code that should execute when a determination is completed. This is particularly
43
+ # helpful for preparing or sending events to record that an actor has seen a particular experiment variant.
44
+ #
45
+ # Please note that this block will be executed _synchronously_ before delivering the determination to the callsite.
46
+ #
47
+ # @yield [id, guid, feature, determination] Will be called when a determination was requested for the
48
+ # specified `feature`, for the actor with `id` and `guid`, and received the determination `determination`.
49
+ # @yieldparam id [String, nil] The ID that was used to request the determination
50
+ # @yieldparam guid [String, nil] The GUID that was used to request the determination
51
+ # @yieldparam feature [Determinator::Feature] The feature that was requested
52
+ # @yieldparam determination [String,Boolean] The result of the determination
53
+ def on_determination(&block)
54
+ @determination_callback = block
55
+ end
56
+
57
+ # Returns the feature with the given name as Determinator uses it. This is useful for
58
+ # debugging issues with the retrieval mechanism which delivers features to Determinator.
59
+ # @returns [Determinator::Feature,nil] The feature details Determinator would use for a determination right now.
60
+ def feature_details(name)
61
+ with_retrieval_cache(name) { instance.retrieval.retrieve(name) }
62
+ end
63
+
64
+ # Allows Determinator to track that an error has happened with determination
65
+ # @api private
66
+ def notice_error(error)
67
+ return unless @error_logger
68
+
69
+ error = RuntimeError.new(error) unless error.is_a?(StandardError)
70
+ @error_logger.call(error)
71
+ end
72
+
73
+ # Allows Determinator to track that a feature was requested but was missing
74
+ # @api private
75
+ def notice_missing_feature(name)
76
+ return unless @missing_feature_logger
77
+
78
+ @missing_feature_logger.call(name)
79
+ end
80
+
81
+ def notice_determination(id, guid, feature, determination)
82
+ return unless @determination_callback
83
+ @determination_callback.call(id, guid, feature, determination)
84
+ end
37
85
 
38
- def self.missing_feature(name)
39
- return unless @missing_feature_logger
86
+ # Allows access to the chosen caching mechanism for any retrieval plugin.
87
+ # @api private
88
+ def with_retrieval_cache(name)
89
+ return yield unless @feature_cache.respond_to?(:call)
40
90
 
41
- @missing_feature_logger.call(name)
91
+ @feature_cache.call(name) { yield }
92
+ end
42
93
  end
43
94
  end
@@ -0,0 +1,20 @@
1
+ module Determinator
2
+ module Cache
3
+ class FetchWrapper
4
+ # @param cache [#fetch] An instance of a cache class which implements #fetch like ActiveSupport::Cache does
5
+ def initialize(cache)
6
+ @cache = cache
7
+ end
8
+
9
+ def call(feature_name)
10
+ @cache.fetch(key(feature_name)) { yield }
11
+ end
12
+
13
+ private
14
+
15
+ def key(feature_name)
16
+ "determinator:feature_cache:#{feature_name}"
17
+ end
18
+ end
19
+ end
20
+ end
@@ -27,9 +27,10 @@ module Determinator
27
27
  # @param :id [#to_s] The id of the actor being determinated for
28
28
  # @param :guid [#to_s] The Anonymous id of the actor being determinated for
29
29
  # @param :properties [Hash<Symbol,String>] The properties of this actor which will be used for including this actor or not
30
+ # @raise [ArgumentError] When the arguments given to this method aren't ever going to produce a useful response
30
31
  # @return [true,false] Whether the feature is on (true) or off (false) for this actor
31
32
  def feature_flag_on?(name, id: nil, guid: nil, properties: {})
32
- determinate(name, id: id, guid: guid, properties: properties) do |feature|
33
+ determinate_and_notice(name, id: id, guid: guid, properties: properties) do |feature|
33
34
  feature.feature_flag?
34
35
  end
35
36
  end
@@ -40,9 +41,10 @@ module Determinator
40
41
  # @param :id [#to_s] The id of the actor being determinated for
41
42
  # @param :guid [#to_s] The Anonymous id of the actor being determinated for
42
43
  # @param :properties [Hash<Symbol,String>] The properties of this actor which will be used for including this actor or not
44
+ # @raise [ArgumentError] When the arguments given to this method aren't ever going to produce a useful response
43
45
  # @return [false,String] Returns false, if the actor is not in this experiment, or otherwise the variant name.
44
46
  def which_variant(name, id: nil, guid: nil, properties: {})
45
- determinate(name, id: id, guid: guid, properties: properties) do |feature|
47
+ determinate_and_notice(name, id: id, guid: guid, properties: properties) do |feature|
46
48
  feature.experiment?
47
49
  end
48
50
  end
@@ -55,26 +57,33 @@ module Determinator
55
57
 
56
58
  Indicators = Struct.new(:rollout, :variant)
57
59
 
58
- def determinate(name, id:, guid:, properties:)
59
- feature = retrieval.retrieve(name)
60
+ def determinate_and_notice(name, id:, guid:, properties:)
61
+ feature = Determinator.with_retrieval_cache(name) { retrieval.retrieve(name) }
62
+
60
63
  if feature.nil?
61
- Determinator.missing_feature(name)
64
+ Determinator.notice_missing_feature(name)
62
65
  return false
63
66
  end
64
67
 
68
+ determinate(feature, id: id, guid: guid, properties: properties).tap do |determination|
69
+ Determinator.notice_determination(id, guid, feature, determination)
70
+ end
71
+ end
72
+
73
+ def determinate(feature, id:, guid:, properties:)
65
74
  # Calling method can place constraints on the feature, eg. experiment only
66
75
  return false if block_given? && !yield(feature)
67
76
 
68
- # Overrides take precedence
69
- return feature.override_value_for(id) if feature.overridden_for?(id)
70
-
77
+ # Inactive features are always, always off
71
78
  return false unless feature.active?
72
79
 
80
+ return feature.override_value_for(id) if feature.overridden_for?(id)
81
+
73
82
  target_group = choose_target_group(feature, properties)
74
83
  # Given constraints have excluded this actor from this experiment
75
84
  return false unless target_group
76
85
 
77
- indicators = indicators_for(feature, id, guid)
86
+ indicators = indicators_for(feature, actor_identifier(feature, id, guid))
78
87
  # This actor isn't described in enough detail to form indicators
79
88
  return false unless indicators
80
89
 
@@ -86,30 +95,53 @@ module Determinator
86
95
  return true unless feature.experiment?
87
96
 
88
97
  variant_for(feature, indicators.variant)
98
+
99
+ rescue ArgumentError
100
+ raise
101
+
102
+ rescue => e
103
+ Determinator.notice_error(e)
104
+ false
89
105
  end
90
106
 
91
107
  def choose_target_group(feature, properties)
92
- # Keys must be strings
93
- normalised_properties = properties.each_with_object({}) do |(k, v), h|
94
- h[k.to_s] = v
108
+ # Keys and values must be strings
109
+ normalised_properties = properties.each_with_object({}) do |(name, values), hash|
110
+ hash[name.to_s] = [*values].map(&:to_s)
95
111
  end
96
112
 
97
113
  feature.target_groups.select { |tg|
114
+ next false unless tg.rollout.between?(1, 65_536)
115
+
98
116
  tg.constraints.reduce(true) do |fit, (scope, *required)|
99
- present = [*normalised_properties[scope.to_s]]
117
+ present = [*normalised_properties[scope]]
100
118
  fit && (required.flatten & present.flatten).any?
101
119
  end
102
120
  # Must choose target group deterministically, if more than one match
103
121
  }.sort_by { |tg| tg.rollout }.last
104
122
  end
105
123
 
106
- def indicators_for(feature, id, guid)
107
- # If we're slicing by guid then we never pay attention to id
108
- actor_identifier = case feature.bucket_type
109
- when :id then id
110
- when :guid then guid
111
- when :fallback then id || guid
112
- end
124
+ def actor_identifier(feature, id, guid)
125
+ case feature.bucket_type
126
+ when :id
127
+ id
128
+ when :guid
129
+ return guid if guid.to_s != ''
130
+
131
+ raise ArgumentError, 'A GUID must always be given for GUID bucketed features'
132
+ when :fallback
133
+ identifier = (id || guid).to_s
134
+ return identifier if identifier != ''
135
+
136
+ raise ArgumentError, 'An ID or GUID must always be given for Fallback bucketed features'
137
+ when :single
138
+ 'all'
139
+ else
140
+ Determinator.notice_error "Cannot process the '#{feature.bucket_type}' bucket type found in #{feature.name}"
141
+ end
142
+ end
143
+
144
+ def indicators_for(feature, actor_identifier)
113
145
  # No identified means not enough info was given by the caller
114
146
  # to determine an outcome for this feature
115
147
  return unless actor_identifier
@@ -132,12 +164,12 @@ module Determinator
132
164
  variant_weight_total = feature.variants.values.reduce(:+)
133
165
  scale_factor = 65_535 / variant_weight_total.to_f
134
166
 
167
+ sorted_variants = feature.variants.keys.sort
135
168
  # Find the variant the indicator sits within
136
- previous_upper_bound = 0
137
- feature.variants.each do |name, weight|
138
- new_upper_bound = previous_upper_bound + scale_factor * weight
139
- return name if indicator <= new_upper_bound
140
- previous_upper_bound = new_upper_bound
169
+ upper_bound = 0
170
+ sorted_variants.each do |variant_name|
171
+ upper_bound = upper_bound + scale_factor * feature.variants[variant_name]
172
+ return variant_name if indicator <= upper_bound
141
173
  end
142
174
 
143
175
  raise ArgumentError, "A variant should have been found by this point, there is a bug in the code."
@@ -5,21 +5,20 @@ module Determinator
5
5
  class Feature
6
6
  attr_reader :name, :identifier, :bucket_type, :variants, :target_groups, :active, :winning_variant
7
7
 
8
- BUCKET_TYPES = %i(id guid fallback)
9
-
10
8
  def initialize(name:, identifier:, bucket_type:, target_groups:, variants: {}, overrides: {}, active: false, winning_variant: nil)
11
9
  @name = name.to_s
12
- @identifier = identifier.to_s
10
+ @identifier = (identifier || name).to_s
13
11
  @variants = variants
14
- @target_groups = target_groups
15
- @winning_variant = winning_variant
12
+ @target_groups = parse_target_groups(target_groups)
13
+ @winning_variant = parse_outcome(winning_variant, allow_exclusion: false)
16
14
  @active = active
17
-
18
15
  @bucket_type = bucket_type.to_sym
19
- raise ArgumentError, "Unknown bucket type: #{bucket_type}" unless BUCKET_TYPES.include?(@bucket_type)
20
16
 
21
17
  # To prevent confusion between actor id data types
22
- @overrides = Hash[overrides.map { |k, v| [k.to_s, v] }]
18
+ @overrides = overrides.each_with_object({}) do |(identifier, outcome), hash|
19
+ parsed = parse_outcome(outcome, allow_exclusion: true)
20
+ hash[identifier.to_s] = parsed unless parsed.nil?
21
+ end
23
22
  end
24
23
 
25
24
  def active?
@@ -47,8 +46,40 @@ module Determinator
47
46
  overrides[id.to_s]
48
47
  end
49
48
 
49
+ # Validates the given outcome for this feature.
50
+ def parse_outcome(outcome, allow_exclusion:)
51
+ valid_outcomes = experiment? ? variants.keys : [true]
52
+ valid_outcomes << false if allow_exclusion
53
+ valid_outcomes.include?(outcome) ? outcome : nil
54
+ end
55
+
56
+ def ==(other)
57
+ Marshal.dump(self) == Marshal.dump(other)
58
+ end
59
+
50
60
  private
51
61
 
52
62
  attr_reader :overrides
63
+
64
+ def parse_target_groups(target_groups)
65
+ target_groups.map(&method(:parse_target_group)).compact
66
+ end
67
+
68
+ def parse_target_group(target_group)
69
+ return target_group if target_group.is_a? TargetGroup
70
+
71
+ constraints = target_group['constraints'].to_h
72
+
73
+ TargetGroup.new(
74
+ rollout: target_group['rollout'].to_i,
75
+ constraints: constraints.each_with_object({}) do |(key, value), hash|
76
+ hash[key.to_s] = [*value].map(&:to_s)
77
+ end
78
+ )
79
+
80
+ # Invalid target groups are ignored
81
+ rescue
82
+ nil
83
+ end
53
84
  end
54
85
  end
@@ -0,0 +1,24 @@
1
+ require 'pathname'
2
+
3
+ module Determinator
4
+ module Retrieve
5
+ # A class which loads features from files within the initialized folder
6
+ class File
7
+ # @param :root [String,Pathname] The path to be used as the root to look in
8
+ # @param :serializer [#load] A serializer which will take the string of the read file and return a Feature object.
9
+ def initialize(root:, serializer: Determinator::Serializers::JSON )
10
+ @root = Pathname.new(root)
11
+ @serializer = serializer
12
+ end
13
+
14
+ def retrieve(feature_id)
15
+ feature = @root.join(feature_id)
16
+ return unless feature.exist?
17
+ @serializer.load(feature.read)
18
+ rescue => e
19
+ Determinator.notice_error(e)
20
+ nil
21
+ end
22
+ end
23
+ end
24
+ end
@@ -1,7 +1,7 @@
1
1
  require 'uri'
2
2
  require 'routemaster/drain/caching'
3
3
  require 'routemaster/responses/hateoas_response'
4
- require 'determinator/retrieve/null_cache'
4
+ require 'determinator/serializers/json'
5
5
 
6
6
  module Determinator
7
7
  module Retrieve
@@ -18,21 +18,18 @@ module Determinator
18
18
  CALLBACK_PATH = (URI.parse(ENV['ROUTEMASTER_CALLBACK_URL']).path rescue '/events').freeze
19
19
 
20
20
  # @param :discovery_url [String] The bootstrap URL of the instance of Florence which defines Features.
21
- def initialize(discovery_url:, retrieval_cache: NullCache.new)
21
+ def initialize(discovery_url:)
22
22
  client = ::Routemaster::APIClient.new(
23
23
  response_class: ::Routemaster::Responses::HateoasResponse
24
24
  )
25
- @retrieval_cache = retrieval_cache
26
25
  @actor_service = client.discover(discovery_url)
27
-
28
26
  end
29
27
 
30
28
  # Retrieves and processes the feature that goes by the given name on this retrieval mechanism.
31
29
  # @return [Determinator::Feature,nil] The details of the specified feature
32
30
  def retrieve(name)
33
- cached_feature_lookup(name) do
34
- @actor_service.feature.show(name)
35
- end
31
+ obj = @actor_service.feature.show(name)
32
+ Determinator::Serializers::JSON.load(obj.body.to_hash)
36
33
  rescue ::Routemaster::Errors::ResourceNotFound
37
34
  # Don't be noisy
38
35
  nil
@@ -51,68 +48,6 @@ module Determinator
51
48
  def routemaster_app
52
49
  @routemaster_app ||= ::Routemaster::Drain::Caching.new
53
50
  end
54
-
55
- def self.lookup_cache_key(feature_name)
56
- "determinator_cache:#{feature_name}"
57
- end
58
-
59
- private
60
-
61
- def cached_feature_lookup(feature_name)
62
- build_feature_from_api_response(
63
- @retrieval_cache.fetch(self.class.lookup_cache_key(feature_name)){ yield }
64
- )
65
- end
66
-
67
- def build_feature_from_api_response(obj)
68
- Feature.new(
69
- name: obj.body.name,
70
- identifier: obj.body.identifier,
71
- bucket_type: obj.body.bucket_type,
72
- active: obj.body.active,
73
- target_groups: target_groups_attribute(obj.body.target_groups),
74
- variants: obj.body.variants.to_h,
75
- overrides: overrides_attribute(obj.body.overrides),
76
- winning_variant: obj.body.winning_variant,
77
- )
78
- end
79
-
80
- def target_groups_attribute(target_groups)
81
- target_groups.map do |tg|
82
- TargetGroup.new(
83
- rollout: tg.rollout,
84
- constraints: constraints_attribute(tg.constraints)
85
- )
86
- end
87
- end
88
-
89
- def constraints_attribute(constraints)
90
- # TODO when FLO-3 closed: no need for legacy guard
91
- return legacy_constraints_attribute(constraints) unless constraints.respond_to?(:each_pair)
92
- constraints
93
- end
94
-
95
- def legacy_constraints_attribute(constraints)
96
- constraints.each_with_object({}) do |constraint, h|
97
- if h[constraint.scope]
98
- h[constraint.scope] = [*h[constraint.scope], constraint.identifier]
99
- else
100
- h[constraint.scope] = constraint.identifier
101
- end
102
- end
103
- end
104
-
105
- def overrides_attribute(overrides)
106
- # TODO when FLO-3 closed: no need for legacy guard
107
- return legacy_overrides_attribute(overrides) unless overrides.respond_to?(:each_pair)
108
- overrides
109
- end
110
-
111
- def legacy_overrides_attribute(overrides)
112
- overrides.each_with_object({}) do |override, h|
113
- h[override[:user_id]] = override[:variant]
114
- end
115
- end
116
51
  end
117
52
  end
118
53
  end
@@ -0,0 +1,28 @@
1
+ require 'json'
2
+
3
+ module Determinator
4
+ module Serializers
5
+ module JSON
6
+ class << self
7
+ def dump(feature)
8
+ raise NotImplementedError
9
+ end
10
+
11
+ def load(string_or_hash)
12
+ obj = string_or_hash.is_a?(Hash) ? string_or_hash : ::JSON.parse(string_or_hash)
13
+
14
+ Determinator::Feature.new(
15
+ name: obj['name'],
16
+ identifier: obj['identifier'],
17
+ bucket_type: obj['bucket_type'],
18
+ active: (obj['active'] === true),
19
+ target_groups: obj['target_groups'],
20
+ variants: obj['variants'].to_h,
21
+ overrides: obj['overrides'].to_h,
22
+ winning_variant: obj['winning_variant'].to_s,
23
+ )
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -1,3 +1,3 @@
1
1
  module Determinator
2
- VERSION = '0.12.1'
2
+ VERSION = '1.0.0'
3
3
  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.12.1
4
+ version: 1.0.0
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: 2018-02-01 00:00:00.000000000 Z
11
+ date: 2018-02-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: routemaster-drain
@@ -130,6 +130,7 @@ extensions: []
130
130
  extra_rdoc_files: []
131
131
  files:
132
132
  - ".gitignore"
133
+ - ".gitmodules"
133
134
  - ".rspec"
134
135
  - ".travis.yml"
135
136
  - CHANGELOG.md
@@ -178,11 +179,13 @@ files:
178
179
  - examples/determinator-rails/public/robots.txt
179
180
  - lib/determinator.rb
180
181
  - lib/determinator/actor_control.rb
182
+ - lib/determinator/cache/fetch_wrapper.rb
181
183
  - lib/determinator/control.rb
182
184
  - lib/determinator/feature.rb
183
- - lib/determinator/retrieve/null_cache.rb
185
+ - lib/determinator/retrieve/file.rb
184
186
  - lib/determinator/retrieve/null_retriever.rb
185
187
  - lib/determinator/retrieve/routemaster.rb
188
+ - lib/determinator/serializers/json.rb
186
189
  - lib/determinator/target_group.rb
187
190
  - lib/determinator/version.rb
188
191
  - lib/rspec/determinator.rb
@@ -1,5 +0,0 @@
1
- class NullCache
2
- def fetch(name)
3
- yield
4
- end
5
- end