splitclient-rb 3.3.0 → 4.0.0.pre.rc1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 63050dc6d8209d6fdd050462bbafabc650e74fc7
4
- data.tar.gz: cbb40be59c858289ae351c6115e147a7062362d1
3
+ metadata.gz: c6868ad86ef9ff027cff7dbdb58cfa973c7d02d2
4
+ data.tar.gz: 254804356acd6e57dedd0f01fcec6549374651e7
5
5
  SHA512:
6
- metadata.gz: 3e9be02c6bb231fce90773edac53d39af03c634012a39dd96ac6480009836c9e34102b8651b8b55f6449a967a0a06292b89b4186ada8e62ac81b3c158956d1de
7
- data.tar.gz: a1875dd25ac3602f5660224dd5a890a86c058820c08a716e32c9c29bbdedc35ea26fce9e47977fa5224a382f93cea3140973e0bf8b21be99b4f9f5d6c82ff7be
6
+ metadata.gz: 2d6f2d5097557c0eda262290c9d80a6a031e1c2b7f4acab2dc3fb163113136241435ccbbe137cd5d5e63f37d46dcea2c9514d0269a792f3ff7f4ea9359db8bc3
7
+ data.tar.gz: 7a164d6e280b8223108d418392d7051ef2b9f7b3d78389aea46d772180b35af68264cdc3e0943f2634c1ccac0a4b1cfa9fbaf243befc3058a39426193d8b0f0d
data/CHANGES.txt CHANGED
@@ -1,3 +1,7 @@
1
+ 4.0.0
2
+ - Add support for murmur3 algo
3
+ - Optimize memory usage
4
+
1
5
  3.3.0
2
6
  - Add support for traffic allocation
3
7
 
data/Detailed-README.md CHANGED
@@ -467,6 +467,14 @@ This will generate a file gemspec with the right version, then:
467
467
  gem push splitclient-rb-<VERSION>.gem
468
468
  ```
469
469
 
470
+ ## Benchmarking
471
+
472
+ To benchmark hashing algorithms (currently we're using MurmurHash) you'll need to run:
473
+
474
+ ```bash
475
+ bundle exec rake benchmark_hashing_algorithm
476
+ ```
477
+
470
478
  ## Contributing
471
479
 
472
480
  Bug reports and pull requests are welcome on GitHub at https://github.com/splitio/ruby-client.
data/NEWS CHANGED
@@ -1,3 +1,8 @@
1
+ 4.0.0
2
+
3
+ Add support for murmur3 hashing algorithm
4
+ Optimize gem memory usage
5
+
1
6
  3.3.0
2
7
 
3
8
  Add support for traffic allocation
@@ -6,7 +6,7 @@ module SplitIoClient
6
6
  module MemoryAdapters
7
7
  # Memory adapter implementation, which stores everything inside thread-safe Map
8
8
  class MapAdapter
9
- def initialize(_ = nil)
9
+ def initialize
10
10
  @map = Concurrent::Map.new
11
11
  end
12
12
 
@@ -1,110 +1,118 @@
1
+ require 'digest/murmurhash'
2
+
1
3
  module SplitIoClient
2
4
  # Misc class in charge of providing hash functions and
3
5
  # determination of treatment based on concept of buckets
4
6
  # based on provided key
5
7
  #
6
8
  class Splitter < NoMethodError
7
-
8
- #
9
- # Checks if the partiotion size is 100%
10
- #
11
- # @param partitions [object] array of partitions
12
- #
13
- # @return [boolean] true if partition is 100% false otherwise
14
- def self.hundred_percent_one_treatment?(partitions)
15
- if partitions.size != 1
16
- return false
9
+ class << self
10
+ #
11
+ # Checks if the partiotion size is 100%
12
+ #
13
+ # @param partitions [object] array of partitions
14
+ #
15
+ # @return [boolean] true if partition is 100% false otherwise
16
+ def hundred_percent_one_treatment?(partitions)
17
+ (partitions.size != 1) ? false : (partitions.first.size == 100)
17
18
  end
18
- return (partitions.first).size == 100
19
- end
20
19
 
20
+ #
21
+ # gets the appropriate treatment based on id, seed and partition value
22
+ #
23
+ # @param id [string] user key
24
+ # @param seed [number] seed for the user key
25
+ # @param partitions [object] array of partitions
26
+ #
27
+ # @return traetment [object] treatment value
28
+ def get_treatment(id, seed, partitions, legacy_algo)
29
+ legacy = (legacy_algo == 1 || legacy_algo == nil) ? true : false
21
30
 
22
- #
23
- # gets the appropriate treatment based on id, seed and partition value
24
- #
25
- # @param id [string] user key
26
- # @param seed [number] seed for the user key
27
- # @param partitions [object] array of partitions
28
- #
29
- # @return traetment [object] treatment value
30
- def self.get_treatment(id, seed, partitions)
31
- if partitions.empty?
32
- return Treatments::CONTROL
33
- end
31
+ if partitions.empty?
32
+ return Treatments::CONTROL
33
+ end
34
34
 
35
- if hundred_percent_one_treatment?(partitions)
36
- return (partitions.first).treatment
37
- end
35
+ if hundred_percent_one_treatment?(partitions)
36
+ return (partitions.first).treatment
37
+ end
38
38
 
39
- return get_treatment_for_key(bucket(hash(id, seed)), partitions)
40
- end
39
+ return get_treatment_for_key(bucket(count_hash(id, seed, legacy_algo)), partitions)
40
+ end
41
41
 
42
- #
43
- # returns a hash value for the give key, sedd pair
44
- #
45
- # @param key [string] user key
46
- # @param seed [number] seed for the user key
47
- #
48
- # @return hash [string] hash value
49
- def self.hash(key, seed)
50
- h = 0
51
- for i in 0..key.length-1
52
- h = to_int32(31 * h + key[i].ord)
42
+ # returns a hash value for the give key, seed pair
43
+ #
44
+ # @param key [String] user key
45
+ # @param seed [Fixnum] seed for the user key
46
+ #
47
+ # @return hash [String] hash value
48
+ def count_hash(key, seed, legacy)
49
+ legacy ? legacy_hash(key, seed) : murmur_hash(key, seed)
53
50
  end
54
- h^seed
55
- end
56
51
 
57
- #
58
- # misc method to convert ruby number to int 32 since overflow is handled different to java
59
- #
60
- # @param number [number] ruby number value
61
- #
62
- # @return [int] returns the int 32 value of the provided number
63
- def self.to_int32(number)
64
- begin
65
- sign = number < 0 ? -1 : 1
66
- abs = number.abs
67
- return 0 if abs == 0 || abs == Float::INFINITY
68
- rescue
69
- return 0
52
+ def murmur_hash(key, seed)
53
+ Digest::MurmurHash3_x86_32.rawdigest(key, [seed].pack('L'))
70
54
  end
71
55
 
72
- pos_int = sign * abs.floor
73
- int_32bit = pos_int % 2**32
56
+ def legacy_hash(key, seed)
57
+ h = 0
74
58
 
75
- return int_32bit - 2**32 if int_32bit >= 2**31
76
- int_32bit
77
- end
59
+ for i in 0..key.length-1
60
+ h = to_int32(31 * h + key[i].ord)
61
+ end
78
62
 
79
- #
80
- # returns the treatment for a bucket given the partitions
81
- #
82
- # @param bucket [number] bucket value
83
- # @param parittions [object] array of partitions
84
- #
85
- # @return treatment [treatment] treatment value for this bucket and partitions
86
- def self.get_treatment_for_key(bucket, partitions)
87
- buckets_covered_thus_far = 0
88
- partitions.each do |p|
89
- unless p.is_empty?
90
- buckets_covered_thus_far += p.size
91
- return p.treatment if buckets_covered_thus_far >= bucket
63
+ h^seed
64
+ end
65
+
66
+ #
67
+ # misc method to convert ruby number to int 32 since overflow is handled different to java
68
+ #
69
+ # @param number [number] ruby number value
70
+ #
71
+ # @return [int] returns the int 32 value of the provided number
72
+ def to_int32(number)
73
+ begin
74
+ sign = number < 0 ? -1 : 1
75
+ abs = number.abs
76
+ return 0 if abs == 0 || abs == Float::INFINITY
77
+ rescue
78
+ return 0
92
79
  end
80
+
81
+ pos_int = sign * abs.floor
82
+ int_32bit = pos_int % 2**32
83
+
84
+ return int_32bit - 2**32 if int_32bit >= 2**31
85
+ int_32bit
93
86
  end
94
87
 
95
- return Treatments::CONTROL
96
- end
88
+ #
89
+ # returns the treatment for a bucket given the partitions
90
+ #
91
+ # @param bucket [number] bucket value
92
+ # @param parittions [object] array of partitions
93
+ #
94
+ # @return treatment [treatment] treatment value for this bucket and partitions
95
+ def get_treatment_for_key(bucket, partitions)
96
+ buckets_covered_thus_far = 0
97
+ partitions.each do |p|
98
+ unless p.is_empty?
99
+ buckets_covered_thus_far += p.size
100
+ return p.treatment if buckets_covered_thus_far >= bucket
101
+ end
102
+ end
97
103
 
98
- #
99
- # returns bucket value for the given hash value
100
- #
101
- # @param hash_value [string] hash value
102
- #
103
- # @return bucket [number] bucket number
104
- def self.bucket(hash_value)
105
- (hash_value.abs % 100) + 1
106
- end
104
+ return Treatments::CONTROL
105
+ end
107
106
 
107
+ #
108
+ # returns bucket value for the given hash value
109
+ #
110
+ # @param hash_value [string] hash value
111
+ #
112
+ # @return bucket [number] bucket number
113
+ def bucket(hash_value)
114
+ (hash_value.abs % 100) + 1
115
+ end
116
+ end
108
117
  end
109
-
110
118
  end
@@ -23,6 +23,7 @@ module SplitIoClient
23
23
  def match(split, keys, attributes)
24
24
  in_rollout = false
25
25
  key = keys[:bucketing_key] ? keys[:bucketing_key] : keys[:matching_key]
26
+ legacy_algo = (split[:algo] == 1 || split[:algo] == nil) ? true : false
26
27
 
27
28
  split[:conditions].each do |c|
28
29
  condition = SplitIoClient::Condition.new(c)
@@ -31,7 +32,7 @@ module SplitIoClient
31
32
 
32
33
  if !in_rollout && condition.type == SplitIoClient::Condition::TYPE_ROLLOUT
33
34
  if split[:trafficAllocation] < 100
34
- bucket = Splitter.bucket(Splitter.hash(key, split[:trafficAllocationSeed].to_i))
35
+ bucket = Splitter.bucket(Splitter.count_hash(key, split[:trafficAllocationSeed].to_i, legacy_algo))
35
36
 
36
37
  if bucket >= split[:trafficAllocation]
37
38
  return treatment(Models::Label::NOT_IN_SPLIT, @default_treatment, split[:changeNumber])
@@ -42,7 +43,8 @@ module SplitIoClient
42
43
  end
43
44
 
44
45
  if matcher_type(condition).match?(keys[:matching_key], attributes)
45
- result = Splitter.get_treatment(key, split[:seed], condition.partitions)
46
+ key = keys[:bucketing_key] ? keys[:bucketing_key] : keys[:matching_key]
47
+ result = Splitter.get_treatment(key, split[:seed], condition.partitions, split[:algo])
46
48
 
47
49
  if result.nil?
48
50
  return treatment(Models::Label::NO_RULE_MATCHED, @default_treatment, split[:changeNumber])
@@ -1,6 +1,5 @@
1
1
  module SplitIoClient
2
2
  class SplitClient
3
-
4
3
  #
5
4
  # Creates a new split client instance that connects to split.io API.
6
5
  #
@@ -79,7 +78,7 @@ module SplitIoClient
79
78
 
80
79
  store_impression(
81
80
  split_name, matching_key, bucketing_key,
82
- { treatment: SplitIoClient::Treatments::CONTROL, label: Models::Label::EXCEPTION },
81
+ { treatment: SplitIoClient::Treatments::CONTROL, label: Engine::Models::Label::EXCEPTION },
83
82
  store_impressions
84
83
  )
85
84
 
@@ -98,7 +97,7 @@ module SplitIoClient
98
97
 
99
98
  store_impression(
100
99
  split_name, matching_key, bucketing_key,
101
- { treatment: SplitIoClient::Treatments::CONTROL, label: Models::Label::EXCEPTION },
100
+ { treatment: SplitIoClient::Treatments::CONTROL, label: Engine::Models::Label::EXCEPTION },
102
101
  store_impressions
103
102
  )
104
103
 
@@ -141,5 +140,11 @@ module SplitIoClient
141
140
  treatment_label_change_number[:treatment]
142
141
  end
143
142
  end
143
+
144
+ private
145
+
146
+ def split_treatment
147
+ @split_treatment ||= SplitIoClient::Engine::Parser::SplitTreatment.new(@segments_repository)
148
+ end
144
149
  end
145
150
  end
@@ -185,12 +185,7 @@ module SplitIoClient
185
185
  def self.init_cache_adapter(adapter, data_structure, redis_url = nil, impressions_queue_size = nil)
186
186
  case adapter
187
187
  when :memory
188
- # takes :memory_adapter (symbol) and returns MemoryAdapter (string)
189
- adapter = SplitIoClient::Cache::Adapters::MemoryAdapters.const_get(
190
- data_structure.to_s.split('_').collect(&:capitalize).join
191
- ).new(impressions_queue_size)
192
-
193
- SplitIoClient::Cache::Adapters::MemoryAdapter.new(adapter)
188
+ SplitIoClient::Cache::Adapters::MemoryAdapter.new(map_memory_adapter(data_structure, impressions_queue_size))
194
189
  when :redis
195
190
  begin
196
191
  require 'redis'
@@ -202,6 +197,15 @@ module SplitIoClient
202
197
  end
203
198
  end
204
199
 
200
+ def self.map_memory_adapter(name, queue_size)
201
+ case name
202
+ when :map_adapter
203
+ SplitIoClient::Cache::Adapters::MemoryAdapters::MapAdapter.new
204
+ when :queue_adapter
205
+ SplitIoClient::Cache::Adapters::MemoryAdapters::QueueAdapter.new(queue_size)
206
+ end
207
+ end
208
+
205
209
  def self.default_mode
206
210
  :standalone
207
211
  end
@@ -1,3 +1,3 @@
1
1
  module SplitIoClient
2
- VERSION = '3.3.0'
2
+ VERSION = '4.0.0-rc1'
3
3
  end
@@ -27,6 +27,7 @@ Gem::Specification.new do |spec|
27
27
  spec.add_development_dependency "pry"
28
28
  spec.add_development_dependency "pry-byebug"
29
29
  spec.add_development_dependency "simplecov"
30
+ spec.add_development_dependency "allocation_stats"
30
31
 
31
32
  spec.add_runtime_dependency "json", "~> 1.8"
32
33
  spec.add_runtime_dependency "thread_safe"
@@ -36,4 +37,5 @@ Gem::Specification.new do |spec|
36
37
  spec.add_runtime_dependency "faraday_middleware"
37
38
  spec.add_runtime_dependency "net-http-persistent", "<= 2.9.4"
38
39
  spec.add_runtime_dependency "redis"
40
+ spec.add_runtime_dependency "digest-murmurhash"
39
41
  end
@@ -0,0 +1,51 @@
1
+ require 'benchmark'
2
+ require 'digest/murmurhash'
3
+
4
+ desc 'Benchmark murmur32 hashing algorithm'
5
+
6
+ task :benchmark_hashing_algorithm do
7
+ iterations = 200_000
8
+ key = SecureRandom.uuid
9
+
10
+ Benchmark.bmbm do |x|
11
+ x.report('MurmurHash1') do
12
+ iterations.times { Digest::MurmurHash1.rawdigest(key) }
13
+ end
14
+
15
+ x.report('MurmurHash2') do
16
+ iterations.times { Digest::MurmurHash2.rawdigest(key) }
17
+ end
18
+
19
+ x.report('MurmurHash2A') do
20
+ iterations.times { Digest::MurmurHash2A.rawdigest(key) }
21
+ end
22
+
23
+ x.report('LegacyHash') do
24
+ iterations.times { legacy_hash(key, 123) }
25
+ end
26
+ end
27
+ end
28
+
29
+ def legacy_hash(key, seed)
30
+ h = 0
31
+ for i in 0..key.length-1
32
+ h = to_int32(31 * h + key[i].ord)
33
+ end
34
+ h^seed
35
+ end
36
+
37
+ def to_int32(number)
38
+ begin
39
+ sign = number < 0 ? -1 : 1
40
+ abs = number.abs
41
+ return 0 if abs == 0 || abs == Float::INFINITY
42
+ rescue
43
+ return 0
44
+ end
45
+
46
+ pos_int = sign * abs.floor
47
+ int_32bit = pos_int % 2**32
48
+
49
+ return int_32bit - 2**32 if int_32bit >= 2**31
50
+ int_32bit
51
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: splitclient-rb
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.3.0
4
+ version: 4.0.0.pre.rc1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Split Software
@@ -122,6 +122,20 @@ dependencies:
122
122
  - - ">="
123
123
  - !ruby/object:Gem::Version
124
124
  version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: allocation_stats
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
125
139
  - !ruby/object:Gem::Dependency
126
140
  name: json
127
141
  requirement: !ruby/object:Gem::Requirement
@@ -234,6 +248,20 @@ dependencies:
234
248
  - - ">="
235
249
  - !ruby/object:Gem::Version
236
250
  version: '0'
251
+ - !ruby/object:Gem::Dependency
252
+ name: digest-murmurhash
253
+ requirement: !ruby/object:Gem::Requirement
254
+ requirements:
255
+ - - ">="
256
+ - !ruby/object:Gem::Version
257
+ version: '0'
258
+ type: :runtime
259
+ prerelease: false
260
+ version_requirements: !ruby/object:Gem::Requirement
261
+ requirements:
262
+ - - ">="
263
+ - !ruby/object:Gem::Version
264
+ version: '0'
237
265
  description: Ruby client for using split SDK.
238
266
  email:
239
267
  - pato@split.io
@@ -313,6 +341,7 @@ files:
313
341
  - splitclient-rb.gemspec
314
342
  - splitio.yml.example
315
343
  - tasks/benchmark_get_treatment.rake
344
+ - tasks/benchmark_hashing_algorithm.rake
316
345
  - tasks/console.rake
317
346
  - tasks/rspec.rake
318
347
  homepage: https://github.com/splitio/ruby-client
@@ -330,9 +359,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
330
359
  version: '0'
331
360
  required_rubygems_version: !ruby/object:Gem::Requirement
332
361
  requirements:
333
- - - ">="
362
+ - - ">"
334
363
  - !ruby/object:Gem::Version
335
- version: '0'
364
+ version: 1.3.1
336
365
  requirements: []
337
366
  rubyforge_project:
338
367
  rubygems_version: 2.5.2