hackle-ruby-sdk 0.1.0 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/hackle/client.rb +191 -79
- 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/internal/evaluation/bucketer/bucketer.rb +46 -0
- 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/internal/model/event_type.rb +19 -0
- 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 -32
- metadata +123 -51
- 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/bucketer.rb +0 -30
- data/lib/hackle/decision/decider.rb +0 -54
- data/lib/hackle/events/event.rb +0 -33
- data/lib/hackle/events/event_dispatcher.rb +0 -89
- data/lib/hackle/events/event_processor.rb +0 -115
- data/lib/hackle/http/http.rb +0 -37
- data/lib/hackle/models/bucket.rb +0 -15
- data/lib/hackle/models/event_type.rb +0 -14
- data/lib/hackle/models/experiment.rb +0 -36
- data/lib/hackle/models/slot.rb +0 -15
- data/lib/hackle/models/variation.rb +0 -11
- data/lib/hackle/workspaces/http_workspace_fetcher.rb +0 -24
- data/lib/hackle/workspaces/polling_workspace_fetcher.rb +0 -44
- data/lib/hackle/workspaces/workspace.rb +0 -87
@@ -1,115 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Hackle
|
4
|
-
|
5
|
-
class EventProcessor
|
6
|
-
|
7
|
-
DEFAULT_FLUSH_INTERVAL = 10
|
8
|
-
|
9
|
-
def initialize(config:, event_dispatcher:)
|
10
|
-
@logger = config.logger
|
11
|
-
@event_dispatcher = event_dispatcher
|
12
|
-
@message_processor = MessageProcessor.new(config: config, event_dispatcher: event_dispatcher)
|
13
|
-
@flush_task = Concurrent::TimerTask.new(execution_interval: DEFAULT_FLUSH_INTERVAL) { flush }
|
14
|
-
@consume_task = nil
|
15
|
-
@running = false
|
16
|
-
end
|
17
|
-
|
18
|
-
def start!
|
19
|
-
return if @running
|
20
|
-
|
21
|
-
@consume_task = Thread.new { @message_processor.consuming_loop }
|
22
|
-
@flush_task.execute
|
23
|
-
@running = true
|
24
|
-
end
|
25
|
-
|
26
|
-
def stop!
|
27
|
-
return unless @running
|
28
|
-
|
29
|
-
@message_processor.produce(message: Message::Shutdown.new, non_block: false)
|
30
|
-
@consume_task.join(10)
|
31
|
-
@flush_task.shutdown
|
32
|
-
@event_dispatcher.shutdown
|
33
|
-
|
34
|
-
@running = false
|
35
|
-
end
|
36
|
-
|
37
|
-
def process(event:)
|
38
|
-
@message_processor.produce(message: Message::Event.new(event))
|
39
|
-
end
|
40
|
-
|
41
|
-
def flush
|
42
|
-
@message_processor.produce(message: Message::Flush.new)
|
43
|
-
end
|
44
|
-
|
45
|
-
class Message
|
46
|
-
class Event < Message
|
47
|
-
attr_reader :event
|
48
|
-
|
49
|
-
def initialize(event)
|
50
|
-
@event = event
|
51
|
-
end
|
52
|
-
end
|
53
|
-
|
54
|
-
class Flush < Message
|
55
|
-
end
|
56
|
-
|
57
|
-
class Shutdown < Message
|
58
|
-
end
|
59
|
-
end
|
60
|
-
|
61
|
-
class MessageProcessor
|
62
|
-
|
63
|
-
DEFAULT_MESSAGE_QUEUE_CAPACITY = 1000
|
64
|
-
DEFAULT_MAX_EVENT_DISPATCH_SIZE = 500
|
65
|
-
|
66
|
-
def initialize(config:, event_dispatcher:)
|
67
|
-
@logger = config.logger
|
68
|
-
@event_dispatcher = event_dispatcher
|
69
|
-
@message_queue = SizedQueue.new(DEFAULT_MESSAGE_QUEUE_CAPACITY)
|
70
|
-
@random = Random.new
|
71
|
-
@consumed_events = []
|
72
|
-
end
|
73
|
-
|
74
|
-
def produce(message:, non_block: true)
|
75
|
-
@message_queue.push(message, non_block)
|
76
|
-
rescue ThreadError
|
77
|
-
if @random.rand(1..100) == 1 # log only 1% of the time
|
78
|
-
@logger.warn { 'Events are produced faster than can be consumed. Some events will be dropped.' }
|
79
|
-
end
|
80
|
-
end
|
81
|
-
|
82
|
-
def consuming_loop
|
83
|
-
loop do
|
84
|
-
message = @message_queue.pop
|
85
|
-
case message
|
86
|
-
when Message::Event
|
87
|
-
consume_event(event: message.event)
|
88
|
-
when Message::Flush
|
89
|
-
dispatch_events
|
90
|
-
when Message::Shutdown
|
91
|
-
break
|
92
|
-
end
|
93
|
-
end
|
94
|
-
rescue => e
|
95
|
-
@logger.warn { "Uncaught exception in events message processor: #{e.inspect}" }
|
96
|
-
ensure
|
97
|
-
dispatch_events
|
98
|
-
end
|
99
|
-
|
100
|
-
private
|
101
|
-
|
102
|
-
def consume_event(event:)
|
103
|
-
@consumed_events << event
|
104
|
-
dispatch_events if @consumed_events.length >= DEFAULT_MAX_EVENT_DISPATCH_SIZE
|
105
|
-
end
|
106
|
-
|
107
|
-
def dispatch_events
|
108
|
-
return if @consumed_events.empty?
|
109
|
-
|
110
|
-
@event_dispatcher.dispatch(events: @consumed_events)
|
111
|
-
@consumed_events = []
|
112
|
-
end
|
113
|
-
end
|
114
|
-
end
|
115
|
-
end
|
data/lib/hackle/http/http.rb
DELETED
@@ -1,37 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'net/http'
|
4
|
-
|
5
|
-
module Hackle
|
6
|
-
class UnexpectedResponseError < StandardError
|
7
|
-
end
|
8
|
-
|
9
|
-
class HTTP
|
10
|
-
def self.client(base_uri:)
|
11
|
-
uri = URI.parse(base_uri)
|
12
|
-
client = Net::HTTP.new(uri.host, uri.port)
|
13
|
-
client.use_ssl = uri.scheme == 'https'
|
14
|
-
client.open_timeout = 5
|
15
|
-
client.read_timeout = 10
|
16
|
-
client
|
17
|
-
end
|
18
|
-
|
19
|
-
def self.sdk_headers(sdk_info:)
|
20
|
-
{
|
21
|
-
'X-HACKLE-SDK-KEY' => sdk_info.key,
|
22
|
-
'X-HACKLE-SDK-NAME' => sdk_info.name,
|
23
|
-
'X-HACKLE-SDK-VERSION' => sdk_info.version
|
24
|
-
}
|
25
|
-
end
|
26
|
-
|
27
|
-
def self.successful?(status_code:)
|
28
|
-
status_code >= 200 && status_code < 300
|
29
|
-
end
|
30
|
-
|
31
|
-
def self.check_successful(status_code:)
|
32
|
-
unless successful?(status_code: status_code)
|
33
|
-
raise UnexpectedResponseError, "HTTP status code #{status_code}"
|
34
|
-
end
|
35
|
-
end
|
36
|
-
end
|
37
|
-
end
|
data/lib/hackle/models/bucket.rb
DELETED
@@ -1,15 +0,0 @@
|
|
1
|
-
module Hackle
|
2
|
-
class Bucket
|
3
|
-
attr_reader :seed, :slot_size
|
4
|
-
|
5
|
-
def initialize(seed:, slot_size:, slots:)
|
6
|
-
@seed = seed
|
7
|
-
@slot_size = slot_size
|
8
|
-
@slots = slots
|
9
|
-
end
|
10
|
-
|
11
|
-
def get_slot(slot_number:)
|
12
|
-
@slots.find { |slot| slot.contains?(slot_number: slot_number) }
|
13
|
-
end
|
14
|
-
end
|
15
|
-
end
|
@@ -1,36 +0,0 @@
|
|
1
|
-
module Hackle
|
2
|
-
class Experiment
|
3
|
-
attr_reader :id, :key
|
4
|
-
|
5
|
-
class Running < Experiment
|
6
|
-
attr_reader :bucket
|
7
|
-
|
8
|
-
def initialize(id:, key:, bucket:, variations:, user_overrides:)
|
9
|
-
@id = id
|
10
|
-
@key = key
|
11
|
-
@bucket = bucket
|
12
|
-
@variations = variations
|
13
|
-
@user_overrides = user_overrides
|
14
|
-
end
|
15
|
-
|
16
|
-
def get_variation(variation_id:)
|
17
|
-
@variations[variation_id]
|
18
|
-
end
|
19
|
-
|
20
|
-
def get_overridden_variation(user_id:)
|
21
|
-
variation_id = @user_overrides[user_id]
|
22
|
-
get_variation(variation_id: variation_id)
|
23
|
-
end
|
24
|
-
end
|
25
|
-
|
26
|
-
class Completed < Experiment
|
27
|
-
attr_reader :winner_variation_key
|
28
|
-
|
29
|
-
def initialize(id:, key:, winner_variation_key:)
|
30
|
-
@id = id
|
31
|
-
@key = key
|
32
|
-
@winner_variation_key = winner_variation_key
|
33
|
-
end
|
34
|
-
end
|
35
|
-
end
|
36
|
-
end
|
data/lib/hackle/models/slot.rb
DELETED
@@ -1,15 +0,0 @@
|
|
1
|
-
module Hackle
|
2
|
-
class Slot
|
3
|
-
attr_reader :variation_id
|
4
|
-
|
5
|
-
def initialize(start_inclusive:, end_exclusive:, variation_id:)
|
6
|
-
@start_inclusive = start_inclusive
|
7
|
-
@end_exclusive = end_exclusive
|
8
|
-
@variation_id = variation_id
|
9
|
-
end
|
10
|
-
|
11
|
-
def contains?(slot_number:)
|
12
|
-
@start_inclusive <= slot_number && slot_number < @end_exclusive
|
13
|
-
end
|
14
|
-
end
|
15
|
-
end
|
@@ -1,24 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'json'
|
4
|
-
|
5
|
-
module Hackle
|
6
|
-
class HttpWorkspaceFetcher
|
7
|
-
|
8
|
-
def initialize(config:, sdk_info:)
|
9
|
-
@client = HTTP.client(base_uri: config.base_uri)
|
10
|
-
@headers = HTTP.sdk_headers(sdk_info: sdk_info)
|
11
|
-
end
|
12
|
-
|
13
|
-
def fetch
|
14
|
-
request = Net::HTTP::Get.new('/api/v1/workspaces', @headers)
|
15
|
-
response = @client.request(request)
|
16
|
-
|
17
|
-
status_code = response.code.to_i
|
18
|
-
HTTP.check_successful(status_code: status_code)
|
19
|
-
|
20
|
-
response_body = JSON.parse(response.body, symbolize_names: true)
|
21
|
-
Workspace.create(data: response_body)
|
22
|
-
end
|
23
|
-
end
|
24
|
-
end
|
@@ -1,44 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'concurrent'
|
4
|
-
|
5
|
-
module Hackle
|
6
|
-
class PollingWorkspaceFetcher
|
7
|
-
|
8
|
-
DEFAULT_POLLING_INTERVAL = 10
|
9
|
-
|
10
|
-
def initialize(config:, http_fetcher:)
|
11
|
-
@logger = config.logger
|
12
|
-
@http_fetcher = http_fetcher
|
13
|
-
@current_workspace = Concurrent::AtomicReference.new
|
14
|
-
@task = Concurrent::TimerTask.new(execution_interval: DEFAULT_POLLING_INTERVAL) { poll }
|
15
|
-
@running = false
|
16
|
-
end
|
17
|
-
|
18
|
-
def fetch
|
19
|
-
@current_workspace.get
|
20
|
-
end
|
21
|
-
|
22
|
-
def start!
|
23
|
-
return if @running
|
24
|
-
|
25
|
-
poll
|
26
|
-
@task.execute
|
27
|
-
@running = true
|
28
|
-
end
|
29
|
-
|
30
|
-
def stop!
|
31
|
-
return unless @running
|
32
|
-
|
33
|
-
@task.shutdown
|
34
|
-
@running = false
|
35
|
-
end
|
36
|
-
|
37
|
-
def poll
|
38
|
-
workspace = @http_fetcher.fetch
|
39
|
-
@current_workspace.set(workspace)
|
40
|
-
rescue => e
|
41
|
-
@logger.error { "Failed to poll Workspace: #{e.inspect}" }
|
42
|
-
end
|
43
|
-
end
|
44
|
-
end
|
@@ -1,87 +0,0 @@
|
|
1
|
-
module Hackle
|
2
|
-
class Workspace
|
3
|
-
def initialize(experiments:, event_types:)
|
4
|
-
@experiments = experiments
|
5
|
-
@event_types = event_types
|
6
|
-
end
|
7
|
-
|
8
|
-
def get_experiment(experiment_key:)
|
9
|
-
@experiments[experiment_key]
|
10
|
-
end
|
11
|
-
|
12
|
-
def get_event_type(event_type_key:)
|
13
|
-
event_type = @event_types[event_type_key]
|
14
|
-
|
15
|
-
if event_type.nil?
|
16
|
-
EventType.undefined(key: event_type_key)
|
17
|
-
else
|
18
|
-
event_type
|
19
|
-
end
|
20
|
-
end
|
21
|
-
|
22
|
-
class << self
|
23
|
-
def create(data:)
|
24
|
-
buckets = Hash[data[:buckets].map { |b| [b[:id], bucket(b)] }]
|
25
|
-
running_experiments = Hash[data[:experiments].map { |re| [re[:key], running_experiment(re, buckets)] }]
|
26
|
-
completed_experiment = Hash[data[:completedExperiments].map { |ce| [ce[:experimentKey], completed_experiment(ce)] }]
|
27
|
-
event_types = Hash[data[:events].map { |e| [e[:key], event_type(e)] }]
|
28
|
-
experiments = running_experiments.merge(completed_experiment)
|
29
|
-
Workspace.new(
|
30
|
-
experiments: experiments,
|
31
|
-
event_types: event_types
|
32
|
-
)
|
33
|
-
end
|
34
|
-
|
35
|
-
private
|
36
|
-
|
37
|
-
def running_experiment(data, buckets)
|
38
|
-
Experiment::Running.new(
|
39
|
-
id: data[:id],
|
40
|
-
key: data[:key],
|
41
|
-
bucket: buckets[data[:bucketId]],
|
42
|
-
variations: Hash[data[:variations].map { |v| [v[:id], variation(v)] }],
|
43
|
-
user_overrides: Hash[data[:execution][:userOverrides].map { |u| [u[:userId], u[:variationId]] }]
|
44
|
-
)
|
45
|
-
end
|
46
|
-
|
47
|
-
def completed_experiment(data)
|
48
|
-
Experiment::Completed.new(
|
49
|
-
id: data[:experimentId],
|
50
|
-
key: data[:experimentKey],
|
51
|
-
winner_variation_key: data[:winnerVariationKey]
|
52
|
-
)
|
53
|
-
end
|
54
|
-
|
55
|
-
def variation(data)
|
56
|
-
Variation.new(
|
57
|
-
id: data[:id],
|
58
|
-
key: data[:key],
|
59
|
-
dropped: data[:status] == 'DROPPED'
|
60
|
-
)
|
61
|
-
end
|
62
|
-
|
63
|
-
def bucket(data)
|
64
|
-
Bucket.new(
|
65
|
-
seed: data[:seed],
|
66
|
-
slot_size: data[:slotSize],
|
67
|
-
slots: data[:slots].map { |s| slot(s) }
|
68
|
-
)
|
69
|
-
end
|
70
|
-
|
71
|
-
def slot(data)
|
72
|
-
Slot.new(
|
73
|
-
start_inclusive: data[:startInclusive],
|
74
|
-
end_exclusive: data[:endExclusive],
|
75
|
-
variation_id: data[:variationId]
|
76
|
-
)
|
77
|
-
end
|
78
|
-
|
79
|
-
def event_type(data)
|
80
|
-
EventType.new(
|
81
|
-
id: data[:id],
|
82
|
-
key: data[:key]
|
83
|
-
)
|
84
|
-
end
|
85
|
-
end
|
86
|
-
end
|
87
|
-
end
|