rox-rollout 0.1.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.circleci/config.yml +45 -0
- data/.gitignore +8 -0
- data/Gemfile +6 -0
- data/LICENSE +50 -0
- data/README_DEVELOP.md +19 -0
- data/Rakefile +16 -0
- data/_archive/.document +5 -0
- data/_archive/.rspec +1 -0
- data/_archive/Gemfile +15 -0
- data/_archive/Gemfile.lock +87 -0
- data/_archive/README.md +32 -0
- data/_archive/README.rdoc +19 -0
- data/_archive/Rakefile +50 -0
- data/_archive/lib/expr_function_definition.rb +52 -0
- data/_archive/lib/function_definition.rb +48 -0
- data/_archive/lib/function_token.rb +12 -0
- data/_archive/lib/object_extends.rb +12 -0
- data/_archive/lib/ruby_interpreter.rb +292 -0
- data/_archive/lib/stack.rb +48 -0
- data/_archive/lib/string_extends.rb +14 -0
- data/_archive/spec/ruby_interpreter_spec.rb +203 -0
- data/_archive/spec/spec_helper.rb +30 -0
- data/_archive/spec/stack_spec.rb +77 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/e2e/container.rb +38 -0
- data/e2e/custom_props.rb +55 -0
- data/e2e/rox_e2e_test.rb +159 -0
- data/e2e/test_vars.rb +24 -0
- data/lib/rox.rb +5 -0
- data/lib/rox/core/client/buid.rb +82 -0
- data/lib/rox/core/client/device_properties.rb +45 -0
- data/lib/rox/core/client/internal_flags.rb +20 -0
- data/lib/rox/core/client/sdk_settings.rb +5 -0
- data/lib/rox/core/configuration/configuration.rb +5 -0
- data/lib/rox/core/configuration/configuration_fetched_args.rb +23 -0
- data/lib/rox/core/configuration/configuration_fetched_invoker.rb +37 -0
- data/lib/rox/core/configuration/configuration_parser.rb +85 -0
- data/lib/rox/core/configuration/fetcher_error.rb +13 -0
- data/lib/rox/core/configuration/fetcher_status.rb +10 -0
- data/lib/rox/core/configuration/models/experiment_model.rb +5 -0
- data/lib/rox/core/configuration/models/target_group_model.rb +5 -0
- data/lib/rox/core/consts/build.rb +8 -0
- data/lib/rox/core/consts/environment.rb +42 -0
- data/lib/rox/core/consts/property_type.rb +29 -0
- data/lib/rox/core/context/merged_context.rb +16 -0
- data/lib/rox/core/core.rb +131 -0
- data/lib/rox/core/entities/flag.rb +26 -0
- data/lib/rox/core/entities/flag_setter.rb +39 -0
- data/lib/rox/core/entities/variant.rb +56 -0
- data/lib/rox/core/impression/impression_args.rb +5 -0
- data/lib/rox/core/impression/impression_invoker.rb +41 -0
- data/lib/rox/core/impression/models/experiment.rb +14 -0
- data/lib/rox/core/impression/models/reporting_value.rb +5 -0
- data/lib/rox/core/logging/logging.rb +17 -0
- data/lib/rox/core/logging/no_op_logger.rb +11 -0
- data/lib/rox/core/network/configuration_fetch_result.rb +5 -0
- data/lib/rox/core/network/configuration_fetcher.rb +38 -0
- data/lib/rox/core/network/configuration_fetcher_base.rb +25 -0
- data/lib/rox/core/network/configuration_fetcher_roxy.rb +29 -0
- data/lib/rox/core/network/configuration_source.rb +9 -0
- data/lib/rox/core/network/request.rb +46 -0
- data/lib/rox/core/network/request_configuration_builder.rb +48 -0
- data/lib/rox/core/network/request_data.rb +5 -0
- data/lib/rox/core/network/response.rb +16 -0
- data/lib/rox/core/properties/custom_property.rb +18 -0
- data/lib/rox/core/properties/custom_property_type.rb +18 -0
- data/lib/rox/core/properties/device_property.rb +11 -0
- data/lib/rox/core/register/registerer.rb +35 -0
- data/lib/rox/core/reporting/error_reporter.rb +152 -0
- data/lib/rox/core/repositories/custom_property_repository.rb +61 -0
- data/lib/rox/core/repositories/experiment_repository.rb +21 -0
- data/lib/rox/core/repositories/flag_repository.rb +49 -0
- data/lib/rox/core/repositories/roxx/experiments_extensions.rb +82 -0
- data/lib/rox/core/repositories/roxx/properties_extensions.rb +26 -0
- data/lib/rox/core/repositories/target_group_repository.rb +17 -0
- data/lib/rox/core/roxx/core_stack.rb +22 -0
- data/lib/rox/core/roxx/evaluation_result.rb +28 -0
- data/lib/rox/core/roxx/node.rb +11 -0
- data/lib/rox/core/roxx/parser.rb +143 -0
- data/lib/rox/core/roxx/regular_expression_extensions.rb +33 -0
- data/lib/rox/core/roxx/string_tokenizer.rb +68 -0
- data/lib/rox/core/roxx/symbols.rb +14 -0
- data/lib/rox/core/roxx/token_type.rb +30 -0
- data/lib/rox/core/roxx/tokenized_expression.rb +119 -0
- data/lib/rox/core/roxx/value_compare_extensions.rb +137 -0
- data/lib/rox/core/security/signature_verifier.rb +18 -0
- data/lib/rox/core/utils/periodic_task.rb +12 -0
- data/lib/rox/core/utils/type_utils.rb +9 -0
- data/lib/rox/server/client/sdk_settings.rb +5 -0
- data/lib/rox/server/client/server_properties.rb +20 -0
- data/lib/rox/server/flags/rox_flag.rb +19 -0
- data/lib/rox/server/flags/rox_variant.rb +8 -0
- data/lib/rox/server/logging/server_logger.rb +35 -0
- data/lib/rox/server/rox_options.rb +27 -0
- data/lib/rox/server/rox_server.rb +83 -0
- data/lib/rox/version.rb +3 -0
- data/rox.gemspec +29 -0
- metadata +184 -0
@@ -0,0 +1,16 @@
|
|
1
|
+
module Rox
|
2
|
+
module Core
|
3
|
+
class Response
|
4
|
+
attr_accessor :status_code, :text
|
5
|
+
|
6
|
+
def initialize(status_code, text)
|
7
|
+
@status_code = status_code
|
8
|
+
@text = text
|
9
|
+
end
|
10
|
+
|
11
|
+
def success_status_code?
|
12
|
+
@status_code >= 200 && @status_code < 300
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Rox
|
2
|
+
module Core
|
3
|
+
class CustomProperty
|
4
|
+
attr_accessor :name, :type
|
5
|
+
|
6
|
+
def initialize(name, type, value = nil, &block)
|
7
|
+
@name = name
|
8
|
+
@type = type
|
9
|
+
@value = value
|
10
|
+
@block = block
|
11
|
+
end
|
12
|
+
|
13
|
+
def value(context)
|
14
|
+
@block.nil? ? @value : @block.call(context)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Rox
|
2
|
+
module Core
|
3
|
+
class CustomPropertyType
|
4
|
+
attr_accessor :type, :external_type
|
5
|
+
|
6
|
+
def initialize(type, external_type)
|
7
|
+
@type = type
|
8
|
+
@external_type = external_type
|
9
|
+
end
|
10
|
+
|
11
|
+
STRING = CustomPropertyType.new('string', 'String')
|
12
|
+
BOOL = CustomPropertyType.new('bool', 'Boolean')
|
13
|
+
INT = CustomPropertyType.new('int', 'Number')
|
14
|
+
FLOAT = CustomPropertyType.new('double', 'Number')
|
15
|
+
SEMVER = CustomPropertyType.new('semver', 'Semver')
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Rox
|
2
|
+
module Core
|
3
|
+
class Registerer
|
4
|
+
def initialize(flag_repository)
|
5
|
+
@flag_repository = flag_repository
|
6
|
+
@namespaces = []
|
7
|
+
@mutex = Mutex.new
|
8
|
+
end
|
9
|
+
|
10
|
+
def register_instance(container, ns)
|
11
|
+
raise ArgumentError, 'A namespace cannot be null' if ns.nil?
|
12
|
+
|
13
|
+
@mutex.synchronize do
|
14
|
+
raise ArgumentError, "A container with the given namespace (#{ns}) has already been registered" if @namespaces.include?(ns)
|
15
|
+
end
|
16
|
+
|
17
|
+
@namespaces << ns
|
18
|
+
|
19
|
+
container.methods.each do |method_name|
|
20
|
+
method = container.method(method_name)
|
21
|
+
next unless method.arity.zero?
|
22
|
+
|
23
|
+
begin
|
24
|
+
value = method.call
|
25
|
+
if value.is_a?(Variant)
|
26
|
+
@flag_repository.add_flag(value, ns == '' ? method_name.to_s : "#{ns}.#{method_name}")
|
27
|
+
end
|
28
|
+
rescue StandardError
|
29
|
+
next
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,152 @@
|
|
1
|
+
require 'rox/core/logging/logging'
|
2
|
+
|
3
|
+
module Rox
|
4
|
+
module Core
|
5
|
+
class ErrorReporter
|
6
|
+
BUGSNAG_NOTIFY_URL = 'https://notify.bugsnag.com'.freeze
|
7
|
+
|
8
|
+
def initialize(request, device_properties, buid)
|
9
|
+
@request = request
|
10
|
+
@device_properties = device_properties
|
11
|
+
@buid = buid
|
12
|
+
end
|
13
|
+
|
14
|
+
def report(message, ex)
|
15
|
+
return if @device_properties.rollout_environment == 'LOCAL'
|
16
|
+
|
17
|
+
Logging.logger.error("Error report: #{message}", ex)
|
18
|
+
|
19
|
+
begin
|
20
|
+
stack_trace = ex.nil? ? caller : ex.backtrace
|
21
|
+
stack = parse_stack_trace(stack_trace)
|
22
|
+
payload = create_payload(message, ex, stack)
|
23
|
+
rescue StandardError => e
|
24
|
+
Logging.logger.error('failed to create bugsnag json payload of the error', e)
|
25
|
+
else
|
26
|
+
Thread.new { send_error(payload) }
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def send_error(payload)
|
31
|
+
Logging.logger.debug('Sending bugsnag error report...')
|
32
|
+
|
33
|
+
begin
|
34
|
+
@request.send_post(ErrorReporter::BUGSNAG_NOTIFY_URL, payload)
|
35
|
+
Logging.logger.debug('Bugsnag error report was sent')
|
36
|
+
rescue StandardError => ex
|
37
|
+
Logging.logger.error('Failed to send bugsnag error ', ex)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def create_payload(message, ex, stack)
|
42
|
+
payload = {}
|
43
|
+
add_api_key(payload)
|
44
|
+
add_notifier(payload)
|
45
|
+
add_events(message, ex, stack, payload)
|
46
|
+
|
47
|
+
payload
|
48
|
+
end
|
49
|
+
|
50
|
+
def add_metadata(message, ev)
|
51
|
+
inner_data = {
|
52
|
+
'message' => message,
|
53
|
+
'deviceId' => @device_properties.distinct_id,
|
54
|
+
'buid' => @buid.to_s
|
55
|
+
}
|
56
|
+
|
57
|
+
metadata = {
|
58
|
+
'data' => inner_data
|
59
|
+
}
|
60
|
+
|
61
|
+
ev['metaData'] = metadata
|
62
|
+
end
|
63
|
+
|
64
|
+
def add_api_key(payload)
|
65
|
+
payload['apiKey'] = '9569ec14f61546c6aa2a97856492bf4d'
|
66
|
+
end
|
67
|
+
|
68
|
+
def add_events(message, ex, stack, payload)
|
69
|
+
evs = []
|
70
|
+
add_event(message, ex, stack, evs)
|
71
|
+
payload['events'] = evs
|
72
|
+
end
|
73
|
+
|
74
|
+
def add_event(message, ex, stack, events)
|
75
|
+
ev = {}
|
76
|
+
add_payload_version(ev)
|
77
|
+
add_exceptions(message, ex, stack, ev)
|
78
|
+
add_user('id', @device_properties.rollout_key, ev)
|
79
|
+
add_metadata(message, ev)
|
80
|
+
add_app(ev)
|
81
|
+
|
82
|
+
events << ev
|
83
|
+
end
|
84
|
+
|
85
|
+
def add_payload_version(ev)
|
86
|
+
ev['payloadVersion'] = 2
|
87
|
+
end
|
88
|
+
|
89
|
+
def add_notifier(payload)
|
90
|
+
notifier = {
|
91
|
+
'name' => 'Rollout Ruby SDK',
|
92
|
+
'version' => @device_properties.lib_version
|
93
|
+
}
|
94
|
+
payload['notifier'] = notifier
|
95
|
+
end
|
96
|
+
|
97
|
+
def add_user(id, rollout_key, ev)
|
98
|
+
user = {
|
99
|
+
id => rollout_key
|
100
|
+
}
|
101
|
+
ev['user'] = user
|
102
|
+
end
|
103
|
+
|
104
|
+
def add_exceptions(message, ex, stack, ev)
|
105
|
+
exceptions = []
|
106
|
+
exception = {}
|
107
|
+
|
108
|
+
if ex.nil?
|
109
|
+
exception['errorClass'] = message
|
110
|
+
exception['message'] = message
|
111
|
+
exception['stacktrace'] = []
|
112
|
+
else
|
113
|
+
exception['errorClass'] = ex.message
|
114
|
+
exception['message'] = ex.message
|
115
|
+
exception['stacktrace'] = stack
|
116
|
+
end
|
117
|
+
|
118
|
+
exceptions.append(exception)
|
119
|
+
ev['exceptions'] = exceptions
|
120
|
+
end
|
121
|
+
|
122
|
+
def add_app(ev)
|
123
|
+
app = {
|
124
|
+
'releaseStage' => @device_properties.rollout_environment,
|
125
|
+
'version' => @device_properties.lib_version
|
126
|
+
}
|
127
|
+
ev['app'] = app
|
128
|
+
end
|
129
|
+
|
130
|
+
STACK_TRACE_LINE_REGEX = /^((?:[a-zA-Z]:)?[^:]+):(\d+)(?::in `([^']+)')?$/
|
131
|
+
|
132
|
+
def parse_stack_trace(stack_trace)
|
133
|
+
stack = []
|
134
|
+
stack_trace.each do |line|
|
135
|
+
match = line.match(STACK_TRACE_LINE_REGEX)
|
136
|
+
next if match.nil?
|
137
|
+
|
138
|
+
file = match[1]
|
139
|
+
line_str = match[2]
|
140
|
+
func = match[3]
|
141
|
+
stack << {
|
142
|
+
'file' => file,
|
143
|
+
'method' => func,
|
144
|
+
'lineNumber' => line_str.to_i,
|
145
|
+
'columnNumber' => 0
|
146
|
+
}
|
147
|
+
end
|
148
|
+
stack
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module Rox
|
2
|
+
module Core
|
3
|
+
class CustomPropertyRepository
|
4
|
+
def initialize
|
5
|
+
@custom_properties = {}
|
6
|
+
@property_added_handlers = []
|
7
|
+
@mutex = Mutex.new
|
8
|
+
@handlers_mutex = Mutex.new
|
9
|
+
end
|
10
|
+
|
11
|
+
def add_custom_property(custom_property)
|
12
|
+
return if custom_property.name.nil? || custom_property.name.empty?
|
13
|
+
|
14
|
+
@mutex.synchronize do
|
15
|
+
@custom_properties[custom_property.name] = custom_property
|
16
|
+
end
|
17
|
+
|
18
|
+
raise_property_added_event(custom_property)
|
19
|
+
end
|
20
|
+
|
21
|
+
def add_custom_property_if_not_exists(custom_property)
|
22
|
+
return if custom_property.name.nil? || custom_property.name.empty?
|
23
|
+
|
24
|
+
@mutex.synchronize do
|
25
|
+
return if @custom_properties.include?(custom_property.name)
|
26
|
+
end
|
27
|
+
|
28
|
+
add_custom_property(custom_property)
|
29
|
+
end
|
30
|
+
|
31
|
+
def custom_property(name)
|
32
|
+
@mutex.synchronize do
|
33
|
+
return @custom_properties[name]
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def all_custom_properties
|
38
|
+
@mutex.synchronize do
|
39
|
+
return @custom_properties.values
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def register_property_added_handler(&block)
|
44
|
+
@handlers_mutex.synchronize do
|
45
|
+
@property_added_handlers << block
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def raise_property_added_event(custom_property)
|
50
|
+
handlers = []
|
51
|
+
@handlers_mutex.synchronize do
|
52
|
+
handlers = @property_added_handlers.clone
|
53
|
+
end
|
54
|
+
|
55
|
+
handlers.each do |handler|
|
56
|
+
handler.call(custom_property)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Rox
|
2
|
+
module Core
|
3
|
+
class ExperimentRepository
|
4
|
+
def initialize
|
5
|
+
@experiments = []
|
6
|
+
end
|
7
|
+
|
8
|
+
def experiments=(experiments)
|
9
|
+
@experiments = experiments
|
10
|
+
end
|
11
|
+
|
12
|
+
def experiment_by_flag(flag_name)
|
13
|
+
@experiments.detect { |e| e.flags.include?(flag_name) }
|
14
|
+
end
|
15
|
+
|
16
|
+
def all_experiments
|
17
|
+
@experiments
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module Rox
|
2
|
+
module Core
|
3
|
+
class FlagRepository
|
4
|
+
def initialize
|
5
|
+
@variants = {}
|
6
|
+
@flag_added_handlers = []
|
7
|
+
@mutex = Mutex.new
|
8
|
+
@handlers_mutex = Mutex.new
|
9
|
+
end
|
10
|
+
|
11
|
+
def add_flag(variant, name)
|
12
|
+
variant.name = name if variant.name.nil? || variant.name.empty?
|
13
|
+
@mutex.synchronize do
|
14
|
+
@variants[name] = variant
|
15
|
+
end
|
16
|
+
raise_flag_added_event(variant)
|
17
|
+
end
|
18
|
+
|
19
|
+
def flag(name)
|
20
|
+
@mutex.synchronize do
|
21
|
+
return @variants[name]
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def all_flags
|
26
|
+
@mutex.synchronize do
|
27
|
+
return @variants.values
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def register_flag_added_handler(&block)
|
32
|
+
@handlers_mutex.synchronize do
|
33
|
+
@flag_added_handlers << block
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def raise_flag_added_event(flag)
|
38
|
+
handlers = []
|
39
|
+
@handlers_mutex.synchronize do
|
40
|
+
handlers = @flag_added_handlers.clone
|
41
|
+
end
|
42
|
+
|
43
|
+
handlers.each do |handler|
|
44
|
+
handler.call(flag)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
require 'digest'
|
2
|
+
|
3
|
+
module Rox
|
4
|
+
module Core
|
5
|
+
class ExperimentsExtensions
|
6
|
+
def initialize(parser, target_groups_repository, flags_repository, experiment_repository)
|
7
|
+
@parser = parser
|
8
|
+
@target_groups_repository = target_groups_repository
|
9
|
+
@flags_repository = flags_repository
|
10
|
+
@experiment_repository = experiment_repository
|
11
|
+
end
|
12
|
+
|
13
|
+
def extend
|
14
|
+
@parser.add_operator('mergeSeed') do |parser, stack, context|
|
15
|
+
seed1 = stack.pop
|
16
|
+
seed2 = stack.pop
|
17
|
+
stack.push("#{seed1}.#{seed2}")
|
18
|
+
end
|
19
|
+
|
20
|
+
@parser.add_operator('isInPercentage') do |parser, stack, context|
|
21
|
+
percentage = stack.pop
|
22
|
+
seed = stack.pop
|
23
|
+
|
24
|
+
percentage = percentage.is_a?(Numeric) ? percentage : percentage.to_s.to_f
|
25
|
+
|
26
|
+
bucket = bucket(seed)
|
27
|
+
stack.push(bucket <= percentage)
|
28
|
+
end
|
29
|
+
|
30
|
+
@parser.add_operator('isInPercentageRange') do |parser, stack, context|
|
31
|
+
percentage_low = stack.pop
|
32
|
+
percentage_high = stack.pop
|
33
|
+
seed = stack.pop
|
34
|
+
|
35
|
+
percentage_low = percentage_low.is_a?(Numeric) ? percentage_low : percentage_low.to_s.to_f
|
36
|
+
percentage_high = percentage_high.is_a?(Numeric) ? percentage_high : percentage_high.to_s.to_f
|
37
|
+
|
38
|
+
bucket = bucket(seed)
|
39
|
+
stack.push(percentage_low <= bucket && bucket < percentage_high)
|
40
|
+
end
|
41
|
+
|
42
|
+
@parser.add_operator('flagValue') do |parser, stack, context|
|
43
|
+
feature_flag_identifier = stack.pop
|
44
|
+
result = Flag::FLAG_FALSE_VALUE
|
45
|
+
variant = @flags_repository.flag(feature_flag_identifier)
|
46
|
+
|
47
|
+
if !variant.nil?
|
48
|
+
result = variant.value(context)
|
49
|
+
else
|
50
|
+
flags_experiment = @experiment_repository.experiment_by_flag(feature_flag_identifier)
|
51
|
+
|
52
|
+
if !flags_experiment.nil? && !flags_experiment.condition.nil? && !flags_experiment.condition.empty?
|
53
|
+
experiment_eval_result = parser.evaluate_expression(flags_experiment.condition, context).string_value
|
54
|
+
result = experiment_eval_result if !experiment_eval_result.nil? && !experiment_eval_result.empty?
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
stack.push(result)
|
59
|
+
end
|
60
|
+
|
61
|
+
@parser.add_operator('isInTargetGroup') do |parser, stack, context|
|
62
|
+
target_group_identifier = stack.pop
|
63
|
+
target_group = @target_groups_repository.target_group(target_group_identifier)
|
64
|
+
if target_group.nil?
|
65
|
+
stack.push(false)
|
66
|
+
else
|
67
|
+
stack.push(parser.evaluate_expression(target_group.condition, context).bool_value)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def bucket(seed)
|
73
|
+
bytes = Digest::MD5.digest(seed).bytes
|
74
|
+
hash = (bytes[0] & 0xFF) | ((bytes[1] & 0xFF) << 8) | ((bytes[2] & 0xFF) << 16) | ((bytes[3] & 0xFF) << 24)
|
75
|
+
hash &= 0xFFFFFFFF
|
76
|
+
bucket = hash * 1.0 / (2**32 - 1)
|
77
|
+
|
78
|
+
bucket == 1 ? 0 : bucket
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|