consistent_random 1.0.0 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +11 -0
- data/README.md +52 -4
- data/VERSION +1 -1
- data/lib/consistent_random/active_job.rb +34 -0
- data/lib/consistent_random/sidekiq_client_middleware.rb +20 -0
- data/lib/consistent_random/sidekiq_middleware.rb +1 -1
- data/lib/consistent_random.rb +89 -18
- metadata +4 -3
- data/lib/consistent_random/context.rb +0 -21
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 980cb06e9d5c20085243ecb114932f744343830090a6148db527d5c685458c46
|
4
|
+
data.tar.gz: 8872129783254ab5cd4ae70f4051ceb5901c4d28852168dfa0e4b229c8541d13
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 99ad2eb1aaf87bdee2aa1c1052d73c8e9db13e392c9d501c9d8d445644902414a0602ef4f8da3c63199cd55e2da0519bd15dd48b7070bbec59414506d181613c
|
7
|
+
data.tar.gz: 76a775637c84fa12fb56bf6437943ffdc59cb7a454ec1fa9af44fda471d614237e61297c18251744fb45d5de2c0d18b8ec84ef1a3603b4e56b16ce954bb4a5e1
|
data/CHANGELOG.md
CHANGED
@@ -4,6 +4,17 @@ 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.0.0
|
8
|
+
|
9
|
+
### Changed
|
10
|
+
|
11
|
+
- `ConsistentRandom#rand` uses a faster hashing algorithm for generating random values. This will make the value consistent across different Ruby versions and platforms.
|
12
|
+
|
13
|
+
### Added
|
14
|
+
|
15
|
+
- Added `ConsistentRandom::SidekiqClientMiddleware` to allow persisting consistent random seeds to Sidekiq jobs so behavior is consistent between when jobs are enqueued and when they are executed.
|
16
|
+
- Added `ConsistentRandom::ActiveJob` for hooking into ActiveJob to persist consistent random seeds to jobs.
|
17
|
+
|
7
18
|
## 1.0.0
|
8
19
|
|
9
20
|
### Added
|
data/README.md
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
#
|
1
|
+
# Consistent Random
|
2
2
|
|
3
3
|
[![Continuous Integration](https://github.com/bdurand/consistent_random/actions/workflows/continuous_integration.yml/badge.svg)](https://github.com/bdurand/consistent_random/actions/workflows/continuous_integration.yml)
|
4
4
|
[![Ruby Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://github.com/testdouble/standard)
|
@@ -6,9 +6,9 @@
|
|
6
6
|
|
7
7
|
## Introduction
|
8
8
|
|
9
|
-
This Ruby gem allows you to generate consistent random values tied to a specific name within a defined scope. It ensures that random behavior remains consistent within a particular context
|
9
|
+
This Ruby gem allows you to generate consistent random values tied to a specific name within a defined scope. It ensures that random behavior remains consistent within a particular context.
|
10
10
|
|
11
|
-
For example, consider rolling out a new feature to a subset of requests
|
11
|
+
For example, consider rolling out a new feature to a subset of requests. You may want to do this to allow testing a new feature by only enabling it for 10% of requests. You want to randomize which requests get the new feature, but ensure that within each request, the feature is consistently enabled or disabled across all actions. This gem allows you to achieve that by tying random values to specific names and defining a scope. Within that scope, the same value will be consistently generated for each named variable.
|
12
12
|
|
13
13
|
## Usage
|
14
14
|
|
@@ -89,7 +89,7 @@ config.middleware.use ConsistentRandom::RackMiddleware
|
|
89
89
|
|
90
90
|
#### Sidekiq Middleware
|
91
91
|
|
92
|
-
Add the
|
92
|
+
Add the middlewares to your Sidekiq configuration:
|
93
93
|
|
94
94
|
```ruby
|
95
95
|
Sidekiq.configure_server do |config|
|
@@ -97,6 +97,54 @@ Sidekiq.configure_server do |config|
|
|
97
97
|
chain.add ConsistentRandom::SidekiqMiddleware
|
98
98
|
end
|
99
99
|
end
|
100
|
+
|
101
|
+
Sidekiq.configure_client do |config|
|
102
|
+
config.client_middleware do |chain|
|
103
|
+
chain.add ConsistentRandom::SidekiqClientMiddleware
|
104
|
+
end
|
105
|
+
end
|
106
|
+
```
|
107
|
+
|
108
|
+
Consistent random values will be propagated from the original request to any Sidekiq jobs so you will get consistent behavior on any ansynchronous jobs. You can disable this behavior on a job by setting the `conistent_random` sidekiq option to `false`:
|
109
|
+
|
110
|
+
```ruby
|
111
|
+
class MyWorker
|
112
|
+
include Sidekiq::Job
|
113
|
+
|
114
|
+
sidekiq_options consistent_random: false
|
115
|
+
|
116
|
+
def perform
|
117
|
+
# Each job will use it's own random scope.
|
118
|
+
end
|
119
|
+
end
|
120
|
+
```
|
121
|
+
|
122
|
+
### ActiveJob
|
123
|
+
|
124
|
+
You can use consistent random values in your ActiveJob jobs by including the `ConsistentRandom::ActiveJob` module.
|
125
|
+
|
126
|
+
```ruby
|
127
|
+
class MyJob < ApplicationJob
|
128
|
+
include ConsistentRandom::ActiveJob
|
129
|
+
|
130
|
+
def perform
|
131
|
+
# Job will use consistent random values using the same scope from when it was enqueued.
|
132
|
+
end
|
133
|
+
end
|
134
|
+
```
|
135
|
+
|
136
|
+
Jobs will inherit the same consistent random values as the request that spawned the job. You can force a job to use it's own random scope by setting the `consistent_random` option to `false`:
|
137
|
+
|
138
|
+
```ruby
|
139
|
+
class MyJob < ApplicationJob
|
140
|
+
include ConsistentRandom::ActiveJob
|
141
|
+
|
142
|
+
self.inherit_consistent_random_scope = false
|
143
|
+
|
144
|
+
def perform
|
145
|
+
# Job will use it's own random scope.
|
146
|
+
end
|
147
|
+
end
|
100
148
|
```
|
101
149
|
|
102
150
|
## Installation
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
|
1
|
+
2.0.0
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class ConsistentRandom
|
4
|
+
module ActiveJob
|
5
|
+
def self.included(base)
|
6
|
+
attr_reader :consistent_random_seeds
|
7
|
+
|
8
|
+
base.around_perform :peform_with_consistent_random_scope
|
9
|
+
|
10
|
+
base.class_attribute :inherit_consistent_random_scope, instance_writer: false
|
11
|
+
end
|
12
|
+
|
13
|
+
def serialize
|
14
|
+
job_data = super
|
15
|
+
if inherit_consistent_random_scope != false
|
16
|
+
seed = ConsistentRandom.current_seed
|
17
|
+
job_data["consistent_random_seed"] = seed unless seed.nil?
|
18
|
+
end
|
19
|
+
job_data
|
20
|
+
end
|
21
|
+
|
22
|
+
def deserialize(job_data)
|
23
|
+
super
|
24
|
+
@consistent_random_seeds = job_data["consistent_random_seed"]
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def peform_with_consistent_random_scope(&block)
|
30
|
+
seeds = consistent_random_seeds unless inherit_consistent_random_scope == false
|
31
|
+
ConsistentRandom.scope(seeds, &block)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class ConsistentRandom
|
4
|
+
# Sidekiq client middleware that adds the current seeds to the job options. These
|
5
|
+
# seeds will be deserialized and used when the job is run on the server so that
|
6
|
+
# the client and server can share consistent random values.
|
7
|
+
class SidekiqClientMiddleware
|
8
|
+
if defined?(Sidekiq::ClientMiddleware)
|
9
|
+
include Sidekiq::ClientMiddleware
|
10
|
+
end
|
11
|
+
|
12
|
+
def call(job_class_or_string, job, queue, redis_pool)
|
13
|
+
unless job["consistent_random"] == false
|
14
|
+
seed = ConsistentRandom.current_seed
|
15
|
+
job["consistent_random_seed"] = seed unless seed.nil?
|
16
|
+
end
|
17
|
+
yield
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
data/lib/consistent_random.rb
CHANGED
@@ -1,25 +1,56 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
3
|
+
require "digest/sha1"
|
4
|
+
require "securerandom"
|
5
|
+
|
4
6
|
require_relative "consistent_random/rack_middleware"
|
5
7
|
require_relative "consistent_random/sidekiq_middleware"
|
8
|
+
require_relative "consistent_random/sidekiq_client_middleware"
|
9
|
+
require_relative "consistent_random/active_job"
|
6
10
|
|
7
11
|
class ConsistentRandom
|
12
|
+
SEED_DIVISOR = (2**64 - 1).to_f
|
13
|
+
private_constant :SEED_DIVISOR
|
14
|
+
|
8
15
|
class << self
|
9
16
|
# Define a scope where consistent random values will be generated.
|
10
17
|
#
|
18
|
+
# @param seeds [String, Symbol, Integer, Array<String>, Array<Integer>, nil] optional value to
|
19
|
+
# use for generating random numbers. By default a random value will be generated. If the
|
20
|
+
# scope is nested in another scope block, then the seed from the parent scope will be used
|
21
|
+
# by default.
|
11
22
|
# @yield block of code to execute within the scope
|
12
23
|
# @return the result of the block
|
13
|
-
def scope
|
14
|
-
|
24
|
+
def scope(seed = nil)
|
25
|
+
existing_seed = Thread.current[:consistent_random_seed]
|
26
|
+
seed_value = case seed
|
27
|
+
when nil
|
28
|
+
existing_seed || SecureRandom.hex
|
29
|
+
when String, Symbol, Integer
|
30
|
+
seed.to_s
|
31
|
+
when Array
|
32
|
+
seed.map { |s| s.to_s }.join("\x1C")
|
33
|
+
else
|
34
|
+
raise ArgumentError, "Invalid seed value: #{seed.inspect}"
|
35
|
+
end
|
36
|
+
|
15
37
|
begin
|
16
|
-
|
17
|
-
Thread.current[:consistent_random_context] = context
|
38
|
+
Thread.current[:consistent_random_seed] = seed_value
|
18
39
|
yield
|
19
40
|
ensure
|
20
|
-
Thread.current[:
|
41
|
+
Thread.current[:consistent_random_seed] = existing_seed
|
21
42
|
end
|
22
43
|
end
|
44
|
+
|
45
|
+
# Get the current seed used to generate random numbers. This will return nil if called
|
46
|
+
# outside of a scope.
|
47
|
+
#
|
48
|
+
# @return [String, nil] the seed value for the current scope
|
49
|
+
# or nil if called outside of a scope.
|
50
|
+
# @api private
|
51
|
+
def current_seed
|
52
|
+
Thread.current[:consistent_random_seed]
|
53
|
+
end
|
23
54
|
end
|
24
55
|
|
25
56
|
# @param name [Object] a name used to identifuy a consistent random value
|
@@ -27,7 +58,9 @@ class ConsistentRandom
|
|
27
58
|
@name = name
|
28
59
|
end
|
29
60
|
|
30
|
-
# Generate a random
|
61
|
+
# Generate a random number. The same number will be generated within a scope block.
|
62
|
+
# This method works the same as Kernel#rand. It will generate a consistent value even
|
63
|
+
# across Ruby versions and platforms.
|
31
64
|
#
|
32
65
|
# @param max [Integer, Range] the maximum value of the random float or a range indicating
|
33
66
|
# the minimum and maximum values.
|
@@ -35,35 +68,73 @@ class ConsistentRandom
|
|
35
68
|
# a number in that range. If max is an number, then it will be an integer between 0 and that
|
36
69
|
# value. Otherwise, it will be a float between 0 and 1.
|
37
70
|
def rand(max = nil)
|
38
|
-
|
71
|
+
value = seed / SEED_DIVISOR
|
72
|
+
case max
|
73
|
+
when nil
|
74
|
+
value
|
75
|
+
when Numeric
|
76
|
+
(value * max.to_i).to_i
|
77
|
+
when Range
|
78
|
+
cap_to_range(value, max)
|
79
|
+
end
|
39
80
|
end
|
40
81
|
|
41
|
-
# Generate a random
|
82
|
+
# Generate a random array of bytes. The same number will be generated within a scope block.
|
83
|
+
# This method works the same as Random#bytes.
|
42
84
|
#
|
43
85
|
# @param size [Integer] the number of bytes to generate.
|
44
86
|
# @return [String] a string of random bytes.
|
45
87
|
def bytes(size)
|
46
|
-
|
88
|
+
bytes = []
|
89
|
+
((size + 19) / 20).times { |i| bytes << seed_hash("#{@name}#{i}").to_s }
|
90
|
+
bytes.join[0, size]
|
47
91
|
end
|
48
92
|
|
49
|
-
# Generate a
|
50
|
-
#
|
93
|
+
# Generate a seed that can be used to generate random numbers. This seed will be
|
94
|
+
# return a consistent value when called within a scope.
|
51
95
|
#
|
52
|
-
# @return [
|
53
|
-
def
|
54
|
-
|
96
|
+
# @return [Integer] a 64 bit integer for seeding random values
|
97
|
+
def seed
|
98
|
+
hash = seed_hash(@name)
|
99
|
+
hash.byteslice(0, 8).unpack1("Q>")
|
55
100
|
end
|
56
101
|
|
57
102
|
# @return [Boolean] true if the other object is a ConsistentRandom that returns
|
58
103
|
# the same random number generator. If called outside of a scope, then it will
|
59
104
|
# always return false.
|
60
105
|
def ==(other)
|
61
|
-
other.is_a?(self.class) && other.
|
106
|
+
other.is_a?(self.class) && other.seed = seed
|
107
|
+
end
|
108
|
+
|
109
|
+
# Generate a random number generator for the given name. The generator will always
|
110
|
+
# have the same seed within a scope.
|
111
|
+
#
|
112
|
+
# This value is dependent on the Ruby Random class and may not generate consistent values
|
113
|
+
# across Ruby versions and platforms.
|
114
|
+
#
|
115
|
+
# @return [Random] a random number generator
|
116
|
+
def random
|
117
|
+
Random.new(seed)
|
62
118
|
end
|
63
119
|
|
64
120
|
private
|
65
121
|
|
66
|
-
def
|
67
|
-
|
122
|
+
def seed_hash(name)
|
123
|
+
random_seed = self.class.current_seed || SecureRandom.hex
|
124
|
+
Digest::SHA1.digest("#{random_seed}\x1C#{name}")
|
125
|
+
end
|
126
|
+
|
127
|
+
def cap_to_range(value, range)
|
128
|
+
min = range.begin
|
129
|
+
max = range.end
|
130
|
+
if min.nil? || max.nil?
|
131
|
+
raise ArgumentError, "Cannot generate random value for infinite range"
|
132
|
+
end
|
133
|
+
|
134
|
+
int_range = min.is_a?(Integer) && max.is_a?(Integer)
|
135
|
+
max += 1 if int_range && range.include?(max)
|
136
|
+
|
137
|
+
val = (value * (max - min)) + min
|
138
|
+
int_range ? val.to_i : val
|
68
139
|
end
|
69
140
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: consistent_random
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 2.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Brian Durand
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-10-
|
11
|
+
date: 2024-10-24 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -37,8 +37,9 @@ files:
|
|
37
37
|
- VERSION
|
38
38
|
- consistent_random.gemspec
|
39
39
|
- lib/consistent_random.rb
|
40
|
-
- lib/consistent_random/
|
40
|
+
- lib/consistent_random/active_job.rb
|
41
41
|
- lib/consistent_random/rack_middleware.rb
|
42
|
+
- lib/consistent_random/sidekiq_client_middleware.rb
|
42
43
|
- lib/consistent_random/sidekiq_middleware.rb
|
43
44
|
homepage: https://github.com/bdurand/consistent_random
|
44
45
|
licenses:
|
@@ -1,21 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
class ConsistentRandom
|
4
|
-
class Context
|
5
|
-
# @api private
|
6
|
-
attr_reader :seeds
|
7
|
-
|
8
|
-
# @param existing_context [Context, nil] Existing context to copy generators from
|
9
|
-
def initialize(existing_context = nil)
|
10
|
-
@seeds = (existing_context ? existing_context.seeds.dup : {})
|
11
|
-
end
|
12
|
-
|
13
|
-
# Return a random number generator for the given name and seed
|
14
|
-
#
|
15
|
-
# @param name [String] Name of the generator
|
16
|
-
# @return [Random] Random number generator
|
17
|
-
def seed(name)
|
18
|
-
@seeds[name] ||= Random.new_seed
|
19
|
-
end
|
20
|
-
end
|
21
|
-
end
|