aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--generalresearch/managers/thl/user_manager/memcached_user_manager.py49
-rw-r--r--generalresearch/managers/thl/user_manager/mysql_user_manager.py21
-rw-r--r--generalresearch/managers/thl/user_manager/user_manager.py24
3 files changed, 29 insertions, 65 deletions
diff --git a/generalresearch/managers/thl/user_manager/memcached_user_manager.py b/generalresearch/managers/thl/user_manager/memcached_user_manager.py
deleted file mode 100644
index d2c68ed..0000000
--- a/generalresearch/managers/thl/user_manager/memcached_user_manager.py
+++ /dev/null
@@ -1,49 +0,0 @@
-# from typing import List, Optional
-#
-# import pylibmc
-#
-# from generalresearch.models.thl.user import User
-#
-#
-# class MemcachedUserManager:
-# def __init__(self, servers: List[str], cache_prefix: Optional[str] = None):
-# self.servers = servers
-# self.cache_prefix = cache_prefix if cache_prefix else "user-lookup"
-#
-# def create_client(self):
-# # Clients are NOT thread safe. Make a new one each time
-#
-# # There's a receive_timeout and send_timeout also, but the documentation is incomprehensible,
-# # and they don't seem to do anything??? (I tested setting them at 1ms and I can't
-# # get it to fail)
-# # https://sendapatch.se/projects/pylibmc/behaviors.html
-# mc_client = pylibmc.Client(servers=self.servers, binary=True,
-# behaviors={'connect_timeout': 100})
-# return mc_client
-#
-# def get_user(self, *, product_id: str = None, product_user_id: str = None, user_id: int = None,
-# user_uuid: UUIDStr = None) -> User:
-# # assume we did input validation in user_manager.get_user() function
-# mc_client = self.create_client()
-# if user_uuid:
-# d = mc_client.get(f"{self.cache_prefix}:uuid:{user_uuid}")
-# elif user_id:
-# d = mc_client.get(f"{self.cache_prefix}:user_id:{user_id}")
-# else:
-# d = mc_client.get(f"{self.cache_prefix}:ubp:{product_id}:{product_user_id}")
-# if d:
-# return User.model_validate_json(d)
-#
-# def set_user(self, user: User):
-# d = user.to_json()
-# mc_client = self.create_client()
-# mc_client.set(f"{self.cache_prefix}:uuid:{user.uuid}", d, time=60 * 60 * 24)
-# mc_client.set(f"{self.cache_prefix}:user_id:{user.user_id}", d, time=60 * 60 * 24)
-# mc_client.set(f"{self.cache_prefix}:ubp:{user.product_id}:{user.product_user_id}", d, time=60 * 60 * 24)
-#
-# def clear_user(self, user: User):
-# # this should only be used by tests
-# mc_client = self.create_client()
-# mc_client.delete(f"{self.cache_prefix}:uuid:{user.uuid}")
-# mc_client.delete(f"{self.cache_prefix}:user_id:{user.user_id}")
-# mc_client.delete(f"{self.cache_prefix}:ubp:{user.product_id}:{user.product_user_id}")
diff --git a/generalresearch/managers/thl/user_manager/mysql_user_manager.py b/generalresearch/managers/thl/user_manager/mysql_user_manager.py
index dbed5de..22abaa8 100644
--- a/generalresearch/managers/thl/user_manager/mysql_user_manager.py
+++ b/generalresearch/managers/thl/user_manager/mysql_user_manager.py
@@ -1,11 +1,13 @@
from __future__ import annotations
import logging
+import operator
from collections.abc import Collection
from datetime import datetime, timezone
-from functools import lru_cache
+from threading import Lock
from uuid import uuid4
+from cachetools import LRUCache, cachedmethod
import psycopg
from psycopg import sql
@@ -22,6 +24,8 @@ class MysqlUserManager:
def __init__(self, pg_config: PostgresConfig, is_read_replica: bool):
self.pg_config = pg_config
self.is_read_replica = is_read_replica
+ self.product_id_exists_cache = LRUCache(maxsize=5000)
+ self.product_id_exists_cache_lock = Lock()
def _set_last_seen(self, user: User) -> None:
# Don't call this directly. Use UserManager.set_last_seen()
@@ -143,7 +147,7 @@ class MysqlUserManager:
with conn.cursor() as c:
c.execute(query=query, params=params)
user_id = c.fetchone()["id"]
- except psycopg.IntegrityError as e:
+ except psycopg.IntegrityError:
# Two machines/processes are trying to create this same (product_id, product_user_id)
# at the same time. There's a unique index, so mysql will not let two be created.
# The 2nd should get an IntegrityError, meaning this already exists, and we can just query it.
@@ -160,7 +164,7 @@ class MysqlUserManager:
else:
# We specifically queried the NON read-replica, and we got an IntegrityError, so
# something else must be wrong...
- raise e
+ raise
else:
user = User(
user_id=user_id,
@@ -173,8 +177,11 @@ class MysqlUserManager:
return user
- @lru_cache(maxsize=5000)
- def product_id_exists(self, product_id: str):
+ @cachedmethod(
+ operator.attrgetter("product_id_exists_cache"),
+ lock=operator.attrgetter("product_id_exists_cache_lock"),
+ )
+ def product_id_exists(self, product_id: str) -> bool:
# 'id' is the primary key, there can only be 0 or 1
query = """
SELECT id
@@ -254,10 +261,10 @@ class MysqlUserManager:
user_ids and user_uuids
), "Must pass ONE of user_ids, user_uuids"
if user_ids:
- assert len(user_ids) <= 500, "limit 500 user_ids"
assert isinstance(
user_ids, (list, set)
), "must pass a collection of user_ids"
+ assert len(user_ids) <= 500, "limit 500 user_ids"
res = self.pg_config.execute_sql_query(
query="""
@@ -270,10 +277,10 @@ class MysqlUserManager:
params={"user_ids": user_ids},
)
else:
- assert len(user_uuids) <= 500, "limit 500 user_uuids"
assert isinstance(
user_uuids, (list, set)
), "must pass a collection of user_uuids"
+ assert len(user_uuids) <= 500, "limit 500 user_uuids"
res = self.pg_config.execute_sql_query(
query="""
SELECT id AS user_id, product_id, product_user_id,
diff --git a/generalresearch/managers/thl/user_manager/user_manager.py b/generalresearch/managers/thl/user_manager/user_manager.py
index 3794020..5da3370 100644
--- a/generalresearch/managers/thl/user_manager/user_manager.py
+++ b/generalresearch/managers/thl/user_manager/user_manager.py
@@ -1,10 +1,12 @@
from __future__ import annotations
import logging
+import operator
from collections.abc import Collection
from datetime import datetime
-from functools import lru_cache
+from threading import Lock
+from cachetools import TTLCache, cachedmethod
from pydantic import RedisDsn
from generalresearch.managers.base import Permission
@@ -78,6 +80,8 @@ class UserManager:
self.product_manager = ProductManager(
pg_config=pg_config, permissions=[Permission.READ]
)
+ self.get_user_cache = TTLCache(maxsize=10000, ttl=30)
+ self.get_user_cache_lock = Lock()
def set_last_seen(self, user: User) -> None:
assert Permission.UPDATE in self.sql_permissions, "permission error"
@@ -105,14 +109,17 @@ class UserManager:
return None
- def cache_clear(self):
- # Generally this is used in testing. This clears the .get_user's lru_cache.
- # There is no way of clearing only a specific key from the cache.
+ def cache_clear(self) -> None:
+ # Generally this is used in testing. This clears get_user's TTL cache.
# It does not clear any redis caches; that has to be done separately.
- self.get_user.__wrapped__.cache_clear()
+ with self.get_user_cache_lock:
+ self.get_user_cache.clear()
@deepcopy_return
- @lru_cache(maxsize=10000)
+ @cachedmethod(
+ operator.attrgetter("get_user_cache"),
+ lock=operator.attrgetter("get_user_cache_lock"),
+ )
def get_user(
self,
*,
@@ -123,7 +130,7 @@ class UserManager:
) -> User:
"""
Retrieve User from (product_id & product_user_id) or (user_id), or (uuid).
- Looks up in lru_cache, then (redis, memcached), then mysql.
+ Looks up in the 30-second TTL cache, then (redis, memcached), then mysql.
Raises UserDoesntExistError if user is not found.
(the * makes all arguments keyword-only arguments)
"""
@@ -316,8 +323,7 @@ class UserManager:
# If we change something about a user, we should update the in-memory caches
self.set_user_inmemory_cache(user)
- # There is no way to clear a single key from the lru_cache...
- # https://bugs.python.org/issue28178
+ # Clear local cached copies so the blocked state is returned immediately.
self.cache_clear()
return True