consistent_random 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +10 -0
- data/MIT-LICENSE.txt +20 -0
- data/README.md +128 -0
- data/VERSION +1 -0
- data/consistent_random.gemspec +41 -0
- data/lib/consistent_random/context.rb +21 -0
- data/lib/consistent_random/rack_middleware.rb +17 -0
- data/lib/consistent_random/sidekiq_middleware.rb +17 -0
- data/lib/consistent_random.rb +69 -0
- metadata +70 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 22c5eb562537bed9919f77d7d5daeafcd33c8279e91c902d66cfc48b07183a19
|
4
|
+
data.tar.gz: 27000167f87f8b8a02723d8e160d054f59a4d077bf5e22c5834a978e99f6f492
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 6c2d988676f767b138d64e44c6f87f97e08058a5c2fd0a7ce528b4728f624d8c887753182b409c9b343ee3ece9d93f681306fcbecbf7967c5e9c524eb4c95b19
|
7
|
+
data.tar.gz: 7ff4c55d561b0947d7d51d7c96b11e9bf85dcccdc0e11c7f1b6c25923cc3b23696dcdd8ee3069c46c358a81506ab46cc3980a7fc3e3f12929d86c97c7ed19c50
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
# Changelog
|
2
|
+
All notable changes to this project will be documented in this file.
|
3
|
+
|
4
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
5
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
6
|
+
|
7
|
+
## 1.0.0
|
8
|
+
|
9
|
+
### Added
|
10
|
+
- Initial release.
|
data/MIT-LICENSE.txt
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2024 Brian Durand
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,128 @@
|
|
1
|
+
# Constant Random
|
2
|
+
|
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
|
+
[![Ruby Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://github.com/testdouble/standard)
|
5
|
+
[![Gem Version](https://badge.fury.io/rb/consistent_random.svg)](https://badge.fury.io/rb/consistent_random)
|
6
|
+
|
7
|
+
## Introduction
|
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, such as handling feature rollouts.
|
10
|
+
|
11
|
+
For example, consider rolling out a new feature to a subset of requests, such as enabling the feature 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
|
+
|
13
|
+
## Usage
|
14
|
+
|
15
|
+
To generate consistent random values, you need to define a scope. You do this with the `ConsistentRandom.scope` method. Within the scope block, calls to `ConsistentRandom` will return the same random values for the same name.
|
16
|
+
|
17
|
+
```ruby
|
18
|
+
ConsistentRandom.scope do
|
19
|
+
random = ConsistentRandom.new("foobar")
|
20
|
+
a = random.rand(100) # Generates a random number between 0 and 99 tied to "foobar"
|
21
|
+
b = random.rand(100) # Same random number as 'a', because "foobar" is reused
|
22
|
+
a == b # => true
|
23
|
+
end
|
24
|
+
```
|
25
|
+
|
26
|
+
This can be used to implement things like feature flags for rolling out new features on a percentage of your requests.
|
27
|
+
|
28
|
+
```ruby
|
29
|
+
class FeatureFlag
|
30
|
+
def initialize(name, roll_out_percentage)
|
31
|
+
@name = name
|
32
|
+
@roll_out_percentage = roll_out_percentage
|
33
|
+
end
|
34
|
+
|
35
|
+
def enabled?
|
36
|
+
ConsistentRandom.new("FeatureFlag.#{@name}").rand < @roll_out_percentage
|
37
|
+
end
|
38
|
+
end
|
39
|
+
```
|
40
|
+
|
41
|
+
Checking a feature flag will return the same value within a scope.
|
42
|
+
|
43
|
+
```ruby
|
44
|
+
class MyService
|
45
|
+
def call(arg)
|
46
|
+
if FeatureFlag.new("new_feature", 0.1).enabled?
|
47
|
+
# Do something new 10% of the time
|
48
|
+
else
|
49
|
+
# Do something else
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
ConsistentRandom.scope do
|
55
|
+
things.each do |thing|
|
56
|
+
MyService.new.call(thing) # You won't get a mix of new and old behavior within this iteration
|
57
|
+
end
|
58
|
+
end
|
59
|
+
```
|
60
|
+
|
61
|
+
If there is no scope defined, the random values will be different each time for different instances of `ConsistentRandom`. So, if your code is executed outside of a scope, it will still work but with random values being generated rather than consistent values.
|
62
|
+
|
63
|
+
```ruby
|
64
|
+
random = ConsistentRandom.new("foobar")
|
65
|
+
random.rand != random.rand # => true
|
66
|
+
```
|
67
|
+
|
68
|
+
### Middlewares
|
69
|
+
|
70
|
+
The gem provides built-in middlewares for Rack and Sidekiq, automatically scoping requests and jobs. This ensures that consistent random values are generated within the request/job context.
|
71
|
+
|
72
|
+
#### Rack Middleware
|
73
|
+
|
74
|
+
In a Rack application:
|
75
|
+
|
76
|
+
```ruby
|
77
|
+
Rack::Builder.app do
|
78
|
+
use ConsistentRandom::RackMiddleware
|
79
|
+
run MyApp
|
80
|
+
end
|
81
|
+
```
|
82
|
+
|
83
|
+
Or in a Rails application:
|
84
|
+
|
85
|
+
```ruby
|
86
|
+
# config/application.rb
|
87
|
+
config.middleware.use ConsistentRandom::RackMiddleware
|
88
|
+
```
|
89
|
+
|
90
|
+
#### Sidekiq Middleware
|
91
|
+
|
92
|
+
Add the middleware to your Sidekiq server configuration:
|
93
|
+
|
94
|
+
```ruby
|
95
|
+
Sidekiq.configure_server do |config|
|
96
|
+
config.server_middleware do |chain|
|
97
|
+
chain.add ConsistentRandom::SidekiqMiddleware
|
98
|
+
end
|
99
|
+
end
|
100
|
+
```
|
101
|
+
|
102
|
+
## Installation
|
103
|
+
|
104
|
+
Add this line to your application's Gemfile:
|
105
|
+
|
106
|
+
```ruby
|
107
|
+
gem "consistent_random"
|
108
|
+
```
|
109
|
+
|
110
|
+
Then execute:
|
111
|
+
```bash
|
112
|
+
$ bundle
|
113
|
+
```
|
114
|
+
|
115
|
+
Or install it yourself as:
|
116
|
+
```bash
|
117
|
+
$ gem install consistent_random
|
118
|
+
```
|
119
|
+
|
120
|
+
## Contributing
|
121
|
+
|
122
|
+
Open a pull request on [GitHub](https://github.com/bdurand/consistent_random).
|
123
|
+
|
124
|
+
Please use the [standardrb](https://github.com/testdouble/standard) syntax and lint your code with `standardrb --fix` before submitting.
|
125
|
+
|
126
|
+
## License
|
127
|
+
|
128
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
1.0.0
|
@@ -0,0 +1,41 @@
|
|
1
|
+
Gem::Specification.new do |spec|
|
2
|
+
spec.name = "consistent_random"
|
3
|
+
spec.version = File.read(File.expand_path("../VERSION", __FILE__)).strip
|
4
|
+
spec.authors = ["Brian Durand"]
|
5
|
+
spec.email = ["bbdurand@gmail.com"]
|
6
|
+
|
7
|
+
spec.summary = "Generates consistent random values within a defined scope, ensuring deterministic behavior for use in feature rollouts and other scoped operations."
|
8
|
+
|
9
|
+
spec.homepage = "https://github.com/bdurand/consistent_random"
|
10
|
+
spec.license = "MIT"
|
11
|
+
|
12
|
+
spec.metadata = {
|
13
|
+
"homepage_uri" => spec.homepage,
|
14
|
+
"source_code_uri" => spec.homepage,
|
15
|
+
"changelog_uri" => "#{spec.homepage}/blob/main/CHANGELOG.md"
|
16
|
+
}
|
17
|
+
|
18
|
+
# Specify which files should be added to the gem when it is released.
|
19
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
20
|
+
ignore_files = %w[
|
21
|
+
.
|
22
|
+
Appraisals
|
23
|
+
Gemfile
|
24
|
+
Gemfile.lock
|
25
|
+
Rakefile
|
26
|
+
config.ru
|
27
|
+
assets/
|
28
|
+
bin/
|
29
|
+
gemfiles/
|
30
|
+
spec/
|
31
|
+
]
|
32
|
+
spec.files = Dir.chdir(File.expand_path("..", __FILE__)) do
|
33
|
+
`git ls-files -z`.split("\x0").reject { |f| ignore_files.any? { |path| f.start_with?(path) } }
|
34
|
+
end
|
35
|
+
|
36
|
+
spec.require_paths = ["lib"]
|
37
|
+
|
38
|
+
spec.required_ruby_version = ">= 2.5"
|
39
|
+
|
40
|
+
spec.add_development_dependency "bundler"
|
41
|
+
end
|
@@ -0,0 +1,21 @@
|
|
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
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class ConsistentRandom
|
4
|
+
# Rack middleware that wraps a request with consistent random scope
|
5
|
+
# so that you can generate consistent random values within a request.
|
6
|
+
class RackMiddleware
|
7
|
+
def initialize(app)
|
8
|
+
@app = app
|
9
|
+
end
|
10
|
+
|
11
|
+
def call(env)
|
12
|
+
ConsistentRandom.scope do
|
13
|
+
@app.call(env)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class ConsistentRandom
|
4
|
+
# Sidekiq server middleware that wraps job execution with consistent random scope
|
5
|
+
# so that you can generate consistent random values within a job.
|
6
|
+
class SidekiqMiddleware
|
7
|
+
if defined?(Sidekiq::ServerMiddleware)
|
8
|
+
include Sidekiq::ServerMiddleware
|
9
|
+
end
|
10
|
+
|
11
|
+
def call(job_instance, job_payload, queue)
|
12
|
+
ConsistentRandom.scope do
|
13
|
+
yield
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "consistent_random/context"
|
4
|
+
require_relative "consistent_random/rack_middleware"
|
5
|
+
require_relative "consistent_random/sidekiq_middleware"
|
6
|
+
|
7
|
+
class ConsistentRandom
|
8
|
+
class << self
|
9
|
+
# Define a scope where consistent random values will be generated.
|
10
|
+
#
|
11
|
+
# @yield block of code to execute within the scope
|
12
|
+
# @return the result of the block
|
13
|
+
def scope
|
14
|
+
existing_context = Thread.current[:consistent_random_context]
|
15
|
+
begin
|
16
|
+
context = Context.new(existing_context)
|
17
|
+
Thread.current[:consistent_random_context] = context
|
18
|
+
yield
|
19
|
+
ensure
|
20
|
+
Thread.current[:consistent_random_context] = existing_context
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# @param name [Object] a name used to identifuy a consistent random value
|
26
|
+
def initialize(name)
|
27
|
+
@name = name
|
28
|
+
end
|
29
|
+
|
30
|
+
# Generate a random float. This method works the same as Kernel#rand.
|
31
|
+
#
|
32
|
+
# @param max [Integer, Range] the maximum value of the random float or a range indicating
|
33
|
+
# the minimum and maximum values.
|
34
|
+
# @return [Numeric] a random number. If the max argument is a range, then the result will be
|
35
|
+
# a number in that range. If max is an number, then it will be an integer between 0 and that
|
36
|
+
# value. Otherwise, it will be a float between 0 and 1.
|
37
|
+
def rand(max = nil)
|
38
|
+
random.rand(max || 1.0)
|
39
|
+
end
|
40
|
+
|
41
|
+
# Generate a random integer. This method works the same as Random#bytes.
|
42
|
+
#
|
43
|
+
# @param size [Integer] the number of bytes to generate.
|
44
|
+
# @return [String] a string of random bytes.
|
45
|
+
def bytes(size)
|
46
|
+
random.bytes(size)
|
47
|
+
end
|
48
|
+
|
49
|
+
# Generate a random number generator for the given name. The generator will always
|
50
|
+
# have the same seed within a scope.
|
51
|
+
#
|
52
|
+
# @return [Random] a random number generator
|
53
|
+
def random
|
54
|
+
Random.new(current_context.seed(@name))
|
55
|
+
end
|
56
|
+
|
57
|
+
# @return [Boolean] true if the other object is a ConsistentRandom that returns
|
58
|
+
# the same random number generator. If called outside of a scope, then it will
|
59
|
+
# always return false.
|
60
|
+
def ==(other)
|
61
|
+
other.is_a?(self.class) && other.random = random
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
def current_context
|
67
|
+
Thread.current[:consistent_random_context] || Context.new
|
68
|
+
end
|
69
|
+
end
|
metadata
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: consistent_random
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Brian Durand
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2024-10-06 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
description:
|
28
|
+
email:
|
29
|
+
- bbdurand@gmail.com
|
30
|
+
executables: []
|
31
|
+
extensions: []
|
32
|
+
extra_rdoc_files: []
|
33
|
+
files:
|
34
|
+
- CHANGELOG.md
|
35
|
+
- MIT-LICENSE.txt
|
36
|
+
- README.md
|
37
|
+
- VERSION
|
38
|
+
- consistent_random.gemspec
|
39
|
+
- lib/consistent_random.rb
|
40
|
+
- lib/consistent_random/context.rb
|
41
|
+
- lib/consistent_random/rack_middleware.rb
|
42
|
+
- lib/consistent_random/sidekiq_middleware.rb
|
43
|
+
homepage: https://github.com/bdurand/consistent_random
|
44
|
+
licenses:
|
45
|
+
- MIT
|
46
|
+
metadata:
|
47
|
+
homepage_uri: https://github.com/bdurand/consistent_random
|
48
|
+
source_code_uri: https://github.com/bdurand/consistent_random
|
49
|
+
changelog_uri: https://github.com/bdurand/consistent_random/blob/main/CHANGELOG.md
|
50
|
+
post_install_message:
|
51
|
+
rdoc_options: []
|
52
|
+
require_paths:
|
53
|
+
- lib
|
54
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
55
|
+
requirements:
|
56
|
+
- - ">="
|
57
|
+
- !ruby/object:Gem::Version
|
58
|
+
version: '2.5'
|
59
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
60
|
+
requirements:
|
61
|
+
- - ">="
|
62
|
+
- !ruby/object:Gem::Version
|
63
|
+
version: '0'
|
64
|
+
requirements: []
|
65
|
+
rubygems_version: 3.4.10
|
66
|
+
signing_key:
|
67
|
+
specification_version: 4
|
68
|
+
summary: Generates consistent random values within a defined scope, ensuring deterministic
|
69
|
+
behavior for use in feature rollouts and other scoped operations.
|
70
|
+
test_files: []
|