consistent_random 2.1.1 → 2.3.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +20 -0
- data/README.md +51 -1
- data/VERSION +1 -1
- data/consistent_random.gemspec +1 -1
- data/lib/consistent_random/active_job.rb +9 -7
- data/lib/consistent_random/testing.rb +169 -0
- data/lib/consistent_random.rb +40 -12
- metadata +5 -8
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1fd62d51e962f285e32463d2b1854f22a3d7fee49e7cb6f44cb3f09dc6af9654
|
|
4
|
+
data.tar.gz: f2dd72200a420d2b36dbae1336f477cafe971993772b0d5afac5a269856a3a7e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 56111c5d7231bf39fadca5ef39f1bef04b23df47962646774355f18c9a543dfec3f74eb5e3ad0a32acd0db66d684f8852eb2161cca4c4d5bb5a10fe729726103
|
|
7
|
+
data.tar.gz: 8daa15435772ad90dcb34c1797edaece7dcf78c34f088a532b843fa9c371f062fb9cd969f99d8afe40702f11c8bcbd6f439d6efc71ab60a1b50aa72f92cfcf97
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,26 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
5
5
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## 2.3.0
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- `ConsistentRandom#rand` can no longer return an out of range value. Previously there was a very small chance (about 1 in 2⁵³) that a seed could produce a value of exactly 1.0, causing `rand` to return 1.0, `rand(n)` to return `n`, and `rand(range)` to return a value above the range. Random floats are now derived from the top 53 bits of the seed, guaranteeing a uniform value in `[0, 1)`. **Note:** this changes the exact values generated for existing seeds (the distribution is unchanged), so percentage rollouts in flight during an upgrade may reassign.
|
|
12
|
+
- Fixed distribution bias in `ConsistentRandom#rand` for ranges with negative bounds. Values were truncated toward zero instead of floored, so for a range like `-5..5` the minimum value was never returned and zero was returned twice as often as other values.
|
|
13
|
+
- `ConsistentRandom#rand` now raises an `ArgumentError` for empty ranges (e.g. `5..1`) and non-numeric ranges instead of returning out of range values or raising a `NoMethodError`.
|
|
14
|
+
- `ConsistentRandom::Testing#bytes` now raises an `ArgumentError` for empty strings, which were previously accepted but silently ignored, producing non-deterministic values in tests.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- `ConsistentRandom.current_seed` is now public API; it can be used to propagate a scope into threads or fibers, which do not inherit the fiber-local scope state.
|
|
19
|
+
|
|
20
|
+
## 2.2.0
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
|
|
24
|
+
- Added `ConsistentRandom.testing` method to allow for deterministic testing of random values.
|
|
25
|
+
- Added `ConsistentRandom#name` method to return the name used for seeding the random value.
|
|
26
|
+
|
|
7
27
|
## 2.1.1
|
|
8
28
|
|
|
9
29
|
### Fixed
|
data/README.md
CHANGED
|
@@ -14,10 +14,12 @@ For example, consider rolling out a new feature to a subset of requests. You may
|
|
|
14
14
|
|
|
15
15
|
## Table of Contents
|
|
16
16
|
- [Usage](#usage)
|
|
17
|
+
- [Threads and Fibers](#threads-and-fibers)
|
|
17
18
|
- [Middlewares](#middlewares)
|
|
18
19
|
- [Rack Middleware](#rack-middleware)
|
|
19
20
|
- [Sidekiq Middleware](#sidekiq-middleware)
|
|
20
21
|
- [ActiveJob](#activejob)
|
|
22
|
+
- [Testing](#testing)
|
|
21
23
|
- [Installation](#installation)
|
|
22
24
|
- [Contributing](#contributing)
|
|
23
25
|
- [License](#license)
|
|
@@ -77,6 +79,24 @@ random = ConsistentRandom.new("foobar")
|
|
|
77
79
|
random.rand != random.rand # => true
|
|
78
80
|
```
|
|
79
81
|
|
|
82
|
+
### Threads and Fibers
|
|
83
|
+
|
|
84
|
+
Scopes are stored in fiber-local state, so they do not propagate to threads or fibers spawned inside the scope block. Code running in a `Thread.new` block, an `Enumerator` (which runs on its own fiber), or Rails' `load_async` will not see the surrounding scope and will generate independent random values.
|
|
85
|
+
|
|
86
|
+
If you need consistent values in spawned threads or fibers, capture the current seed and re-establish the scope inside them:
|
|
87
|
+
|
|
88
|
+
```ruby
|
|
89
|
+
ConsistentRandom.scope do
|
|
90
|
+
seed = ConsistentRandom.current_seed
|
|
91
|
+
|
|
92
|
+
Thread.new do
|
|
93
|
+
ConsistentRandom.scope(seed) do
|
|
94
|
+
# Values here are consistent with the parent scope.
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
```
|
|
99
|
+
|
|
80
100
|
## Middlewares
|
|
81
101
|
|
|
82
102
|
The gem provides built-in middlewares for Rack, Sidekiq, and ActiveJob. These middlewares allow you to automatically scope web requests and propagate consistent random values from the original request to asynchronous jobs.
|
|
@@ -138,7 +158,7 @@ Sidekiq.configure_client do |config|
|
|
|
138
158
|
end
|
|
139
159
|
```
|
|
140
160
|
|
|
141
|
-
Consistent random values will be propagated from the original request to any Sidekiq jobs so you will get consistent behavior on any
|
|
161
|
+
Consistent random values will be propagated from the original request to any Sidekiq jobs so you will get consistent behavior on any asynchronous jobs. You can disable this behavior on a job by setting the `consistent_random` sidekiq option to `false`:
|
|
142
162
|
|
|
143
163
|
```ruby
|
|
144
164
|
class MyWorker
|
|
@@ -206,6 +226,36 @@ class MyJob < ApplicationJob
|
|
|
206
226
|
end
|
|
207
227
|
```
|
|
208
228
|
|
|
229
|
+
## Testing
|
|
230
|
+
|
|
231
|
+
The gem provides a `ConsistentRandom.testing` method to allow for deterministic testing of random values. This method can be used to set fixed values within the block so that your tests will produce consistent results.
|
|
232
|
+
|
|
233
|
+
```ruby
|
|
234
|
+
# Specify that all random values should be 0.5
|
|
235
|
+
ConsistentRandom.testing.rand(0.5) do
|
|
236
|
+
expect(ConsistentRandom.new("foo").rand).to eq(0.5)
|
|
237
|
+
expect(ConsistentRandom.new("bar").rand).to eq(0.5)
|
|
238
|
+
|
|
239
|
+
# The rand value must be between 0 and 1, but it will be scaled to fit
|
|
240
|
+
# any size or range specified for `rand`.
|
|
241
|
+
expect(ConsistentRandom.new("foo").rand(10)).to eq(5)
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# You can also specify values for specific names.
|
|
245
|
+
# If a values isn't specified, it will return a random value.
|
|
246
|
+
ConsistentRandom.testing.rand(foo: 0.5, bar: 0.8) do
|
|
247
|
+
expect(ConsistentRandom.new("foo").rand).to eq(0.5)
|
|
248
|
+
expect(ConsistentRandom.new("bar").rand).to eq(0.8)
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
# You can also specify values for the `bytes` and `seed` methods. The methods
|
|
252
|
+
# for setting test valus can be chained together.
|
|
253
|
+
ConsistentRandom.testing.bytes(foo: "bar").seed(baz: 123) do
|
|
254
|
+
expect(ConsistentRandom.new("foo").bytes(6)).to eq("barbar")
|
|
255
|
+
expect(ConsistentRandom.new("baz").seed).to eq(123)
|
|
256
|
+
end
|
|
257
|
+
```
|
|
258
|
+
|
|
209
259
|
## Installation
|
|
210
260
|
|
|
211
261
|
Add this line to your application's Gemfile:
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
2.
|
|
1
|
+
2.3.0
|
data/consistent_random.gemspec
CHANGED
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
class ConsistentRandom
|
|
4
4
|
module ActiveJob
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
# @return [String, nil] the seed deserialized from the job data
|
|
6
|
+
# @api private
|
|
7
|
+
attr_reader :consistent_random_seed
|
|
7
8
|
|
|
8
|
-
|
|
9
|
+
def self.included(base)
|
|
10
|
+
base.around_perform :perform_with_consistent_random_scope
|
|
9
11
|
|
|
10
12
|
base.class_attribute :inherit_consistent_random_scope, instance_writer: false
|
|
11
13
|
end
|
|
@@ -21,14 +23,14 @@ class ConsistentRandom
|
|
|
21
23
|
|
|
22
24
|
def deserialize(job_data)
|
|
23
25
|
super
|
|
24
|
-
@
|
|
26
|
+
@consistent_random_seed = job_data["consistent_random_seed"]
|
|
25
27
|
end
|
|
26
28
|
|
|
27
29
|
private
|
|
28
30
|
|
|
29
|
-
def
|
|
30
|
-
|
|
31
|
-
ConsistentRandom.scope(
|
|
31
|
+
def perform_with_consistent_random_scope(&block)
|
|
32
|
+
seed = consistent_random_seed unless inherit_consistent_random_scope == false
|
|
33
|
+
ConsistentRandom.scope(seed, &block)
|
|
32
34
|
end
|
|
33
35
|
end
|
|
34
36
|
end
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class ConsistentRandom
|
|
4
|
+
# This class returns an object that can be used to generate deterministic values
|
|
5
|
+
# for use in testing.
|
|
6
|
+
#
|
|
7
|
+
# @example
|
|
8
|
+
# ConsistentRandom.testing.rand(foo: 0.5).bytes(foo: "foobar").seed(123) do
|
|
9
|
+
# expect(ConsistentRandom.new("foo").rand).to eq(0.5)
|
|
10
|
+
# expect(ConsistentRandom.new("bar").rand).not_to eq(0.5)
|
|
11
|
+
#
|
|
12
|
+
# expect(ConsistentRandom.new("foo").bytes(12)).to eq("foobarfoobar")
|
|
13
|
+
#
|
|
14
|
+
# expect(ConsistentRandom.new("foo").seed).to eq(123)
|
|
15
|
+
# end
|
|
16
|
+
class ConsistentRandom::Testing
|
|
17
|
+
class << self
|
|
18
|
+
# Get the testing object if any for the current block.
|
|
19
|
+
#
|
|
20
|
+
# @return [ConsistentRandom::Testing, nil] the testing object or nil if not set
|
|
21
|
+
# @api private
|
|
22
|
+
def current
|
|
23
|
+
Thread.current[:consistent_random_testing]
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def initialize
|
|
28
|
+
@rand_hash = {}
|
|
29
|
+
@bytes_hash = {}
|
|
30
|
+
@seed_hash = {}
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Set the random value returned by ConsistentRandom#rand.
|
|
34
|
+
#
|
|
35
|
+
# param options [Float, Hash<String, Float>] the value to return for rand. If a Float is given
|
|
36
|
+
# then it will be used as the value for all calls to rand. If a Hash is given then the value
|
|
37
|
+
# will only be returned for ConsitentRandom objects with the names specified in the keys.
|
|
38
|
+
# @yield block of code to execute with the test values.
|
|
39
|
+
# @return [ConsistentRandom::Testing, Object] If a block is specified, then the result
|
|
40
|
+
# of the block is returned. Otherwise the testing object is returned so that you can
|
|
41
|
+
# chain calls to set up more test values.
|
|
42
|
+
def rand(options, &block)
|
|
43
|
+
options = validate_rand(options)
|
|
44
|
+
unless options
|
|
45
|
+
raise ArgumentError.new("Argument must be a Float between 0 and 1 or a Hash with Float values")
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
@rand_hash = options.default ? options.merge(@rand_hash) : @rand_hash.merge(options)
|
|
49
|
+
if block
|
|
50
|
+
use(&block)
|
|
51
|
+
else
|
|
52
|
+
self
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Set the random bytes returned by ConsistentRandom#bytes.
|
|
57
|
+
#
|
|
58
|
+
# param options [String, Hash<String, String>] the value to return for bytes. If a String is given
|
|
59
|
+
# then it will be used as the value for all calls to bytes. If a Hash is given then the value
|
|
60
|
+
# will only be returned for ConsitentRandom objects with the names specified in the keys.
|
|
61
|
+
# @yield block of code to execute with the test values.
|
|
62
|
+
# @return [ConsistentRandom::Testing, Object] If a block is specified, then the result
|
|
63
|
+
# of the block is returned. Otherwise the testing object is returned so that you can
|
|
64
|
+
# chain calls to set up more test values.
|
|
65
|
+
def bytes(options, &block)
|
|
66
|
+
options = validate_bytes(options)
|
|
67
|
+
unless options
|
|
68
|
+
raise ArgumentError.new("Argument must be a non-empty String or a Hash with non-empty String values")
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
@bytes_hash = options.default ? options.merge(@bytes_hash) : @bytes_hash.merge(options)
|
|
72
|
+
if block
|
|
73
|
+
use(&block)
|
|
74
|
+
else
|
|
75
|
+
self
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Set the seed value returned by ConsistentRandom#seed.
|
|
80
|
+
#
|
|
81
|
+
# param options [Integer, Hash<String, Integer>] the value to return for seed. If an Integer is given
|
|
82
|
+
# then it will be used as the value for all calls to seed. If a Hash is given then the value
|
|
83
|
+
# will only be returned for ConsitentRandom objects with the names specified in the keys.
|
|
84
|
+
# @yield block of code to execute with the test values.
|
|
85
|
+
# @return [ConsistentRandom::Testing, Object] If a block is specified, then the result
|
|
86
|
+
# of the block is returned. Otherwise the testing object is returned so that you can
|
|
87
|
+
# chain calls to set up more test
|
|
88
|
+
def seed(options, &block)
|
|
89
|
+
options = validate_seed(options)
|
|
90
|
+
unless options
|
|
91
|
+
raise ArgumentError.new("Argument must be an Integer or a Hash with Integer values")
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
@seed_hash = options.default ? options.merge(@seed_hash) : @seed_hash.merge(options)
|
|
95
|
+
if block
|
|
96
|
+
use(&block)
|
|
97
|
+
else
|
|
98
|
+
self
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Use the test values within a block of code.
|
|
103
|
+
#
|
|
104
|
+
# @yield block of code to execute with the test values.
|
|
105
|
+
# @return [Object] the result of the block
|
|
106
|
+
def use(&block)
|
|
107
|
+
save_val = Thread.current[:consistent_random_testing]
|
|
108
|
+
begin
|
|
109
|
+
Thread.current[:consistent_random_testing] = self
|
|
110
|
+
yield
|
|
111
|
+
ensure
|
|
112
|
+
Thread.current[:consistent_random_testing] = save_val
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Get the test value for rand.
|
|
117
|
+
#
|
|
118
|
+
# @param name [String] the name of the ConsistentRandom object
|
|
119
|
+
# @return [Float, nil] the test value for rand if there is a test value for the name
|
|
120
|
+
# @api private
|
|
121
|
+
def rand_for(name)
|
|
122
|
+
@rand_hash[name]
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Get the test value for bytes.
|
|
126
|
+
#
|
|
127
|
+
# @param name [String] the name of the ConsistentRandom object
|
|
128
|
+
# @return [String, nil] the test value for bytes if there is a test value for the name
|
|
129
|
+
# @api private
|
|
130
|
+
def bytes_for(name)
|
|
131
|
+
@bytes_hash[name]
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Get the test value for seed.
|
|
135
|
+
#
|
|
136
|
+
# @param name [String] the name of the ConsistentRandom object
|
|
137
|
+
# @return [Integer, nil] the test value for seed if there is a test value for the name
|
|
138
|
+
# @api private
|
|
139
|
+
def seed_for(name)
|
|
140
|
+
@seed_hash[name]
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
private
|
|
144
|
+
|
|
145
|
+
def validate_rand(options)
|
|
146
|
+
if options.is_a?(Hash) && options.values.all? { |value| value.is_a?(Float) && (0...1).cover?(value) }
|
|
147
|
+
options.each_with_object({}) { |(key, value), hash| hash[key.to_s] = value }
|
|
148
|
+
elsif options.is_a?(Float) && (0...1).cover?(options)
|
|
149
|
+
Hash.new(options)
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def validate_bytes(options)
|
|
154
|
+
if options.is_a?(Hash) && options.values.all? { |value| value.is_a?(String) && !value.empty? }
|
|
155
|
+
options.each_with_object({}) { |(key, value), hash| hash[key.to_s] = value.encode(Encoding::ASCII_8BIT) }
|
|
156
|
+
elsif options.is_a?(String) && !options.empty?
|
|
157
|
+
Hash.new(options)
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def validate_seed(options)
|
|
162
|
+
if options.is_a?(Hash) && options.values.all? { |value| value.is_a?(Integer) }
|
|
163
|
+
options.each_with_object({}) { |(key, value), hash| hash[key.to_s] = value }
|
|
164
|
+
elsif options.is_a?(Integer)
|
|
165
|
+
Hash.new(options)
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
data/lib/consistent_random.rb
CHANGED
|
@@ -4,8 +4,11 @@ require "digest/sha1"
|
|
|
4
4
|
require "securerandom"
|
|
5
5
|
|
|
6
6
|
class ConsistentRandom
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
# Divisor for converting the top 53 bits of a seed into a Float. Using 53 bits
|
|
8
|
+
# ensures the result is exactly representable so values are uniform in [0, 1)
|
|
9
|
+
# and can never round up to 1.0.
|
|
10
|
+
FLOAT_DIVISOR = 2.0**53
|
|
11
|
+
private_constant :FLOAT_DIVISOR
|
|
9
12
|
|
|
10
13
|
autoload :RackMiddleware, "consistent_random/rack_middleware"
|
|
11
14
|
autoload :SidekiqMiddleware, "consistent_random/sidekiq_middleware"
|
|
@@ -15,7 +18,7 @@ class ConsistentRandom
|
|
|
15
18
|
class << self
|
|
16
19
|
# Define a scope where consistent random values will be generated.
|
|
17
20
|
#
|
|
18
|
-
# @param
|
|
21
|
+
# @param seed [String, Symbol, Integer, Array<String>, Array<Integer>, nil] optional value to
|
|
19
22
|
# use for generating random numbers. By default a random value will be generated. If the
|
|
20
23
|
# scope is nested in another scope block, then the seed from the parent scope will be used
|
|
21
24
|
# by default.
|
|
@@ -29,7 +32,7 @@ class ConsistentRandom
|
|
|
29
32
|
when String, Symbol, Integer
|
|
30
33
|
seed.to_s
|
|
31
34
|
when Array
|
|
32
|
-
seed.
|
|
35
|
+
seed.join("\x1C")
|
|
33
36
|
else
|
|
34
37
|
raise ArgumentError, "Invalid seed value: #{seed.inspect}"
|
|
35
38
|
end
|
|
@@ -43,19 +46,25 @@ class ConsistentRandom
|
|
|
43
46
|
end
|
|
44
47
|
|
|
45
48
|
# Get the current seed used to generate random numbers. This will return nil if called
|
|
46
|
-
# outside of a scope.
|
|
49
|
+
# outside of a scope. The value can be passed to a new scope to generate the same
|
|
50
|
+
# random values in another thread or fiber.
|
|
47
51
|
#
|
|
48
52
|
# @return [String, nil] the seed value for the current scope
|
|
49
53
|
# or nil if called outside of a scope.
|
|
50
|
-
# @api private
|
|
51
54
|
def current_seed
|
|
52
55
|
Thread.current[:consistent_random_seed]
|
|
53
56
|
end
|
|
57
|
+
|
|
58
|
+
def testing
|
|
59
|
+
Testing.new
|
|
60
|
+
end
|
|
54
61
|
end
|
|
55
62
|
|
|
63
|
+
attr_reader :name
|
|
64
|
+
|
|
56
65
|
# @param name [Object] a name used to identifuy a consistent random value
|
|
57
66
|
def initialize(name)
|
|
58
|
-
@name = name
|
|
67
|
+
@name = (name.is_a?(String) ? name.dup : name.to_s).freeze
|
|
59
68
|
end
|
|
60
69
|
|
|
61
70
|
# Generate a random number. The same number will be generated within a scope block.
|
|
@@ -68,7 +77,7 @@ class ConsistentRandom
|
|
|
68
77
|
# a number in that range. If max is an number, then it will be an integer between 0 and that
|
|
69
78
|
# value. Otherwise, it will be a float between 0 and 1.
|
|
70
79
|
def rand(max = nil)
|
|
71
|
-
value = seed /
|
|
80
|
+
value = Testing.current&.rand_for(name) || (seed >> 11) / FLOAT_DIVISOR
|
|
72
81
|
case max
|
|
73
82
|
when nil
|
|
74
83
|
value
|
|
@@ -85,8 +94,13 @@ class ConsistentRandom
|
|
|
85
94
|
# @param size [Integer] the number of bytes to generate.
|
|
86
95
|
# @return [String] a string of random bytes.
|
|
87
96
|
def bytes(size)
|
|
97
|
+
test_bytes = Testing.current&.bytes_for(name)
|
|
98
|
+
chunk_size = (test_bytes ? test_bytes.length : 20)
|
|
99
|
+
|
|
88
100
|
bytes = []
|
|
89
|
-
(
|
|
101
|
+
(size / chunk_size.to_f).ceil.times do |i|
|
|
102
|
+
bytes << (test_bytes || seed_hash("#{name}#{i}").to_s)
|
|
103
|
+
end
|
|
90
104
|
bytes.join[0, size]
|
|
91
105
|
end
|
|
92
106
|
|
|
@@ -95,13 +109,17 @@ class ConsistentRandom
|
|
|
95
109
|
#
|
|
96
110
|
# @return [Integer] a 64 bit integer for seeding random values
|
|
97
111
|
def seed
|
|
98
|
-
|
|
112
|
+
test_seed = Testing.current&.seed_for(name)
|
|
113
|
+
return test_seed if test_seed
|
|
114
|
+
|
|
115
|
+
hash = seed_hash(name)
|
|
99
116
|
hash.byteslice(0, 8).unpack1("Q>")
|
|
100
117
|
end
|
|
101
118
|
|
|
102
119
|
# @return [Boolean] true if the other object is a ConsistentRandom that returns
|
|
103
120
|
# the same random number generator. If called outside of a scope, then it will
|
|
104
|
-
# always return false.
|
|
121
|
+
# always return false. The functionality is designed to be similar to the
|
|
122
|
+
# same behavior as Random.
|
|
105
123
|
def ==(other)
|
|
106
124
|
other.is_a?(self.class) && other.seed == seed
|
|
107
125
|
end
|
|
@@ -131,10 +149,20 @@ class ConsistentRandom
|
|
|
131
149
|
raise ArgumentError, "Cannot generate random value for infinite range"
|
|
132
150
|
end
|
|
133
151
|
|
|
152
|
+
unless min.is_a?(Numeric) && max.is_a?(Numeric)
|
|
153
|
+
raise ArgumentError, "Cannot generate random value for non-numeric range"
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
if min > max || (range.exclude_end? && min == max)
|
|
157
|
+
raise ArgumentError, "Cannot generate random value for empty range"
|
|
158
|
+
end
|
|
159
|
+
|
|
134
160
|
int_range = min.is_a?(Integer) && max.is_a?(Integer)
|
|
135
161
|
max += 1 if int_range && range.include?(max)
|
|
136
162
|
|
|
137
163
|
val = (value * (max - min)) + min
|
|
138
|
-
int_range ? val.
|
|
164
|
+
int_range ? val.floor : val
|
|
139
165
|
end
|
|
140
166
|
end
|
|
167
|
+
|
|
168
|
+
require_relative "consistent_random/testing"
|
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: consistent_random
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.
|
|
4
|
+
version: 2.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Brian Durand
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: bin
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: bundler
|
|
@@ -24,7 +23,6 @@ dependencies:
|
|
|
24
23
|
- - ">="
|
|
25
24
|
- !ruby/object:Gem::Version
|
|
26
25
|
version: '0'
|
|
27
|
-
description:
|
|
28
26
|
email:
|
|
29
27
|
- bbdurand@gmail.com
|
|
30
28
|
executables: []
|
|
@@ -41,6 +39,7 @@ files:
|
|
|
41
39
|
- lib/consistent_random/rack_middleware.rb
|
|
42
40
|
- lib/consistent_random/sidekiq_client_middleware.rb
|
|
43
41
|
- lib/consistent_random/sidekiq_middleware.rb
|
|
42
|
+
- lib/consistent_random/testing.rb
|
|
44
43
|
homepage: https://github.com/bdurand/consistent_random
|
|
45
44
|
licenses:
|
|
46
45
|
- MIT
|
|
@@ -48,7 +47,6 @@ metadata:
|
|
|
48
47
|
homepage_uri: https://github.com/bdurand/consistent_random
|
|
49
48
|
source_code_uri: https://github.com/bdurand/consistent_random
|
|
50
49
|
changelog_uri: https://github.com/bdurand/consistent_random/blob/main/CHANGELOG.md
|
|
51
|
-
post_install_message:
|
|
52
50
|
rdoc_options: []
|
|
53
51
|
require_paths:
|
|
54
52
|
- lib
|
|
@@ -56,15 +54,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
56
54
|
requirements:
|
|
57
55
|
- - ">="
|
|
58
56
|
- !ruby/object:Gem::Version
|
|
59
|
-
version: '2.
|
|
57
|
+
version: '2.6'
|
|
60
58
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
61
59
|
requirements:
|
|
62
60
|
- - ">="
|
|
63
61
|
- !ruby/object:Gem::Version
|
|
64
62
|
version: '0'
|
|
65
63
|
requirements: []
|
|
66
|
-
rubygems_version:
|
|
67
|
-
signing_key:
|
|
64
|
+
rubygems_version: 4.0.3
|
|
68
65
|
specification_version: 4
|
|
69
66
|
summary: Generates consistent random values within a defined scope, ensuring deterministic
|
|
70
67
|
behavior for use in feature rollouts and other scoped operations.
|