Created
March 17, 2016 22:24
-
-
Save pmcao/432fd66b5a5e73a760a7 to your computer and use it in GitHub Desktop.
Thread-safe hit counter
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import time | |
import random | |
import threading | |
class HitCounter(): | |
"""A thread-safe website hit counter using circular array | |
N: last N seconds we want to keep the number of hits | |
Assumption: number of hit per second < sys.maxsize of Python | |
""" | |
def __init__(self, N=300): # default 5 minutes (300 seconds) | |
# Time: O(1) | |
# Space: O(N) | |
self.N = N | |
self.counter = [(None, 0)] * self.N # a fixed size list of N tuples, \ | |
# each tuple is (timestamp, number of hits) | |
self.lock = threading.Lock() # for multi-threading test | |
def log_hits(self): | |
# Increase the existing hit counter of an item in the self.counter list | |
# Time: O(1) | |
# Space: O(1) | |
epoch_time = int(time.time()) # current time | |
i = epoch_time % self.N # where should we increase the hit in the list | |
curr_timestamp, curr_hit = self.counter[i] | |
with self.lock: # obtain the lock to update the counter | |
# Case 1: initially, update the hit of a timestamp entry in the list to 1 | |
if curr_timestamp is None: | |
self.counter[i] = (epoch_time, 1) | |
# Case 2: when updating an existing timestamp entry in the list | |
# simply increase its hit count | |
elif curr_timestamp == epoch_time: # when update an existing timestamp entry in the list | |
self.counter[i] = (epoch_time, curr_hit + 1) | |
# Case 3: when updating an old timestamp entry (curr_timestamp > timestamp ) | |
# simply reset its previous hit and start counting again (Case 2) | |
else: | |
self.counter[i] = (epoch_time, 1) | |
def get_hit(self): | |
# Time: O(N) | |
# Space: O(N) | |
# Aggregate hits from recent entries in the self.counter list | |
epoch_time = int(time.time()) | |
res = 0 | |
with self.lock: | |
for (item_timestamp, item_hit) in self.counter: | |
if (item_timestamp is not None): | |
res += item_hit if ((epoch_time - item_timestamp) < self.N) else 0 | |
return res | |
def client(hc): | |
# A client worker which randomly sleeps an amount of time (from 0..1) then calls log_hits() | |
# log_hits() is called for num_iterations times | |
num_iterations = 100 | |
for x in range(num_iterations): | |
time.sleep(random.random()) | |
hc.log_hits() | |
curr_hit = hc.get_hit() | |
print("{}:\tget_hit() = {}".format( | |
threading.currentThread().getName(), | |
curr_hit) | |
) | |
class HitCounterTest(): | |
# A hitcounter test | |
# Creates client workers to test the HitCounter concurrently | |
def test(self): | |
hc = HitCounter() | |
clients = [] | |
num_clients = 10 | |
for i in range(num_clients): | |
t = threading.Thread(target=client, args=(hc,)) | |
clients.append(t) | |
t.start() | |
HitCounterTest().test() | |
# % python3 counter.py | |
# Thread-8: get_hit() = 1 | |
# Thread-6: get_hit() = 2 | |
# Thread-8: get_hit() = 3 | |
# Thread-3: get_hit() = 4 | |
# Thread-1: get_hit() = 5 | |
# Thread-10: get_hit() = 6 | |
# Thread-7: get_hit() = 7 | |
# Thread-9: get_hit() = 8 | |
# Thread-2: get_hit() = 9 | |
# Thread-6: get_hit() = 10 | |
# Thread-5: get_hit() = 11 | |
# Thread-10: get_hit() = 12 | |
# Thread-4: get_hit() = 13 | |
# Thread-6: get_hit() = 14 | |
# Thread-8: get_hit() = 15 | |
# Thread-5: get_hit() = 16 | |
# Thread-10: get_hit() = 17 | |
# Thread-5: get_hit() = 18 | |
# Thread-3: get_hit() = 19 | |
# Thread-9: get_hit() = 20 | |
# Thread-2: get_hit() = 21 | |
# Thread-1: get_hit() = 22 | |
# Thread-7: get_hit() = 23 | |
# ... |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment