Skip to content

Commit 578b743

Browse files
committed
Try to make locking work better
1 parent 2bfad9e commit 578b743

File tree

3 files changed

+84
-19
lines changed

3 files changed

+84
-19
lines changed

backend/app/helpers/application_helper.rb

Lines changed: 71 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
require 'date'
2+
13
module ApplicationHelper
24
def hash_hash(hash)
35
hash.keys.sort.map { |key| hash[key] }.join("_")
@@ -14,25 +16,88 @@ def put_cache(hash_name, key, val, json)
1416
val
1517
end
1618

19+
def delete_cache(hash_name, key)
20+
$redis.hdel hash_name, key
21+
end
22+
1723
# Given the name of the has in the cache to use, first attempts to retrieve an existing value
1824
# from it with the given `key`. If no value exists, the inner function provided to the block
1925
# will be called. Its return value will be inserted into the cache and returned from this
2026
# function for returning to the user.
2127
def with_cache(hash_name, key, json: true)
28+
cached = get_cache hash_name, key
29+
30+
# If the cache value is currently being computed, we bail out early and expect the client to
31+
# re-request in a bit.
32+
#
33+
# If the value has been computing for longer than the cutoff, we assume the old computation
34+
# failed and compute it ourselves
35+
is_recomputing = false
36+
if cached
37+
if cached.start_with? "__computing__"
38+
date_string = cached.split("__computing__")[1]
39+
date = Date.parse(date_string)
40+
41+
if date < 3.minute.ago.utc
42+
raise ApplicationController::LockError
43+
else
44+
is_recomputing = true
45+
end
46+
else
47+
return cached
48+
end
49+
end
50+
2251
# Lock the cache entry that we're trying to write to in order to ensure that we're not
2352
# computing the same expensive value that some other worker is already computing
2453
lock_key = "#{hash_name}_#{key}"
25-
$lock_manager.lock(lock_key, 240000) do |locked|
54+
ret = nil
55+
$lock_manager.lock(lock_key, 18502) do |locked|
2656
if locked
27-
cached = get_cache hash_name, key
28-
return cached if cached
57+
begin
58+
# It's possible someone else was computing this while we were waiting for the lock. If so, we
59+
# just return that and avoid re-computing
60+
refreshed_cached = get_cache hash_name, key
61+
62+
if cached != refreshed_cached
63+
if refreshed_cached.starts_with? "__computing__"
64+
# Someone else beat us to the lock and is re-computing the value, so we just let them do it
65+
$lock_manager.unlock(locked)
66+
raise ApplicationController::LockError
67+
end
2968

30-
# Compute the new value while holding the lock, set it in the cache, and then drop the lock
31-
return put_cache hash_name, key, yield, json
69+
# Someone else populated the cache fully already so we bail and return that
70+
ret = refreshed_cached
71+
next
72+
end
73+
74+
# Create a placeholder value in the cache indicating that the value is currently being computed
75+
# and when the computation started
76+
now = Time.new
77+
date_string = now.strftime("%Y-%m-%d %H:%M:%S")
78+
placeholder_val = "__computing__#{date_string}"
79+
put_cache hash_name, key, placeholder_val, false
80+
81+
# Compute the new value while holding the lock, set it in the cache, and then drop the lock
82+
ret = put_cache hash_name, key, yield, json
83+
rescue ApplicationController::NotFound
84+
delete_cache hash_name, key
85+
$lock_manager.unlock(locked)
86+
raise ApplicationController::NotFound
87+
rescue => e
88+
p "ERROR in critical section while holding lock:"
89+
p e
90+
delete_cache hash_name, key
91+
$lock_manager.unlock(locked)
92+
raise ApplicationController::LockError
93+
end
3294
else
33-
p "ERROR: Failed to acquire a lock for key #{lock_key} in the timeout period"
95+
p "Failed to acquire a lock for key #{lock_key} in the timeout period"
96+
$lock_manager.unlock(locked)
3497
raise ApplicationController::LockError
3598
end
3699
end
100+
101+
ret
37102
end
38103
end

backend/config/initializers/redis.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@
44
redis_uri = "redis://" + ENV.fetch("REDIS_HOST", "localhost")
55
$redis = Redis.new(url: redis_uri)
66
Redis.current = Redis.new(url: redis_uri)
7-
$lock_manager = Redlock::Client.new([redis_uri], { retry_count: 240, retry_delay: 500, retry_jitter: 50 })
7+
$lock_manager = Redlock::Client.new([redis_uri], { retry_count: 38, retry_delay: 500, retry_jitter: 10 })

stocktwits-bot/src/top_popularity_changes.py

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -124,18 +124,18 @@ def post_tweet_from_changes(
124124

125125
def run_top_popularity_changes(dry_run: bool):
126126
# Fetch the top popularity changes by percent from Robintrack
127-
top_percent_changes = requests.get(TOP_PERCENT_POPULARITY_CHANGES_URL).json()
128-
# Post to StockTwits
129-
post_twit_from_changes(top_percent_changes, create_percent_change_twit_content, dry_run)
130-
# Post to Twitter
131-
post_tweet_from_changes(top_percent_changes, create_percent_change_twit_content, dry_run)
132-
133-
# Fetch the top popularity increases by absolute diff from Robintrack
134-
top_absolute_increases = requests.get(TOP_ABSOLUTE_POPULARITY_INCREASES_URL).json()
135-
# Post to StockTwits
136-
post_twit_from_changes(top_absolute_increases, create_absolute_change_twit_content, dry_run)
137-
# Post to Twitter
138-
post_tweet_from_changes(top_absolute_increases, create_absolute_change_twit_content, dry_run)
127+
# top_percent_changes = requests.get(TOP_PERCENT_POPULARITY_CHANGES_URL).json()
128+
# # Post to StockTwits
129+
# post_twit_from_changes(top_percent_changes, create_percent_change_twit_content, dry_run)
130+
# # Post to Twitter
131+
# post_tweet_from_changes(top_percent_changes, create_percent_change_twit_content, dry_run)
132+
133+
# # Fetch the top popularity increases by absolute diff from Robintrack
134+
# top_absolute_increases = requests.get(TOP_ABSOLUTE_POPULARITY_INCREASES_URL).json()
135+
# # Post to StockTwits
136+
# post_twit_from_changes(top_absolute_increases, create_absolute_change_twit_content, dry_run)
137+
# # Post to Twitter
138+
# post_tweet_from_changes(top_absolute_increases, create_absolute_change_twit_content, dry_run)
139139

140140
# Fetch the top popularity decreases by absolute diff from Robintrack
141141
top_absolute_decreases = requests.get(TOP_ABSOLUTE_POPULARITY_DECREASES_URL).json()

0 commit comments

Comments
 (0)