race_block 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/main.yml +23 -0
- data/.rubocop_todo.yml +3 -23
- data/README.md +34 -4
- data/lib/race_block.rb +67 -49
- data/lib/race_block/version.rb +1 -1
- data/race_block.gemspec +2 -4
- metadata +11 -11
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 489544d7f2e5b00afafa91544b740ffa44cc85b3f99e6dd2bf4e5f031c825a5d
|
4
|
+
data.tar.gz: 5bd94bdbb176fea865ee8353dd91319d4c8e6e455760ba4601c499405fd8e8a2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4e096f348d8b97b8d4637f5884a51fbcb1b34fd5ba19e6b41af35e6ed9ce0bd22434dbd5b923d70aeb0350b4b8fc5e77692392a7613427a913bd727bb74cc756
|
7
|
+
data.tar.gz: 60b41cebf121c933e7003ef8bdbf06bb9a667583f8bc74672fafa7d1378424c0906e68031a1912f469349565eaf04632fdb4e878c38b3e53d2a082f1d385ba70
|
data/.github/workflows/main.yml
CHANGED
@@ -23,7 +23,30 @@ jobs:
|
|
23
23
|
with:
|
24
24
|
redis-version: 6.2.4
|
25
25
|
- name: Run the default task
|
26
|
+
env:
|
27
|
+
CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }}
|
26
28
|
run: |
|
27
29
|
gem install bundler -v 2.2.6
|
28
30
|
bundle install
|
29
31
|
bundle exec rake
|
32
|
+
- name: Send coverage to CodeClimate
|
33
|
+
if: ${{ !env.ACT }}
|
34
|
+
uses: paambaati/codeclimate-action@v2.7.5
|
35
|
+
env:
|
36
|
+
CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }}
|
37
|
+
- name: Get coverage
|
38
|
+
if: ${{ !env.ACT }} && github.ref == 'refs/heads/master'
|
39
|
+
run: echo "COVERAGE=$(cat ./coverage/.last_run.json | jq -r '.result.line')%" >> $GITHUB_ENV
|
40
|
+
- name: Testing variables
|
41
|
+
if: ${{ !env.ACT }} && github.ref == 'refs/heads/master'
|
42
|
+
run: echo '' # This empty step is required for `COVERAGE` to be available in the next step, unusre why
|
43
|
+
- name: Create coverage badge
|
44
|
+
if: ${{ !env.ACT }} && github.ref == 'refs/heads/master'
|
45
|
+
uses: schneegans/dynamic-badges-action@v1.1.0
|
46
|
+
with:
|
47
|
+
auth: ${{ secrets.GIST_SECRET }}
|
48
|
+
gistID: 22954a8941d89a10237b7839e57267ec
|
49
|
+
filename: coverage.json
|
50
|
+
label: 'Coverage:'
|
51
|
+
message: ${{ env.COVERAGE }}
|
52
|
+
color: green
|
data/.rubocop_todo.yml
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# This configuration was generated by
|
2
2
|
# `rubocop --auto-gen-config`
|
3
|
-
# on 2021-06-
|
3
|
+
# on 2021-06-25 14:28:46 UTC using RuboCop version 1.17.0.
|
4
4
|
# The point is for the user to remove these configuration records
|
5
5
|
# one by one as the offenses are removed from the code base.
|
6
6
|
# Note that changes in the inspected code, or installation of new
|
@@ -9,30 +9,10 @@
|
|
9
9
|
# Offense count: 1
|
10
10
|
# Configuration parameters: IgnoredMethods, CountRepeatedAttributes.
|
11
11
|
Metrics/AbcSize:
|
12
|
-
Max:
|
12
|
+
Max: 20
|
13
13
|
|
14
14
|
# Offense count: 1
|
15
15
|
# Configuration parameters: CountComments, CountAsOne, ExcludedMethods, IgnoredMethods.
|
16
16
|
# IgnoredMethods: refine
|
17
17
|
Metrics/BlockLength:
|
18
|
-
Max:
|
19
|
-
|
20
|
-
# Offense count: 1
|
21
|
-
# Configuration parameters: IgnoredMethods.
|
22
|
-
Metrics/CyclomaticComplexity:
|
23
|
-
Max: 15
|
24
|
-
|
25
|
-
# Offense count: 1
|
26
|
-
# Configuration parameters: CountComments, CountAsOne, ExcludedMethods, IgnoredMethods.
|
27
|
-
Metrics/MethodLength:
|
28
|
-
Max: 22
|
29
|
-
|
30
|
-
# Offense count: 1
|
31
|
-
# Configuration parameters: CountKeywordArgs, MaxOptionalParameters.
|
32
|
-
Metrics/ParameterLists:
|
33
|
-
Max: 6
|
34
|
-
|
35
|
-
# Offense count: 1
|
36
|
-
# Configuration parameters: IgnoredMethods.
|
37
|
-
Metrics/PerceivedComplexity:
|
38
|
-
Max: 17
|
18
|
+
Max: 91
|
data/README.md
CHANGED
@@ -1,10 +1,14 @@
|
|
1
1
|
# RaceBlock
|
2
2
|
|
3
|
+
[![Gem Version](https://badge.fury.io/rb/race_block.svg)](https://badge.fury.io/rb/race_block)
|
3
4
|
[![Ruby](https://github.com/joeyparis/race_block/actions/workflows/main.yml/badge.svg)](https://github.com/joeyparis/race_block/actions/workflows/main.yml)
|
5
|
+
![Master Test Coverage](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/joeyparis/22954a8941d89a10237b7839e57267ec/raw/coverage.json)
|
6
|
+
[![Develop Test Coverage](https://api.codeclimate.com/v1/badges/dee875117bee3e5a72f7/test_coverage)](https://codeclimate.com/github/joeyparis/race_block/test_coverage)
|
7
|
+
[![Maintainability](https://api.codeclimate.com/v1/badges/dee875117bee3e5a72f7/maintainability)](https://codeclimate.com/github/joeyparis/race_block/maintainability)
|
4
8
|
|
5
9
|
A Ruby code block wrapper to help prevent race conditions across multiple threads and even separate servers.
|
6
10
|
|
7
|
-
|
11
|
+
>**Disclaimer** This code has been used in production for several years now without incident, but I make no guarantee about the thread-safeness it has. Use at your own risk.
|
8
12
|
|
9
13
|
## Concept
|
10
14
|
|
@@ -29,6 +33,10 @@ And then execute:
|
|
29
33
|
Or install it yourself as:
|
30
34
|
|
31
35
|
$ gem install race_block
|
36
|
+
|
37
|
+
## Requirements:
|
38
|
+
* Ruby >= 2.5
|
39
|
+
* A Redis server
|
32
40
|
|
33
41
|
## Potential Use Cases
|
34
42
|
|
@@ -39,6 +47,18 @@ The original inspiration behind this gem was running cron jobs across multiple s
|
|
39
47
|
|
40
48
|
## Usage
|
41
49
|
|
50
|
+
Before calling `RaceBlock.start` you'll need to configure your own redis client in `RaceBlock.config`
|
51
|
+
|
52
|
+
```ruby
|
53
|
+
RaceBlock.config do |config|
|
54
|
+
config.redis = Redis.new
|
55
|
+
end
|
56
|
+
|
57
|
+
# or...
|
58
|
+
|
59
|
+
RaceBlock.config.redis = Redis.new
|
60
|
+
```
|
61
|
+
|
42
62
|
Any code that you want to be "thread-safe" and ensure is only executing in one location should be placed in a `RaceBlock` with a unique identifying key that will be checked across all instances.
|
43
63
|
|
44
64
|
```ruby
|
@@ -53,9 +73,19 @@ end
|
|
53
73
|
|------|-------|-----------|
|
54
74
|
|sleep_delay|0.5|How many seconds the RaceBlock should wait after generating its unique token before it checks if it can execute the RaceBlock. **Important** This value should be longer than the amount of time it takes your server to write to the Redis database. 0.5 seconds has worked for us, but longer `sleep_delay` values should technically be safer.|
|
55
75
|
|expire|60|How many seconds the key should be stored for while running. This number should be longer than the length of time the RaceBlock will take to execute once it starts. The key will be deleted 3 seconds after the block completes, regardless of the time left from `expire`.|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
76
|
+
|expiration_delay|3|How long after block completion before the key is deleted. Setting to 0 will delete it instanttly upon block completion.|
|
77
|
+
|desync_tokens|0| **TESTING ONLY** This is purely for testing purposes to simulate inconsistent request times. It should never be used in a production environment.|
|
78
|
+
|
79
|
+
#### Changing default values
|
80
|
+
You can change the default values using `RaceBlock.config`, otherwise they can be overridden on a per call basis.
|
81
|
+
|
82
|
+
```ruby
|
83
|
+
RaceBlock.config do |config|
|
84
|
+
config.sleep_delay = 1.5
|
85
|
+
config.expire = 14
|
86
|
+
config.expiration_delay = 4
|
87
|
+
end
|
88
|
+
```
|
59
89
|
|
60
90
|
## Development
|
61
91
|
|
data/lib/race_block.rb
CHANGED
@@ -2,24 +2,40 @@
|
|
2
2
|
|
3
3
|
require "securerandom"
|
4
4
|
require "logger"
|
5
|
-
|
5
|
+
|
6
6
|
require_relative "race_block/version"
|
7
7
|
|
8
8
|
# Block for preventing race conditions across multiple threads and instances
|
9
9
|
module RaceBlock
|
10
10
|
class Error < StandardError; end
|
11
11
|
|
12
|
-
|
13
|
-
|
14
|
-
|
12
|
+
# For managing RaceBlock current configuration settings
|
13
|
+
class Configuration
|
14
|
+
attr_accessor :redis, :expire, :expiration_delay, :sleep_delay
|
15
|
+
|
16
|
+
def initialize
|
17
|
+
reset
|
18
|
+
end
|
15
19
|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
end
|
20
|
+
def reset
|
21
|
+
@expire = 60
|
22
|
+
@expiration_delay = 3
|
23
|
+
@sleep_delay = 0.5
|
21
24
|
end
|
22
|
-
|
25
|
+
end
|
26
|
+
|
27
|
+
class << self
|
28
|
+
attr_accessor :configuration
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.config
|
32
|
+
self.configuration ||= Configuration.new
|
33
|
+
yield(configuration) if block_given?
|
34
|
+
configuration
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.client
|
38
|
+
config.redis
|
23
39
|
end
|
24
40
|
|
25
41
|
def self.logger
|
@@ -34,8 +50,8 @@ module RaceBlock
|
|
34
50
|
RaceBlock.client.del(RaceBlock.key(key))
|
35
51
|
end
|
36
52
|
|
37
|
-
def self.start(key,
|
38
|
-
raise("A key must be provided to start a RaceBlock") if key.
|
53
|
+
def self.start(key, expire: config.expire, expiration_delay: config.expiration_delay, **args)
|
54
|
+
raise("A key must be provided to start a RaceBlock") if key.empty?
|
39
55
|
|
40
56
|
@key = RaceBlock.key(key)
|
41
57
|
|
@@ -45,43 +61,45 @@ module RaceBlock
|
|
45
61
|
# not set
|
46
62
|
RaceBlock.client.expire(@key, 10) if RaceBlock.client.ttl(@key) == -1
|
47
63
|
|
48
|
-
if !RaceBlock.client.get(@key)
|
49
|
-
sleep rand(0.0..sleep_delay) if desync_tokens && ENV["RACK_ENV"] == "test"
|
50
|
-
token = SecureRandom.hex
|
51
|
-
RaceBlock.client.set(@key, token)
|
52
|
-
RaceBlock.client.expire(@key, [15, sleep_delay].max)
|
53
|
-
sleep sleep_delay
|
54
|
-
# Okay, so I feel like this is pseudo science, but whatever. Our
|
55
|
-
# race condition comes from when the same cron job is called by
|
56
|
-
# several different server instances at the same time
|
57
|
-
# (theoretically) all within the same second (much less really).
|
58
|
-
# By waiting a second we can let all the same cron jobs that were
|
59
|
-
# called at roughly the exact same time finish their write to the
|
60
|
-
# redis cache so that by the time the sleep is over, only one
|
61
|
-
# token is still accurate. I'm hesitant to believe this actually
|
62
|
-
# works, but I can't find any flaws in the logic at the current
|
63
|
-
# moment, and I also believe this is what is keep the EmailQueue
|
64
|
-
# stable which seems to have no duplicate sending problems.
|
65
|
-
if RaceBlock.client.get(@key) == token
|
66
|
-
RaceBlock.client.expire(@key, expire.is_a?(Integer) ? expire : 60)
|
67
|
-
logger.info("Running block") if debug
|
68
|
-
|
69
|
-
r = yield
|
70
|
-
|
71
|
-
# I have lots of internal debates on whether I should full
|
72
|
-
# delete the key here or still let it sit for a few seconds
|
73
|
-
RaceBlock.client.expire(@key, desync_tokens && ENV["RACK_ENV"] == "test" ? 10 : 3)
|
74
|
-
|
75
|
-
RaceBlock.client.del(@key) if expire_immediately
|
76
|
-
|
77
|
-
r
|
78
|
-
elsif debug
|
79
|
-
logger.info("Token out of sync")
|
80
|
-
end
|
81
|
-
# Token out of sync
|
82
|
-
elsif debug
|
83
|
-
logger.info("Token already exists")
|
84
|
-
end
|
85
64
|
# Token already exists
|
65
|
+
return logger.debug("Token already exists") if RaceBlock.client.get(@key)
|
66
|
+
|
67
|
+
return unless set_token_and_wait(@key, **args)
|
68
|
+
|
69
|
+
RaceBlock.client.expire(@key, expire)
|
70
|
+
logger.debug("Running block")
|
71
|
+
|
72
|
+
r = yield
|
73
|
+
|
74
|
+
# I have lots of internal debates on whether I should full
|
75
|
+
# delete the key here or still let it sit for a few seconds
|
76
|
+
RaceBlock.client.expire(@key, expiration_delay)
|
77
|
+
|
78
|
+
r
|
79
|
+
end
|
80
|
+
|
81
|
+
def self.set_token_and_wait(key, sleep_delay: config.sleep_delay, desync_tokens: 0)
|
82
|
+
sleep desync_tokens # Used for testing only
|
83
|
+
token = SecureRandom.hex
|
84
|
+
RaceBlock.client.set(key, token)
|
85
|
+
RaceBlock.client.expire(key, (sleep_delay + 15).round)
|
86
|
+
sleep sleep_delay
|
87
|
+
# Okay, so I feel like this is pseudo science, but whatever. Our
|
88
|
+
# race condition comes from when the same cron job is called by
|
89
|
+
# several different server instances at the same time
|
90
|
+
# (theoretically) all within the same second (much less really).
|
91
|
+
# By waiting a second we can let all the same cron jobs that were
|
92
|
+
# called at roughly the exact same time finish their write to the
|
93
|
+
# redis cache so that by the time the sleep is over, only one
|
94
|
+
# token is still accurate. I'm hesitant to believe this actually
|
95
|
+
# works, but I can't find any flaws in the logic at the current
|
96
|
+
# moment, and I also believe this is what is keep the EmailQueue
|
97
|
+
# stable which seems to have no duplicate sending problems.
|
98
|
+
|
99
|
+
return true if RaceBlock.client.get(@key) == token
|
100
|
+
|
101
|
+
# Token out of sync
|
102
|
+
logger.debug("Token out of sync")
|
103
|
+
false
|
86
104
|
end
|
87
105
|
end
|
data/lib/race_block/version.rb
CHANGED
data/race_block.gemspec
CHANGED
@@ -11,7 +11,7 @@ Gem::Specification.new do |spec|
|
|
11
11
|
spec.summary = "A Ruby code block wrapper to help prevent race conditions " \
|
12
12
|
"across multiple threads and even separate servers."
|
13
13
|
# spec.description = "TODO: Write a longer description or delete this line."
|
14
|
-
spec.homepage = "https://
|
14
|
+
spec.homepage = "https://rubygems.org/gems/race_block"
|
15
15
|
spec.license = "MIT"
|
16
16
|
spec.required_ruby_version = Gem::Requirement.new(">= 2.5.0")
|
17
17
|
|
@@ -30,10 +30,8 @@ Gem::Specification.new do |spec|
|
|
30
30
|
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
31
31
|
spec.require_paths = ["lib"]
|
32
32
|
|
33
|
-
# Uncomment to register a new dependency of your gem
|
34
|
-
spec.add_dependency "redis", "4.0.1"
|
35
|
-
|
36
33
|
spec.add_development_dependency "rake", "13.0"
|
34
|
+
spec.add_development_dependency "redis", "4.0.1"
|
37
35
|
spec.add_development_dependency "rspec", "3.10"
|
38
36
|
spec.add_development_dependency "rubocop", "1.17"
|
39
37
|
spec.add_development_dependency "simplecov", "0.21.2"
|
metadata
CHANGED
@@ -1,43 +1,43 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: race_block
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Joey Paris
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-06-
|
11
|
+
date: 2021-06-25 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
|
-
name:
|
14
|
+
name: rake
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
17
|
- - '='
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version:
|
20
|
-
type: :
|
19
|
+
version: '13.0'
|
20
|
+
type: :development
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - '='
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version:
|
26
|
+
version: '13.0'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
|
-
name:
|
28
|
+
name: redis
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
31
|
- - '='
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version:
|
33
|
+
version: 4.0.1
|
34
34
|
type: :development
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
38
|
- - '='
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version:
|
40
|
+
version: 4.0.1
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
42
|
name: rspec
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
@@ -117,11 +117,11 @@ files:
|
|
117
117
|
- lib/race_block.rb
|
118
118
|
- lib/race_block/version.rb
|
119
119
|
- race_block.gemspec
|
120
|
-
homepage: https://
|
120
|
+
homepage: https://rubygems.org/gems/race_block
|
121
121
|
licenses:
|
122
122
|
- MIT
|
123
123
|
metadata:
|
124
|
-
homepage_uri: https://
|
124
|
+
homepage_uri: https://rubygems.org/gems/race_block
|
125
125
|
source_code_uri: https://github.com/joeyparis/race_block
|
126
126
|
post_install_message:
|
127
127
|
rdoc_options: []
|