hackle-ruby-sdk 0.1.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.
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