Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
A non-blocking, auto-updating cache.
"""
A non-blocking, auto-updating cache.
Consider the following requirements:
- You need a cache that takes a long time to build.
- The creation of the cache is CPU bound.
- The cache needs to be rebuilt at some regular interval.
- The cache is receiving constant queries, and you do not want any blocking while it is rebuilding.
This example provides a solution to such requirements, including:
- MultiplierCache: A large cache of multiple of integers, between 0 and 10000000.
- MultiplierCacheManager: A helper that that supports async updating of a MultiplierCache.
The main method provides consistent queries using gevent, to show that there is no downtime.
"""
import gevent
from gevent import monkey
monkey.patch_all()
from contextlib import contextmanager
from multiprocessing import Process, Pipe
import time
@contextmanager
def track_time(name):
start_time = time.time()
yield # Execute wrapped code.
print('{} took {}'.format(name, time.time() - start_time))
class MultiplierCache(object):
"""
The MultiplierCache represents a cache that takes a while to build. In
this case, calling start() takes about 5 seconds and caches all multiples
of the passed in value.
Building this on the main thread while serving traffic is a bad idea,
because it would make the server unresponsive.
"""
data = {}
def __init__(self, multiple):
self.multiple = multiple
def start(self):
"""Builds the cache. This takes a while."""
print('Initializing cache for multiplier {}...'.format(self.multiple))
for i in range(10000000):
self.data[i] = i * self.multiple
print('Ready!')
def query(self, key):
return self.data[key]
@staticmethod
def run(multiple, conn):
multiplier = MultiplierCache(multiple)
multiplier.start()
conn.send('ready')
# Wait for a query, and respond to the pipe with the result, forever.
while conn.poll(None):
query_args = conn.recv()
result = multiplier.query(query_args)
conn.send(result)
class MultiplierCacheManager(object):
"""
Manages the building of the next MultiplierCache while keeping the old one
around to service queries.
"""
cache_process = None
cache_conn = None
def refresh(self, multiple):
new_cache_conn, child_conn = Pipe()
new_cache_process = Process(target=MultiplierCache.run, args=(multiple, child_conn,))
new_cache_process.start()
ready = False
while not ready:
try:
# Block using time.sleep(), so the greenlet pauses execution.
time.sleep(1)
result = new_cache_conn.recv()
ready = True
except IOError:
print('Waiting for new cache to respond...')
# The new cache is ready.
old_cache_process = self.cache_process
self.cache_conn = new_cache_conn
self.cache_process = new_cache_process
if old_cache_process:
old_cache_process.terminate()
print('Killed old cache process: {}'.format(old_cache_process))
def query(self, query):
"""Cache should be ready to go. Query and expect an immediate response."""
self.cache_conn.send(query)
self.cache_conn.poll(0.1) # Allow up to 100ms for a response.
return self.cache_conn.recv()
if __name__ == '__main__':
config = {
'query': 1,
'multiplier': 1
}
# Create the cache manager, initalizing with its first value.
with track_time('start-subprocess'):
manager = MultiplierCacheManager()
manager.refresh(config['multiplier']) # This will block for a few seconds.
# Force the cache refresh to every second, incrmenting the multiplier by 1.
def refresh_cache(manager, config):
with track_time('update-multiplier'):
new_multiplier = config['multiplier'] + 1
print('Updating multiplier: {}'.format(new_multiplier))
manager.refresh(new_multiplier) # This will be slow, but not block other greenlets.
config['multiplier'] = new_multiplier
gevent.spawn_later(1, refresh_cache, manager, config)
gevent.spawn_later(1, refresh_cache, manager, config)
# Run increment queries continuously.
while True:
with track_time('make-query'):
result = manager.query(config['query'])
print('{} x {} = {}'.format(config['query'], config['multiplier'], result))
config['query'] += 1
time.sleep(0.1)
@babldev

This comment has been minimized.

Copy link
Owner Author

babldev commented May 13, 2016

Sample output:

(venv) bash-3.2$ python test.py 
Initializing cache for multiplier 1...
Waiting for new cache to respond...
Waiting for new cache to respond...
Ready!
start-subprocess took 3.01477694511
1 x 1 = 1
make-query took 0.000189065933228
2 x 1 = 2
make-query took 0.000339031219482
3 x 1 = 3
make-query took 0.000342130661011
4 x 1 = 4
make-query took 0.000324010848999
5 x 1 = 5
make-query took 0.000274896621704
6 x 1 = 6
make-query took 0.000356197357178
7 x 1 = 7
make-query took 0.000330924987793
8 x 1 = 8
make-query took 0.000353097915649
9 x 1 = 9
make-query took 0.000337839126587
10 x 1 = 10
make-query took 0.000419139862061
Updating multiplier: 2
Initializing cache for multiplier 2...
11 x 1 = 11
make-query took 0.000344038009644
12 x 1 = 12
make-query took 0.000160932540894
13 x 1 = 13
make-query took 0.000191926956177
14 x 1 = 14
make-query took 0.00015115737915
15 x 1 = 15
make-query took 0.00017786026001
16 x 1 = 16
make-query took 0.000158786773682
17 x 1 = 17
make-query took 0.000154972076416
18 x 1 = 18
make-query took 0.00016188621521
19 x 1 = 19
make-query took 0.000156164169312
20 x 1 = 20
make-query took 0.000154972076416
Waiting for new cache to respond...
21 x 1 = 21
make-query took 0.000236988067627
22 x 1 = 22
make-query took 0.000159978866577
23 x 1 = 23
make-query took 0.000154972076416
24 x 1 = 24
make-query took 0.000149011611938
25 x 1 = 25
make-query took 0.000155925750732
26 x 1 = 26
make-query took 0.000169992446899
27 x 1 = 27
make-query took 0.000181913375854
28 x 1 = 28
make-query took 0.000173091888428
29 x 1 = 29
make-query took 0.000165939331055
30 x 1 = 30
make-query took 0.000178098678589
Waiting for new cache to respond...
31 x 1 = 31
make-query took 0.000295877456665
32 x 1 = 32
make-query took 0.00025486946106
33 x 1 = 33
make-query took 0.000169038772583
34 x 1 = 34
make-query took 0.000181198120117
35 x 1 = 35
make-query took 0.000186920166016
36 x 1 = 36
make-query took 0.000166893005371
37 x 1 = 37
make-query took 0.00017786026001
38 x 1 = 38
make-query took 0.000164985656738
39 x 1 = 39
make-query took 0.000156164169312
Waiting for new cache to respond...
40 x 1 = 40
make-query took 0.000169992446899
41 x 1 = 41
make-query took 0.000169038772583
42 x 1 = 42
make-query took 0.000188112258911
43 x 1 = 43
make-query took 0.000197172164917
Ready!
44 x 1 = 44
make-query took 0.000300884246826
45 x 1 = 45
make-query took 0.000336885452271
46 x 1 = 46
make-query took 0.000322103500366
47 x 1 = 47
make-query took 0.00031304359436
48 x 1 = 48
make-query took 0.000279903411865
49 x 1 = 49
make-query took 0.000293016433716
Killed old cache process: <Process(Process-1, started)>
update-multiplier took 4.01216602325
50 x 2 = 100
make-query took 0.000249147415161
51 x 2 = 102
make-query took 0.000301122665405
52 x 2 = 104
make-query took 0.000277996063232
53 x 2 = 106
make-query took 0.000278949737549
54 x 2 = 108
make-query took 0.00028395652771
55 x 2 = 110
make-query took 0.000278949737549
56 x 2 = 112
make-query took 0.00031304359436
57 x 2 = 114
make-query took 0.000289916992188
58 x 2 = 116
make-query took 0.000298023223877
59 x 2 = 118
make-query took 0.000289916992188
Updating multiplier: 3
Initializing cache for multiplier 3...
60 x 2 = 120
make-query took 0.000306129455566
61 x 2 = 122
make-query took 0.000197887420654
62 x 2 = 124
make-query took 0.000152826309204
63 x 2 = 126
make-query took 0.000158071517944
64 x 2 = 128
make-query took 0.000156879425049
65 x 2 = 130
make-query took 0.000157117843628
66 x 2 = 132
make-query took 0.000158071517944
67 x 2 = 134
make-query took 0.000152111053467
68 x 2 = 136
make-query took 0.000156879425049
Waiting for new cache to respond...
69 x 2 = 138
make-query took 0.000214815139771
70 x 2 = 140
make-query took 0.000245094299316
71 x 2 = 142
make-query took 0.000153064727783
72 x 2 = 144
make-query took 0.000150918960571
73 x 2 = 146
make-query took 0.000156879425049
74 x 2 = 148
make-query took 0.000159978866577
75 x 2 = 150
make-query took 0.000172138214111
76 x 2 = 152
make-query took 0.000174045562744
77 x 2 = 154
make-query took 0.000167846679688
78 x 2 = 156
make-query took 0.000164985656738
Waiting for new cache to respond...
79 x 2 = 158
make-query took 0.000182151794434
80 x 2 = 160
make-query took 0.0001540184021
81 x 2 = 162
make-query took 0.000162124633789
82 x 2 = 164
make-query took 0.000161170959473
83 x 2 = 166
make-query took 0.000153064727783
84 x 2 = 168
make-query took 0.000162839889526
85 x 2 = 170
make-query took 0.000145196914673
86 x 2 = 172
make-query took 0.000216007232666
87 x 2 = 174
make-query took 0.000162124633789
88 x 2 = 176
make-query took 0.000198841094971
Waiting for new cache to respond...
89 x 2 = 178
make-query took 0.000154972076416
Ready!
90 x 2 = 180
make-query took 0.000306129455566
91 x 2 = 182
make-query took 0.000340938568115
92 x 2 = 184
make-query took 0.000330924987793
93 x 2 = 186
make-query took 0.000325918197632
94 x 2 = 188
make-query took 0.000344038009644
95 x 2 = 190
make-query took 0.000341892242432
96 x 2 = 192
make-query took 0.000336885452271
97 x 2 = 194
make-query took 0.000334978103638
Killed old cache process: <Process(Process-2, started)>
update-multiplier took 4.00875401497
98 x 3 = 294
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.