Module: RaceBlock

Defined in:
lib/race_block.rb,
lib/race_block/version.rb

Overview

Block for preventing race conditions across multiple threads and instances

Defined Under Namespace

Classes: Configuration, Error

Constant Summary collapse

VERSION =
"0.3.0"

Class Attribute Summary collapse

Class Method Summary collapse

Class Attribute Details

.configurationObject

Returns the value of attribute configuration.



28
29
30
# File 'lib/race_block.rb', line 28

def configuration
  @configuration
end

Class Method Details

.clientObject



37
38
39
# File 'lib/race_block.rb', line 37

def self.client
  config.redis
end

.config {|configuration| ... } ⇒ Object

Yields:



31
32
33
34
35
# File 'lib/race_block.rb', line 31

def self.config
  self.configuration ||= Configuration.new
  yield(configuration) if block_given?
  configuration
end

.key(key) ⇒ Object



45
46
47
# File 'lib/race_block.rb', line 45

def self.key(key)
  "race_block_#{key}"
end

.loggerObject



41
42
43
# File 'lib/race_block.rb', line 41

def self.logger
  @logger ||= Logger.new($stdout)
end

.reset(key) ⇒ Object



49
50
51
# File 'lib/race_block.rb', line 49

def self.reset(key)
  RaceBlock.client.del(RaceBlock.key(key))
end

.set_token_and_wait(key, sleep_delay: config.sleep_delay, desync_tokens: 0) ⇒ Object



81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
# File 'lib/race_block.rb', line 81

def self.set_token_and_wait(key, sleep_delay: config.sleep_delay, desync_tokens: 0)
  sleep desync_tokens # Used for testing only
  token = SecureRandom.hex
  RaceBlock.client.set(key, token)
  RaceBlock.client.expire(key, (sleep_delay + 15).round)
  sleep sleep_delay
  # Okay, so I feel like this is pseudo science, but whatever. Our
  # race condition comes from when the same cron job is called by
  # several different server instances at the same time
  # (theoretically) all within the same second (much less really).
  # By waiting a second we can let all the same cron jobs that were
  # called at roughly the exact same time finish their write to the
  # redis cache so that by the time the sleep is over, only one
  # token is still accurate. I'm hesitant to believe this actually
  # works, but I can't find any flaws in the logic at the current
  # moment, and I also believe this is what is keep the EmailQueue
  # stable which seems to have no duplicate sending problems.

  return true if RaceBlock.client.get(key) == token

  # Token out of sync
  logger.debug("Token out of sync")
  false
end

.start(base_key, expire: config.expire, expiration_delay: config.expiration_delay, **args) ⇒ Object



53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
# File 'lib/race_block.rb', line 53

def self.start(base_key, expire: config.expire, expiration_delay: config.expiration_delay, **args)
  raise("A key must be provided to start a RaceBlock") if base_key.empty?

  key = RaceBlock.key(base_key)

  # Set an expiration for the token if the key is defined but doesn't
  # have an expiration set (happens sometimes if a thread dies early).
  # `-1` means the key is set but does not expire, `-2` means the key is
  # not set
  RaceBlock.client.expire(key, 10) if RaceBlock.client.ttl(key) == -1

  # Token already exists
  return logger.debug("Token already exists") if RaceBlock.client.get(key)

  return unless set_token_and_wait(key, **args)

  RaceBlock.client.expire(key, expire)
  logger.debug("Running block")

  r = yield

  # I have lots of internal debates on whether I should full
  # delete the key here or still let it sit for a few seconds
  RaceBlock.client.expire(key, expiration_delay)

  r
end