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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e95fdc53873feda243ec14207fd95bbba5bc6fe37b5a45e6d56c6e85d20cf0be
4
- data.tar.gz: 34be0b8dc146d5318c476a756c9cf4b525c235b6320475360fde81da6fe8b80b
3
+ metadata.gz: 489544d7f2e5b00afafa91544b740ffa44cc85b3f99e6dd2bf4e5f031c825a5d
4
+ data.tar.gz: 5bd94bdbb176fea865ee8353dd91319d4c8e6e455760ba4601c499405fd8e8a2
5
5
  SHA512:
6
- metadata.gz: 69e136e4906f2e26ed3ebc426558fa3c345959f2505a1ef816326fb7a5275fb5c07e2e7a0e599918527b2cdd3cc7e52d9abc7bef29c0f3d5a6e372041830062b
7
- data.tar.gz: e0565376d86a6a005dfea1a88632af7c14537e21223845875720aad49bb10e6a252726b47f9cb1c98753a50e105481167766e38552818494dd777c08b0999933
6
+ metadata.gz: 4e096f348d8b97b8d4637f5884a51fbcb1b34fd5ba19e6b41af35e6ed9ce0bd22434dbd5b923d70aeb0350b4b8fc5e77692392a7613427a913bd727bb74cc756
7
+ data.tar.gz: 60b41cebf121c933e7003ef8bdbf06bb9a667583f8bc74672fafa7d1378424c0906e68031a1912f469349565eaf04632fdb4e878c38b3e53d2a082f1d385ba70
@@ -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-23 20:01:32 UTC using RuboCop version 1.17.0.
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: 43
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: 74
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
- **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.
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
- |expire_immediately|false|Whether or not the key should expire immediately after block completion, or wait the default 3 seconds.|
57
- |debug|false|Can help debug why a RaceBlock call is running or not running.|
58
- |dsync_tokens|false| **TESTING ONLY** This is purely for testing purposes to simulate inconsistent request times. It should never be used in a production environment.|
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
- require "redis"
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
- def self.client(reload: false)
13
- if @redis.nil? || reload
14
- @redis = Redis.new(host: ENV["REDIS_HOST"], port: ENV["REDIS_PORT"])
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
- begin
17
- @redis.ping
18
- rescue Redis::CannotConnectError => e
19
- RaceBlock.logger.error e
20
- end
20
+ def reset
21
+ @expire = 60
22
+ @expiration_delay = 3
23
+ @sleep_delay = 0.5
21
24
  end
22
- @redis
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, sleep_delay: 0.5, expire: 60, expire_immediately: false, debug: false, desync_tokens: false)
38
- raise("A key must be provided to start a RaceBlock") if key.nil? || key.empty?
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RaceBlock
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
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://github.com/joeyparis/race_block"
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.1.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-23 00:00:00.000000000 Z
11
+ date: 2021-06-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: redis
14
+ name: rake
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
17
  - - '='
18
18
  - !ruby/object:Gem::Version
19
- version: 4.0.1
20
- type: :runtime
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: 4.0.1
26
+ version: '13.0'
27
27
  - !ruby/object:Gem::Dependency
28
- name: rake
28
+ name: redis
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - '='
32
32
  - !ruby/object:Gem::Version
33
- version: '13.0'
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: '13.0'
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://github.com/joeyparis/race_block
120
+ homepage: https://rubygems.org/gems/race_block
121
121
  licenses:
122
122
  - MIT
123
123
  metadata:
124
- homepage_uri: https://github.com/joeyparis/race_block
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: []