Skip to content

Instantly share code, notes, and snippets.

@pmcao
Created March 17, 2016 22:24
Show Gist options
  • Save pmcao/432fd66b5a5e73a760a7 to your computer and use it in GitHub Desktop.
Save pmcao/432fd66b5a5e73a760a7 to your computer and use it in GitHub Desktop.
Thread-safe hit counter
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