1
+ require 'date'
2
+
1
3
module ApplicationHelper
2
4
def hash_hash ( hash )
3
5
hash . keys . sort . map { |key | hash [ key ] } . join ( "_" )
@@ -14,25 +16,88 @@ def put_cache(hash_name, key, val, json)
14
16
val
15
17
end
16
18
19
+ def delete_cache ( hash_name , key )
20
+ $redis. hdel hash_name , key
21
+ end
22
+
17
23
# Given the name of the has in the cache to use, first attempts to retrieve an existing value
18
24
# from it with the given `key`. If no value exists, the inner function provided to the block
19
25
# will be called. Its return value will be inserted into the cache and returned from this
20
26
# function for returning to the user.
21
27
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
+
22
51
# Lock the cache entry that we're trying to write to in order to ensure that we're not
23
52
# computing the same expensive value that some other worker is already computing
24
53
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 |
26
56
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
29
68
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
32
94
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 )
34
97
raise ApplicationController ::LockError
35
98
end
36
99
end
100
+
101
+ ret
37
102
end
38
103
end
0 commit comments