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.
Files changed (92) hide show
  1. checksums.yaml +4 -4
  2. data/lib/hackle/client.rb +191 -79
  3. data/lib/hackle/config.rb +59 -17
  4. data/lib/hackle/decision.rb +113 -0
  5. data/lib/hackle/event.rb +89 -0
  6. data/lib/hackle/internal/clock/clock.rb +47 -0
  7. data/lib/hackle/internal/concurrent/executors.rb +20 -0
  8. data/lib/hackle/internal/concurrent/schedule/scheduler.rb +12 -0
  9. data/lib/hackle/internal/concurrent/schedule/timer_scheduler.rb +30 -0
  10. data/lib/hackle/internal/config/parameter_config.rb +50 -0
  11. data/lib/hackle/internal/core/hackle_core.rb +182 -0
  12. data/lib/hackle/internal/evaluation/bucketer/bucketer.rb +46 -0
  13. data/lib/hackle/internal/evaluation/evaluator/contextual/contextual_evaluator.rb +29 -0
  14. data/lib/hackle/internal/evaluation/evaluator/delegating/delegating_evaluator.rb +26 -0
  15. data/lib/hackle/internal/evaluation/evaluator/evaluator.rb +117 -0
  16. data/lib/hackle/internal/evaluation/evaluator/experiment/experiment_evaluation_flow_factory.rb +67 -0
  17. data/lib/hackle/internal/evaluation/evaluator/experiment/experiment_evaluator.rb +172 -0
  18. data/lib/hackle/internal/evaluation/evaluator/experiment/experiment_flow_evaluator.rb +241 -0
  19. data/lib/hackle/internal/evaluation/evaluator/experiment/experiment_resolver.rb +166 -0
  20. data/lib/hackle/internal/evaluation/evaluator/remoteconfig/remote_config_determiner.rb +48 -0
  21. data/lib/hackle/internal/evaluation/evaluator/remoteconfig/remote_config_evaluator.rb +174 -0
  22. data/lib/hackle/internal/evaluation/flow/evaluation_flow.rb +49 -0
  23. data/lib/hackle/internal/evaluation/flow/flow_evaluator.rb +11 -0
  24. data/lib/hackle/internal/evaluation/match/condition/condition_matcher.rb +11 -0
  25. data/lib/hackle/internal/evaluation/match/condition/condition_matcher_factory.rb +53 -0
  26. data/lib/hackle/internal/evaluation/match/condition/experiment/experiment_condition_matcher.rb +29 -0
  27. data/lib/hackle/internal/evaluation/match/condition/experiment/experiment_evaluator_matcher.rb +135 -0
  28. data/lib/hackle/internal/evaluation/match/condition/segment/segment_condition_matcher.rb +67 -0
  29. data/lib/hackle/internal/evaluation/match/condition/user/user_condition_matcher.rb +44 -0
  30. data/lib/hackle/internal/evaluation/match/operator/operator_matcher.rb +185 -0
  31. data/lib/hackle/internal/evaluation/match/operator/operator_matcher_factory.rb +31 -0
  32. data/lib/hackle/internal/evaluation/match/target/target_matcher.rb +31 -0
  33. data/lib/hackle/internal/evaluation/match/value/value_matcher.rb +96 -0
  34. data/lib/hackle/internal/evaluation/match/value/value_matcher_factory.rb +28 -0
  35. data/lib/hackle/internal/evaluation/match/value/value_operator_matcher.rb +59 -0
  36. data/lib/hackle/internal/event/user_event.rb +187 -0
  37. data/lib/hackle/internal/event/user_event_dispatcher.rb +156 -0
  38. data/lib/hackle/internal/event/user_event_factory.rb +58 -0
  39. data/lib/hackle/internal/event/user_event_processor.rb +181 -0
  40. data/lib/hackle/internal/http/http.rb +28 -0
  41. data/lib/hackle/internal/http/http_client.rb +48 -0
  42. data/lib/hackle/internal/identifiers/identifier_builder.rb +67 -0
  43. data/lib/hackle/internal/logger/logger.rb +31 -0
  44. data/lib/hackle/internal/model/action.rb +57 -0
  45. data/lib/hackle/internal/model/bucket.rb +58 -0
  46. data/lib/hackle/internal/model/container.rb +47 -0
  47. data/lib/hackle/internal/model/decision_reason.rb +31 -0
  48. data/lib/hackle/internal/model/event_type.rb +19 -0
  49. data/lib/hackle/internal/model/experiment.rb +194 -0
  50. data/lib/hackle/internal/model/parameter_configuration.rb +19 -0
  51. data/lib/hackle/internal/model/remote_config_parameter.rb +76 -0
  52. data/lib/hackle/internal/model/sdk.rb +23 -0
  53. data/lib/hackle/internal/model/segment.rb +61 -0
  54. data/lib/hackle/internal/model/target.rb +203 -0
  55. data/lib/hackle/internal/model/target_rule.rb +19 -0
  56. data/lib/hackle/internal/model/targeting.rb +45 -0
  57. data/lib/hackle/internal/model/value_type.rb +75 -0
  58. data/lib/hackle/internal/model/variation.rb +27 -0
  59. data/lib/hackle/internal/model/version.rb +153 -0
  60. data/lib/hackle/internal/properties/properties_builder.rb +101 -0
  61. data/lib/hackle/internal/user/hackle_user.rb +74 -0
  62. data/lib/hackle/internal/user/hackle_user_resolver.rb +27 -0
  63. data/lib/hackle/internal/workspace/http_workspace_fetcher.rb +50 -0
  64. data/lib/hackle/internal/workspace/polling_workspace_fetcher.rb +62 -0
  65. data/lib/hackle/internal/workspace/workspace.rb +353 -0
  66. data/lib/hackle/internal/workspace/workspace_fetcher.rb +18 -0
  67. data/lib/hackle/remote_config.rb +55 -0
  68. data/lib/hackle/user.rb +124 -0
  69. data/lib/hackle/version.rb +1 -11
  70. data/lib/hackle.rb +4 -32
  71. metadata +123 -51
  72. data/.gitignore +0 -11
  73. data/.rspec +0 -2
  74. data/.travis.yml +0 -7
  75. data/Gemfile +0 -6
  76. data/README.md +0 -33
  77. data/Rakefile +0 -6
  78. data/hackle-ruby-sdk.gemspec +0 -29
  79. data/lib/hackle/decision/bucketer.rb +0 -30
  80. data/lib/hackle/decision/decider.rb +0 -54
  81. data/lib/hackle/events/event.rb +0 -33
  82. data/lib/hackle/events/event_dispatcher.rb +0 -89
  83. data/lib/hackle/events/event_processor.rb +0 -115
  84. data/lib/hackle/http/http.rb +0 -37
  85. data/lib/hackle/models/bucket.rb +0 -15
  86. data/lib/hackle/models/event_type.rb +0 -14
  87. data/lib/hackle/models/experiment.rb +0 -36
  88. data/lib/hackle/models/slot.rb +0 -15
  89. data/lib/hackle/models/variation.rb +0 -11
  90. data/lib/hackle/workspaces/http_workspace_fetcher.rb +0 -24
  91. data/lib/hackle/workspaces/polling_workspace_fetcher.rb +0 -44
  92. 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
@@ -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
@@ -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,14 +0,0 @@
1
- module Hackle
2
- class EventType
3
- attr_reader :id, :key
4
-
5
- def initialize(id:, key:)
6
- @id = id
7
- @key = key
8
- end
9
-
10
- def self.undefined(key:)
11
- EventType.new(id: 0, key: key)
12
- end
13
- end
14
- 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
@@ -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,11 +0,0 @@
1
- module Hackle
2
- class Variation
3
- attr_reader :id, :key, :dropped
4
-
5
- def initialize(id:, key:, dropped:)
6
- @id = id
7
- @key = key
8
- @dropped = dropped
9
- end
10
- end
11
- 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