determinator 0.12.1 → 1.0.0

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
  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