hackle-ruby-sdk 1.0.0 → 2.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 +4 -4
- data/lib/hackle/client.rb +186 -87
- data/lib/hackle/config.rb +59 -17
- data/lib/hackle/decision.rb +113 -0
- data/lib/hackle/event.rb +89 -0
- data/lib/hackle/internal/clock/clock.rb +47 -0
- data/lib/hackle/internal/concurrent/executors.rb +20 -0
- data/lib/hackle/internal/concurrent/schedule/scheduler.rb +12 -0
- data/lib/hackle/internal/concurrent/schedule/timer_scheduler.rb +30 -0
- data/lib/hackle/internal/config/parameter_config.rb +50 -0
- data/lib/hackle/internal/core/hackle_core.rb +182 -0
- data/lib/hackle/{decision → internal/evaluation/bucketer}/bucketer.rb +17 -15
- data/lib/hackle/internal/evaluation/evaluator/contextual/contextual_evaluator.rb +29 -0
- data/lib/hackle/internal/evaluation/evaluator/delegating/delegating_evaluator.rb +26 -0
- data/lib/hackle/internal/evaluation/evaluator/evaluator.rb +117 -0
- data/lib/hackle/internal/evaluation/evaluator/experiment/experiment_evaluation_flow_factory.rb +67 -0
- data/lib/hackle/internal/evaluation/evaluator/experiment/experiment_evaluator.rb +172 -0
- data/lib/hackle/internal/evaluation/evaluator/experiment/experiment_flow_evaluator.rb +241 -0
- data/lib/hackle/internal/evaluation/evaluator/experiment/experiment_resolver.rb +166 -0
- data/lib/hackle/internal/evaluation/evaluator/remoteconfig/remote_config_determiner.rb +48 -0
- data/lib/hackle/internal/evaluation/evaluator/remoteconfig/remote_config_evaluator.rb +174 -0
- data/lib/hackle/internal/evaluation/flow/evaluation_flow.rb +49 -0
- data/lib/hackle/internal/evaluation/flow/flow_evaluator.rb +11 -0
- data/lib/hackle/internal/evaluation/match/condition/condition_matcher.rb +11 -0
- data/lib/hackle/internal/evaluation/match/condition/condition_matcher_factory.rb +53 -0
- data/lib/hackle/internal/evaluation/match/condition/experiment/experiment_condition_matcher.rb +29 -0
- data/lib/hackle/internal/evaluation/match/condition/experiment/experiment_evaluator_matcher.rb +135 -0
- data/lib/hackle/internal/evaluation/match/condition/segment/segment_condition_matcher.rb +67 -0
- data/lib/hackle/internal/evaluation/match/condition/user/user_condition_matcher.rb +44 -0
- data/lib/hackle/internal/evaluation/match/operator/operator_matcher.rb +185 -0
- data/lib/hackle/internal/evaluation/match/operator/operator_matcher_factory.rb +31 -0
- data/lib/hackle/internal/evaluation/match/target/target_matcher.rb +31 -0
- data/lib/hackle/internal/evaluation/match/value/value_matcher.rb +96 -0
- data/lib/hackle/internal/evaluation/match/value/value_matcher_factory.rb +28 -0
- data/lib/hackle/internal/evaluation/match/value/value_operator_matcher.rb +59 -0
- data/lib/hackle/internal/event/user_event.rb +187 -0
- data/lib/hackle/internal/event/user_event_dispatcher.rb +156 -0
- data/lib/hackle/internal/event/user_event_factory.rb +58 -0
- data/lib/hackle/internal/event/user_event_processor.rb +181 -0
- data/lib/hackle/internal/http/http.rb +28 -0
- data/lib/hackle/internal/http/http_client.rb +48 -0
- data/lib/hackle/internal/identifiers/identifier_builder.rb +67 -0
- data/lib/hackle/internal/logger/logger.rb +31 -0
- data/lib/hackle/internal/model/action.rb +57 -0
- data/lib/hackle/internal/model/bucket.rb +58 -0
- data/lib/hackle/internal/model/container.rb +47 -0
- data/lib/hackle/internal/model/decision_reason.rb +31 -0
- data/lib/hackle/{models → internal/model}/event_type.rb +5 -8
- data/lib/hackle/internal/model/experiment.rb +194 -0
- data/lib/hackle/internal/model/parameter_configuration.rb +19 -0
- data/lib/hackle/internal/model/remote_config_parameter.rb +76 -0
- data/lib/hackle/internal/model/sdk.rb +23 -0
- data/lib/hackle/internal/model/segment.rb +61 -0
- data/lib/hackle/internal/model/target.rb +203 -0
- data/lib/hackle/internal/model/target_rule.rb +19 -0
- data/lib/hackle/internal/model/targeting.rb +45 -0
- data/lib/hackle/internal/model/value_type.rb +75 -0
- data/lib/hackle/internal/model/variation.rb +27 -0
- data/lib/hackle/internal/model/version.rb +153 -0
- data/lib/hackle/internal/properties/properties_builder.rb +101 -0
- data/lib/hackle/internal/user/hackle_user.rb +74 -0
- data/lib/hackle/internal/user/hackle_user_resolver.rb +27 -0
- data/lib/hackle/internal/workspace/http_workspace_fetcher.rb +50 -0
- data/lib/hackle/internal/workspace/polling_workspace_fetcher.rb +62 -0
- data/lib/hackle/internal/workspace/workspace.rb +353 -0
- data/lib/hackle/internal/workspace/workspace_fetcher.rb +18 -0
- data/lib/hackle/remote_config.rb +55 -0
- data/lib/hackle/user.rb +124 -0
- data/lib/hackle/version.rb +1 -11
- data/lib/hackle.rb +4 -69
- metadata +123 -53
- data/.gitignore +0 -11
- data/.rspec +0 -2
- data/.travis.yml +0 -7
- data/Gemfile +0 -6
- data/README.md +0 -33
- data/Rakefile +0 -6
- data/hackle-ruby-sdk.gemspec +0 -29
- data/lib/hackle/decision/decider.rb +0 -69
- data/lib/hackle/events/event_dispatcher.rb +0 -96
- data/lib/hackle/events/event_processor.rb +0 -126
- data/lib/hackle/events/user_event.rb +0 -61
- data/lib/hackle/http/http.rb +0 -37
- data/lib/hackle/models/bucket.rb +0 -26
- data/lib/hackle/models/event.rb +0 -26
- data/lib/hackle/models/experiment.rb +0 -69
- data/lib/hackle/models/slot.rb +0 -22
- data/lib/hackle/models/user.rb +0 -24
- data/lib/hackle/models/variation.rb +0 -21
- data/lib/hackle/workspaces/http_workspace_fetcher.rb +0 -24
- data/lib/hackle/workspaces/polling_workspace_fetcher.rb +0 -47
- data/lib/hackle/workspaces/workspace.rb +0 -100
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'concurrent'
|
|
4
|
+
require 'hackle/internal/concurrent/schedule/scheduler'
|
|
5
|
+
|
|
6
|
+
module Hackle
|
|
7
|
+
class TimerScheduler
|
|
8
|
+
include Scheduler
|
|
9
|
+
|
|
10
|
+
def schedule_periodically(interval_seconds, task)
|
|
11
|
+
timer_task = Concurrent::TimerTask.new(execution_interval: interval_seconds, interval_type: :fixed_rate) do
|
|
12
|
+
task.call
|
|
13
|
+
end
|
|
14
|
+
timer_task.execute
|
|
15
|
+
TimerScheduledJob.new(timer_task)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
class TimerScheduledJob
|
|
20
|
+
include ScheduledJob
|
|
21
|
+
|
|
22
|
+
def initialize(task)
|
|
23
|
+
@task = task
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def cancel
|
|
27
|
+
@task.shutdown
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hackle
|
|
4
|
+
class ParameterConfig
|
|
5
|
+
|
|
6
|
+
# @return [Hash{String => Object}]
|
|
7
|
+
attr_reader :parameters
|
|
8
|
+
|
|
9
|
+
# @param parameters [Hash{String => Object}]
|
|
10
|
+
def initialize(parameters)
|
|
11
|
+
@parameters = parameters
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
@empty = new({})
|
|
15
|
+
|
|
16
|
+
# @return [Hackle::ParameterConfig]
|
|
17
|
+
def self.empty
|
|
18
|
+
@empty
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def ==(other)
|
|
22
|
+
other.is_a?(self.class) && other.parameters == parameters
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def to_s
|
|
26
|
+
"Hackle::ParameterConfig(#{parameters})"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# @param key [String]
|
|
30
|
+
# @param default_value [Object, nil]
|
|
31
|
+
# @return [Object, nil]
|
|
32
|
+
def get(key, default_value = nil)
|
|
33
|
+
parameter_value = parameters.fetch(key, default_value)
|
|
34
|
+
|
|
35
|
+
return default_value if parameter_value.nil?
|
|
36
|
+
return parameter_value if default_value.nil?
|
|
37
|
+
|
|
38
|
+
case default_value
|
|
39
|
+
when String
|
|
40
|
+
parameter_value.is_a?(String) ? parameter_value : default_value
|
|
41
|
+
when Numeric
|
|
42
|
+
parameter_value.is_a?(Numeric) ? parameter_value : default_value
|
|
43
|
+
when TrueClass, FalseClass
|
|
44
|
+
parameter_value.is_a?(TrueClass) || parameter_value.is_a?(FalseClass) ? parameter_value : default_value
|
|
45
|
+
else
|
|
46
|
+
default_value
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'hackle/internal/event/user_event'
|
|
4
|
+
require 'hackle/internal/model/decision_reason'
|
|
5
|
+
require 'hackle/internal/evaluation/evaluator/delegating/delegating_evaluator'
|
|
6
|
+
require 'hackle/internal/evaluation/match/condition/condition_matcher_factory'
|
|
7
|
+
require 'hackle/internal/evaluation/match/target/target_matcher'
|
|
8
|
+
require 'hackle/internal/evaluation/bucketer/bucketer'
|
|
9
|
+
require 'hackle/internal/evaluation/evaluator/experiment/experiment_evaluator'
|
|
10
|
+
require 'hackle/internal/evaluation/evaluator/experiment/experiment_evaluation_flow_factory'
|
|
11
|
+
require 'hackle/internal/evaluation/evaluator/remoteconfig/remote_config_evaluator'
|
|
12
|
+
require 'hackle/internal/evaluation/evaluator/remoteconfig/remote_config_determiner'
|
|
13
|
+
require 'hackle/internal/event/user_event_factory'
|
|
14
|
+
require 'hackle/internal/clock/clock'
|
|
15
|
+
|
|
16
|
+
module Hackle
|
|
17
|
+
class Core
|
|
18
|
+
|
|
19
|
+
# @param experiment_evaluator [ExperimentEvaluator]
|
|
20
|
+
# @param remote_config_evaluator [RemoteConfigEvaluator]
|
|
21
|
+
# @param workspace_fetcher [WorkspaceFetcher]
|
|
22
|
+
# @param event_factory [UserEventFactory]
|
|
23
|
+
# @param event_processor [UserEventProcessor]
|
|
24
|
+
# @param clock [Clock]
|
|
25
|
+
def initialize(
|
|
26
|
+
experiment_evaluator:,
|
|
27
|
+
remote_config_evaluator:,
|
|
28
|
+
workspace_fetcher:,
|
|
29
|
+
event_factory:,
|
|
30
|
+
event_processor:,
|
|
31
|
+
clock:
|
|
32
|
+
)
|
|
33
|
+
# @type experiment_evaluator [ExperimentEvaluator]
|
|
34
|
+
@experiment_evaluator = experiment_evaluator
|
|
35
|
+
# @type remote_config_evaluator [RemoteConfigEvaluator]
|
|
36
|
+
@remote_config_evaluator = remote_config_evaluator
|
|
37
|
+
# @type workspace_fetcher [WorkspaceFetcher]
|
|
38
|
+
@workspace_fetcher = workspace_fetcher
|
|
39
|
+
# @type event_factory [UserEventFactory]
|
|
40
|
+
@event_factory = event_factory
|
|
41
|
+
# @type event_processor [UserEventProcessor]
|
|
42
|
+
@event_processor = event_processor
|
|
43
|
+
# @type clock [Clock]
|
|
44
|
+
@clock = clock
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# @param workspace_fetcher [WorkspaceFetcher]
|
|
48
|
+
# @param event_processor [UserEventProcessor]
|
|
49
|
+
# @return [Core]
|
|
50
|
+
def self.create(workspace_fetcher:, event_processor:)
|
|
51
|
+
delegating_evaluator = DelegatingEvaluator.new
|
|
52
|
+
|
|
53
|
+
condition_matcher_factory = ConditionMatcherFactory.new(evaluator: delegating_evaluator)
|
|
54
|
+
target_matcher = TargetMatcher.new(condition_matcher_factory: condition_matcher_factory)
|
|
55
|
+
bucketer = Bucketer.new(hasher: Hasher.new)
|
|
56
|
+
|
|
57
|
+
experiment_evaluator = ExperimentEvaluator.new(
|
|
58
|
+
flow_factory: ExperimentEvaluationFlowFactory.new(
|
|
59
|
+
target_matcher: target_matcher,
|
|
60
|
+
bucketer: bucketer
|
|
61
|
+
)
|
|
62
|
+
)
|
|
63
|
+
delegating_evaluator.add(experiment_evaluator)
|
|
64
|
+
|
|
65
|
+
remote_config_evaluator = RemoteConfigEvaluator.new(
|
|
66
|
+
target_rule_determiner: RemoteConfigTargetRuleDeterminer.new(
|
|
67
|
+
matcher: RemoteConfigTargetRuleMatcher.new(
|
|
68
|
+
target_matcher: target_matcher,
|
|
69
|
+
bucketer: bucketer
|
|
70
|
+
)
|
|
71
|
+
)
|
|
72
|
+
)
|
|
73
|
+
delegating_evaluator.add(remote_config_evaluator)
|
|
74
|
+
|
|
75
|
+
Core.new(
|
|
76
|
+
experiment_evaluator: experiment_evaluator,
|
|
77
|
+
remote_config_evaluator: remote_config_evaluator,
|
|
78
|
+
workspace_fetcher: workspace_fetcher,
|
|
79
|
+
event_factory: UserEventFactory.new(clock: SystemClock.instance),
|
|
80
|
+
event_processor: event_processor,
|
|
81
|
+
clock: SystemClock.instance
|
|
82
|
+
)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# @param experiment_key [Integer]
|
|
86
|
+
# @param user [HackleUser]
|
|
87
|
+
# @param default_variation [String]
|
|
88
|
+
# @return [ExperimentDecision]
|
|
89
|
+
def experiment(experiment_key, user, default_variation)
|
|
90
|
+
workspace = @workspace_fetcher.fetch
|
|
91
|
+
return ExperimentDecision.new(default_variation, DecisionReason::SDK_NOT_READY, ParameterConfig.empty) if workspace.nil?
|
|
92
|
+
|
|
93
|
+
experiment = workspace.get_experiment_or_nil(experiment_key)
|
|
94
|
+
return ExperimentDecision.new(default_variation, DecisionReason::EXPERIMENT_NOT_FOUND, ParameterConfig.empty) if experiment.nil?
|
|
95
|
+
|
|
96
|
+
request = ExperimentRequest.create(workspace, user, experiment, default_variation)
|
|
97
|
+
evaluation = @experiment_evaluator.evaluate(request, Evaluator.context)
|
|
98
|
+
|
|
99
|
+
events = @event_factory.create(request, evaluation)
|
|
100
|
+
events.each do |event|
|
|
101
|
+
@event_processor.process(event)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
ExperimentDecision.new(evaluation.variation_key, evaluation.reason, evaluation.parameter_config)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# @param feature_key [Integer]
|
|
108
|
+
# @param user [HackleUser]
|
|
109
|
+
# @return [FeatureFlagDecision]
|
|
110
|
+
def feature_flag(feature_key, user)
|
|
111
|
+
workspace = @workspace_fetcher.fetch
|
|
112
|
+
return FeatureFlagDecision.new(false, DecisionReason::SDK_NOT_READY, ParameterConfig.empty) if workspace.nil?
|
|
113
|
+
|
|
114
|
+
feature_flag = workspace.get_feature_flag_or_nil(feature_key)
|
|
115
|
+
return FeatureFlagDecision.new(false, DecisionReason::FEATURE_FLAG_NOT_FOUND, ParameterConfig.empty) if feature_flag.nil?
|
|
116
|
+
|
|
117
|
+
request = ExperimentRequest.create(workspace, user, feature_flag, 'A')
|
|
118
|
+
evaluation = @experiment_evaluator.evaluate(request, Evaluator.context)
|
|
119
|
+
|
|
120
|
+
events = @event_factory.create(request, evaluation)
|
|
121
|
+
events.each do |event|
|
|
122
|
+
@event_processor.process(event)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
is_on = evaluation.variation_key != 'A'
|
|
126
|
+
FeatureFlagDecision.new(is_on, evaluation.reason, evaluation.parameter_config)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# @param parameter_key [String]
|
|
130
|
+
# @param user [HackleUser]
|
|
131
|
+
# @param required_type [ValueType]
|
|
132
|
+
# @param default_value [Object, nil]
|
|
133
|
+
def remote_config(parameter_key, user, required_type, default_value)
|
|
134
|
+
workspace = @workspace_fetcher.fetch
|
|
135
|
+
return RemoteConfigDecision.new(default_value, DecisionReason::SDK_NOT_READY) if workspace.nil?
|
|
136
|
+
|
|
137
|
+
parameter = workspace.get_remote_config_parameter_or_nil(parameter_key)
|
|
138
|
+
return RemoteConfigDecision.new(default_value, DecisionReason::REMOTE_CONFIG_PARAMETER_NOT_FOUND) if parameter.nil?
|
|
139
|
+
|
|
140
|
+
request = RemoteConfigRequest.create(workspace, user, parameter, required_type, default_value)
|
|
141
|
+
evaluation = @remote_config_evaluator.evaluate(request, Evaluator.context)
|
|
142
|
+
|
|
143
|
+
events = @event_factory.create(request, evaluation)
|
|
144
|
+
events.each do |event|
|
|
145
|
+
@event_processor.process(event)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
RemoteConfigDecision.new(evaluation.value, evaluation.reason)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# @param event [Event]
|
|
152
|
+
# @param user [HackleUser]
|
|
153
|
+
def track(event, user)
|
|
154
|
+
event_type = event_type(event)
|
|
155
|
+
@event_processor.process(UserEvent.track(event_type, event, user, @clock.current_millis))
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def close
|
|
159
|
+
@workspace_fetcher.stop
|
|
160
|
+
@event_processor.stop
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def resume
|
|
164
|
+
@workspace_fetcher.resume
|
|
165
|
+
@event_processor.resume
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
private
|
|
169
|
+
|
|
170
|
+
# @param event [Event]
|
|
171
|
+
# @return [EventType]
|
|
172
|
+
def event_type(event)
|
|
173
|
+
workspace = @workspace_fetcher.fetch
|
|
174
|
+
return EventType.new(id: 0, key: event.key) if workspace.nil?
|
|
175
|
+
|
|
176
|
+
event_type = workspace.get_event_type_or_nil(event.key)
|
|
177
|
+
return EventType.new(id: 0, key: event.key) if event_type.nil?
|
|
178
|
+
|
|
179
|
+
event_type
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
|
@@ -5,34 +5,36 @@ require 'murmurhash3'
|
|
|
5
5
|
module Hackle
|
|
6
6
|
class Bucketer
|
|
7
7
|
|
|
8
|
+
# @param hasher [Hasher]
|
|
9
|
+
def initialize(hasher:)
|
|
10
|
+
# @type [Hasher]
|
|
11
|
+
@hasher = hasher
|
|
12
|
+
end
|
|
13
|
+
|
|
8
14
|
# @param bucket [Bucket]
|
|
9
|
-
# @param
|
|
10
|
-
#
|
|
15
|
+
# @param identifier [String]
|
|
11
16
|
# @return [Slot, nil]
|
|
12
|
-
def bucketing(bucket
|
|
13
|
-
slot_number = calculate_slot_number(
|
|
14
|
-
|
|
15
|
-
slot_size: bucket.slot_size,
|
|
16
|
-
user_id: user.id
|
|
17
|
-
)
|
|
18
|
-
bucket.get_slot(slot_number: slot_number)
|
|
17
|
+
def bucketing(bucket, identifier)
|
|
18
|
+
slot_number = calculate_slot_number(seed: bucket.seed, slot_size: bucket.slot_size, value: identifier)
|
|
19
|
+
bucket.get_slot_or_nil(slot_number)
|
|
19
20
|
end
|
|
20
21
|
|
|
21
22
|
# @param seed [Integer]
|
|
22
23
|
# @param slot_size [Integer]
|
|
23
|
-
# @param
|
|
24
|
-
#
|
|
24
|
+
# @param value [String]
|
|
25
25
|
# @return [Integer]
|
|
26
|
-
def calculate_slot_number(seed:, slot_size:,
|
|
27
|
-
hash_value = hash(
|
|
26
|
+
def calculate_slot_number(seed:, slot_size:, value:)
|
|
27
|
+
hash_value = @hasher.hash(value, seed)
|
|
28
28
|
hash_value.abs % slot_size
|
|
29
29
|
end
|
|
30
30
|
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
class Hasher
|
|
31
34
|
# @param data [String]
|
|
32
35
|
# @param seed [Integer]
|
|
33
|
-
#
|
|
34
36
|
# @return [Integer]
|
|
35
|
-
def hash(data
|
|
37
|
+
def hash(data, seed)
|
|
36
38
|
unsigned_value = MurmurHash3::V32.str_hash(data, seed)
|
|
37
39
|
if (unsigned_value & 0x80000000).zero?
|
|
38
40
|
unsigned_value
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'hackle/internal/evaluation/evaluator/evaluator'
|
|
4
|
+
|
|
5
|
+
module Hackle
|
|
6
|
+
module ContextualEvaluator
|
|
7
|
+
include Evaluator
|
|
8
|
+
|
|
9
|
+
# @param request [EvaluatorRequest]
|
|
10
|
+
# @return [boolean]
|
|
11
|
+
def supports?(request) end
|
|
12
|
+
|
|
13
|
+
# @param request [EvaluatorRequest]
|
|
14
|
+
# @param context [EvaluatorContext]
|
|
15
|
+
# @return [EvaluatorEvaluation]
|
|
16
|
+
def evaluate_internal(request, context) end
|
|
17
|
+
|
|
18
|
+
def evaluate(request, context)
|
|
19
|
+
raise ArgumentError, 'circular evaluation has occurred' if context.request_include?(request)
|
|
20
|
+
|
|
21
|
+
context.add_request(request)
|
|
22
|
+
begin
|
|
23
|
+
evaluate_internal(request, context)
|
|
24
|
+
ensure
|
|
25
|
+
context.remove_request(request)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'hackle/internal/evaluation/evaluator/evaluator'
|
|
4
|
+
|
|
5
|
+
module Hackle
|
|
6
|
+
class DelegatingEvaluator
|
|
7
|
+
include Evaluator
|
|
8
|
+
|
|
9
|
+
def initialize
|
|
10
|
+
# @type [Array<ContextualEvaluator>]
|
|
11
|
+
@evaluators = []
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# @param evaluator [ContextualEvaluator]
|
|
15
|
+
def add(evaluator)
|
|
16
|
+
@evaluators << evaluator
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def evaluate(request, context)
|
|
20
|
+
evaluator = @evaluators.find { |it| it.supports?(request) }
|
|
21
|
+
raise ArgumentError, "unsupported EvaluatorRequest [#{request.class}]" if evaluator.nil?
|
|
22
|
+
|
|
23
|
+
evaluator.evaluate(request, context)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hackle
|
|
4
|
+
|
|
5
|
+
module Evaluator
|
|
6
|
+
# @param request [EvaluatorRequest]
|
|
7
|
+
# @param context [EvaluatorContext]
|
|
8
|
+
# @return [EvaluatorEvaluation]
|
|
9
|
+
def evaluate(request, context) end
|
|
10
|
+
|
|
11
|
+
# @return [EvaluatorContext]
|
|
12
|
+
def self.context
|
|
13
|
+
EvaluatorContext.new
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
class EvaluatorRequest
|
|
18
|
+
|
|
19
|
+
# @return [EvaluatorKey]
|
|
20
|
+
attr_reader :key
|
|
21
|
+
|
|
22
|
+
# @return [Workspace]
|
|
23
|
+
attr_reader :workspace
|
|
24
|
+
|
|
25
|
+
# @return [HackleUser]
|
|
26
|
+
attr_reader :user
|
|
27
|
+
|
|
28
|
+
# @param key [EvaluatorKey]
|
|
29
|
+
# @param workspace [Workspace]
|
|
30
|
+
# @param user [HackleUser]
|
|
31
|
+
def initialize(key:, workspace:, user:)
|
|
32
|
+
@key = key
|
|
33
|
+
@workspace = workspace
|
|
34
|
+
@user = user
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def ==(other)
|
|
38
|
+
other.is_a?(EvaluatorRequest) && key == other.key
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
class EvaluatorEvaluation
|
|
43
|
+
|
|
44
|
+
# @return [String]
|
|
45
|
+
attr_reader :reason
|
|
46
|
+
|
|
47
|
+
# @return [Array<EvaluatorEvaluation>]
|
|
48
|
+
attr_reader :target_evaluations
|
|
49
|
+
|
|
50
|
+
# @param reason [String]
|
|
51
|
+
# @param target_evaluations [Array<EvaluatorEvaluation>]
|
|
52
|
+
def initialize(reason:, target_evaluations:)
|
|
53
|
+
@reason = reason
|
|
54
|
+
@target_evaluations = target_evaluations
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
class EvaluatorContext
|
|
59
|
+
|
|
60
|
+
def initialize
|
|
61
|
+
# @type [Array<EvaluatorRequest>]
|
|
62
|
+
@requests = []
|
|
63
|
+
# @type [Array<EvaluatorEvaluation>]
|
|
64
|
+
@evaluations = []
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# @return [Array<EvaluatorRequest>]
|
|
68
|
+
def requests
|
|
69
|
+
@requests.dup
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# @param request [EvaluatorRequest]
|
|
73
|
+
# @return [boolean]
|
|
74
|
+
def request_include?(request)
|
|
75
|
+
@requests.include?(request)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# @param request [EvaluatorRequest]
|
|
79
|
+
def add_request(request)
|
|
80
|
+
@requests << request
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# @param request [EvaluatorRequest]
|
|
84
|
+
def remove_request(request)
|
|
85
|
+
@requests.delete(request)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# @return [Array<EvaluatorEvaluation>]
|
|
89
|
+
def evaluations
|
|
90
|
+
@evaluations.dup
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# @param evaluation [EvaluatorEvaluation]
|
|
94
|
+
def add_evaluation(evaluation)
|
|
95
|
+
@evaluations << evaluation
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
class EvaluatorKey
|
|
100
|
+
# @return [String]
|
|
101
|
+
attr_reader :type
|
|
102
|
+
|
|
103
|
+
# @return [Integer]
|
|
104
|
+
attr_reader :id
|
|
105
|
+
|
|
106
|
+
# @param type [String]
|
|
107
|
+
# @param id [Integer]
|
|
108
|
+
def initialize(type:, id:)
|
|
109
|
+
@type = type
|
|
110
|
+
@id = id
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def ==(other)
|
|
114
|
+
other.is_a?(EvaluatorKey) && type == other.type && id == other.id
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
data/lib/hackle/internal/evaluation/evaluator/experiment/experiment_evaluation_flow_factory.rb
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'hackle/internal/model/experiment'
|
|
4
|
+
require 'hackle/internal/evaluation/flow/evaluation_flow'
|
|
5
|
+
require 'hackle/internal/evaluation/evaluator/experiment/experiment_resolver'
|
|
6
|
+
require 'hackle/internal/evaluation/evaluator/experiment/experiment_flow_evaluator'
|
|
7
|
+
|
|
8
|
+
module Hackle
|
|
9
|
+
class ExperimentEvaluationFlowFactory
|
|
10
|
+
# @param target_matcher [TargetMatcher]
|
|
11
|
+
# @param bucketer [Bucketer]
|
|
12
|
+
def initialize(target_matcher:, bucketer:)
|
|
13
|
+
|
|
14
|
+
action_resolver = ExperimentActionResolver.new(bucketer: bucketer)
|
|
15
|
+
override_resolver = ExperimentOverrideResolver.new(
|
|
16
|
+
target_matcher: target_matcher,
|
|
17
|
+
action_resolver: action_resolver
|
|
18
|
+
)
|
|
19
|
+
container_resolver = ExperimentContainerResolver.new(bucketer: bucketer)
|
|
20
|
+
target_determiner = ExperimentTargetDeterminer.new(target_matcher: target_matcher)
|
|
21
|
+
target_rule_determiner = ExperimentTargetRuleDeterminer.new(target_matcher: target_matcher)
|
|
22
|
+
|
|
23
|
+
# @type [ExperimentFlow]
|
|
24
|
+
@ab_test_flow = EvaluationFlow.create(
|
|
25
|
+
[
|
|
26
|
+
OverrideExperimentFlowEvaluator.new(override_resolver: override_resolver),
|
|
27
|
+
IdentifierExperimentFlowEvaluator.new,
|
|
28
|
+
ContainerExperimentFlowEvaluator.new(container_resolver: container_resolver),
|
|
29
|
+
TargetExperimentFlowEvaluator.new(target_determiner: target_determiner),
|
|
30
|
+
DraftExperimentFlowEvaluator.new,
|
|
31
|
+
PausedExperimentFlowEvaluator.new,
|
|
32
|
+
CompletedExperimentFlowEvaluator.new,
|
|
33
|
+
TrafficAllocateExperimentFlowEvaluator.new(action_resolver: action_resolver)
|
|
34
|
+
]
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
# @type [ExperimentFlow]
|
|
38
|
+
@feature_flag_flow = EvaluationFlow.create(
|
|
39
|
+
[
|
|
40
|
+
DraftExperimentFlowEvaluator.new,
|
|
41
|
+
PausedExperimentFlowEvaluator.new,
|
|
42
|
+
CompletedExperimentFlowEvaluator.new,
|
|
43
|
+
OverrideExperimentFlowEvaluator.new(override_resolver: override_resolver),
|
|
44
|
+
IdentifierExperimentFlowEvaluator.new,
|
|
45
|
+
TargetRuleExperimentFlowEvaluator.new(
|
|
46
|
+
target_rule_determiner: target_rule_determiner,
|
|
47
|
+
action_resolver: action_resolver
|
|
48
|
+
),
|
|
49
|
+
DefaultRuleExperimentFlowEvaluator.new(action_resolver: action_resolver)
|
|
50
|
+
]
|
|
51
|
+
)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# @param experiment_type [ExperimentType]
|
|
55
|
+
# @return [ExperimentFlow]
|
|
56
|
+
def get(experiment_type)
|
|
57
|
+
case experiment_type
|
|
58
|
+
when ExperimentType::AB_TEST
|
|
59
|
+
@ab_test_flow
|
|
60
|
+
when ExperimentType::FEATURE_FLAG
|
|
61
|
+
@feature_flag_flow
|
|
62
|
+
else
|
|
63
|
+
raise ArgumentError, "unsupported ExperimentType [#{experiment_type}]"
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|