moved
This commit is contained in:
@@ -3,8 +3,8 @@ from functools import wraps
|
||||
import httpx
|
||||
|
||||
from resolvers.stat import get_with_stat
|
||||
from services.cache import get_cached_author_by_user_id
|
||||
from services.logger import root_logger as logger
|
||||
from cache.cache import get_cached_author_by_user_id
|
||||
from utils.logger import root_logger as logger
|
||||
from settings import ADMIN_SECRET, AUTH_URL
|
||||
|
||||
|
||||
|
@@ -1,342 +0,0 @@
|
||||
import asyncio
|
||||
import json
|
||||
from typing import List
|
||||
|
||||
from sqlalchemy import select, join, and_
|
||||
from orm.author import Author, AuthorFollower
|
||||
from orm.topic import Topic, TopicFollower
|
||||
from orm.shout import Shout, ShoutAuthor, ShoutTopic
|
||||
from services.db import local_session
|
||||
from services.encoders import CustomJSONEncoder
|
||||
from services.rediscache import redis
|
||||
from services.logger import root_logger as logger
|
||||
|
||||
DEFAULT_FOLLOWS = {
|
||||
"topics": [],
|
||||
"authors": [],
|
||||
"communities": [{"id": 1, "name": "Дискурс", "slug": "discours", "pic": ""}],
|
||||
}
|
||||
|
||||
|
||||
# Кэширование данных темы
|
||||
async def cache_topic(topic: dict):
|
||||
payload = json.dumps(topic, cls=CustomJSONEncoder)
|
||||
# Одновременное кэширование по id и slug для быстрого доступа
|
||||
await asyncio.gather(
|
||||
redis.execute("SET", f"topic:id:{topic['id']}", payload),
|
||||
redis.execute("SET", f"topic:slug:{topic['slug']}", payload),
|
||||
)
|
||||
|
||||
|
||||
# Кэширование данных автора
|
||||
async def cache_author(author: dict):
|
||||
payload = json.dumps(author, cls=CustomJSONEncoder)
|
||||
# Кэширование данных автора по user и id
|
||||
await asyncio.gather(
|
||||
redis.execute("SET", f"author:user:{author['user'].strip()}", str(author["id"])),
|
||||
redis.execute("SET", f"author:id:{author['id']}", payload),
|
||||
)
|
||||
|
||||
|
||||
async def get_cached_topic(topic_id: int):
|
||||
"""
|
||||
Получает информацию о теме из кэша или базы данных.
|
||||
|
||||
Args:
|
||||
topic_id (int): Идентификатор темы.
|
||||
|
||||
Returns:
|
||||
dict: Данные темы в формате словаря или None, если тема не найдена.
|
||||
"""
|
||||
# Ключ для кэширования темы в Redis
|
||||
topic_key = f"topic:id:{topic_id}"
|
||||
cached_topic = await redis.get(topic_key)
|
||||
if cached_topic:
|
||||
return json.loads(cached_topic)
|
||||
|
||||
# Если данных о теме нет в кэше, загружаем из базы данных
|
||||
with local_session() as session:
|
||||
topic = session.execute(select(Topic).where(Topic.id == topic_id)).scalar_one_or_none()
|
||||
if topic:
|
||||
# Кэшируем полученные данные
|
||||
topic_dict = topic.dict()
|
||||
await redis.set(topic_key, json.dumps(topic_dict, cls=CustomJSONEncoder))
|
||||
return topic_dict
|
||||
|
||||
return None
|
||||
|
||||
|
||||
async def get_cached_shout_authors(shout_id: int):
|
||||
"""
|
||||
Retrieves a list of authors for a given shout from the cache or database if not present.
|
||||
|
||||
Args:
|
||||
shout_id (int): The ID of the shout for which to retrieve authors.
|
||||
|
||||
Returns:
|
||||
List[dict]: A list of dictionaries containing author data.
|
||||
"""
|
||||
# Attempt to retrieve cached author IDs for the shout
|
||||
rkey = f"shout:authors:{shout_id}"
|
||||
cached_author_ids = await redis.get(rkey)
|
||||
if cached_author_ids:
|
||||
author_ids = json.loads(cached_author_ids)
|
||||
else:
|
||||
# If not in cache, fetch from the database and cache the result
|
||||
with local_session() as session:
|
||||
query = (
|
||||
select(ShoutAuthor.author)
|
||||
.where(ShoutAuthor.shout == shout_id)
|
||||
.join(Author, ShoutAuthor.author == Author.id)
|
||||
.filter(Author.deleted_at.is_(None))
|
||||
)
|
||||
author_ids = [author_id for (author_id,) in session.execute(query).all()]
|
||||
await redis.execute("set", rkey, json.dumps(author_ids))
|
||||
|
||||
# Retrieve full author details from cached IDs
|
||||
if author_ids:
|
||||
authors = await get_cached_authors_by_ids(author_ids)
|
||||
logger.debug(f"Shout#{shout_id} authors fetched and cached: {len(authors)} authors found.")
|
||||
return authors
|
||||
|
||||
return []
|
||||
|
||||
|
||||
# Кэширование данных о подписках
|
||||
async def cache_follows(follower_id: int, entity_type: str, entity_id: int, is_insert=True):
|
||||
key = f"author:follows-{entity_type}s:{follower_id}"
|
||||
follows_str = await redis.get(key)
|
||||
follows = json.loads(follows_str) if follows_str else []
|
||||
if is_insert:
|
||||
if entity_id not in follows:
|
||||
follows.append(entity_id)
|
||||
else:
|
||||
follows = [eid for eid in follows if eid != entity_id]
|
||||
await redis.execute("set", key, json.dumps(follows, cls=CustomJSONEncoder))
|
||||
update_follower_stat(follower_id, entity_type, len(follows))
|
||||
|
||||
|
||||
# Обновление статистики подписчика
|
||||
async def update_follower_stat(follower_id, entity_type, count):
|
||||
follower_key = f"author:id:{follower_id}"
|
||||
follower_str = await redis.get(follower_key)
|
||||
follower = json.loads(follower_str) if follower_str else None
|
||||
if follower:
|
||||
follower["stat"] = {f"{entity_type}s": count}
|
||||
await cache_author(follower)
|
||||
|
||||
|
||||
# Получение автора из кэша
|
||||
async def get_cached_author(author_id: int):
|
||||
author_key = f"author:id:{author_id}"
|
||||
result = await redis.get(author_key)
|
||||
if result:
|
||||
return json.loads(result)
|
||||
# Загрузка из базы данных, если не найдено в кэше
|
||||
with local_session() as session:
|
||||
author = session.execute(select(Author).where(Author.id == author_id)).scalar_one_or_none()
|
||||
if author:
|
||||
await cache_author(author.dict())
|
||||
return author.dict()
|
||||
return None
|
||||
|
||||
|
||||
# Получение темы по slug из кэша
|
||||
async def get_cached_topic_by_slug(slug: str):
|
||||
topic_key = f"topic:slug:{slug}"
|
||||
result = await redis.get(topic_key)
|
||||
if result:
|
||||
return json.loads(result)
|
||||
# Загрузка из базы данных, если не найдено в кэше
|
||||
with local_session() as session:
|
||||
topic = session.execute(select(Topic).where(Topic.slug == slug)).scalar_one_or_none()
|
||||
if topic:
|
||||
await cache_topic(topic.dict())
|
||||
return topic.dict()
|
||||
return None
|
||||
|
||||
|
||||
# Получение списка авторов по ID из кэша
|
||||
async def get_cached_authors_by_ids(author_ids: List[int]) -> List[dict]:
|
||||
# Одновременное получение данных всех авторов
|
||||
keys = [f"author:id:{author_id}" for author_id in author_ids]
|
||||
results = await asyncio.gather(*(redis.get(key) for key in keys))
|
||||
authors = [json.loads(result) if result else None for result in results]
|
||||
# Загрузка отсутствующих авторов из базы данных и кэширование
|
||||
missing_indices = [index for index, author in enumerate(authors) if author is None]
|
||||
if missing_indices:
|
||||
missing_ids = [author_ids[index] for index in missing_indices]
|
||||
with local_session() as session:
|
||||
query = select(Author).where(Author.id.in_(missing_ids))
|
||||
missing_authors = session.execute(query).scalars().all()
|
||||
await asyncio.gather(*(cache_author(author.dict()) for author in missing_authors))
|
||||
authors = [author.dict() for author in missing_authors]
|
||||
return authors
|
||||
|
||||
|
||||
async def get_cached_topic_followers(topic_id: int):
|
||||
# Попытка извлечь кэшированные данные
|
||||
cached = await redis.get(f"topic:followers:{topic_id}")
|
||||
if cached:
|
||||
followers = json.loads(cached)
|
||||
logger.debug(f"Cached followers for topic#{topic_id}: {len(followers)}")
|
||||
return followers
|
||||
|
||||
# Загрузка из базы данных и кэширование результатов
|
||||
with local_session() as session:
|
||||
followers_ids = [
|
||||
f[0]
|
||||
for f in session.query(Author.id)
|
||||
.join(TopicFollower, TopicFollower.follower == Author.id)
|
||||
.filter(TopicFollower.topic == topic_id)
|
||||
.all()
|
||||
]
|
||||
await redis.execute("SET", f"topic:followers:{topic_id}", json.dumps(followers_ids))
|
||||
followers = await get_cached_authors_by_ids(followers_ids)
|
||||
return followers
|
||||
|
||||
|
||||
async def get_cached_author_followers(author_id: int):
|
||||
# Проверяем кэш на наличие данных
|
||||
cached = await redis.get(f"author:followers:{author_id}")
|
||||
if cached:
|
||||
followers_ids = json.loads(cached)
|
||||
followers = await get_cached_authors_by_ids(followers_ids)
|
||||
logger.debug(f"Cached followers for author#{author_id}: {len(followers)}")
|
||||
return followers
|
||||
|
||||
# Запрос в базу данных если кэш пуст
|
||||
with local_session() as session:
|
||||
followers_ids = [
|
||||
f[0]
|
||||
for f in session.query(Author.id)
|
||||
.join(AuthorFollower, AuthorFollower.follower == Author.id)
|
||||
.filter(AuthorFollower.author == author_id, Author.id != author_id)
|
||||
.all()
|
||||
]
|
||||
await redis.execute("SET", f"author:followers:{author_id}", json.dumps(followers_ids))
|
||||
followers = await get_cached_authors_by_ids(followers_ids)
|
||||
return followers
|
||||
|
||||
|
||||
async def get_cached_follower_authors(author_id: int):
|
||||
# Попытка получить авторов из кэша
|
||||
cached = await redis.get(f"author:follows-authors:{author_id}")
|
||||
if cached:
|
||||
authors_ids = json.loads(cached)
|
||||
else:
|
||||
# Запрос авторов из базы данных
|
||||
with local_session() as session:
|
||||
authors_ids = [
|
||||
a[0]
|
||||
for a in session.execute(
|
||||
select(Author.id)
|
||||
.select_from(join(Author, AuthorFollower, Author.id == AuthorFollower.author))
|
||||
.where(AuthorFollower.follower == author_id)
|
||||
).all()
|
||||
]
|
||||
await redis.execute("SET", f"author:follows-authors:{author_id}", json.dumps(authors_ids))
|
||||
|
||||
authors = await get_cached_authors_by_ids(authors_ids)
|
||||
return authors
|
||||
|
||||
|
||||
async def get_cached_follower_topics(author_id: int):
|
||||
# Попытка получить темы из кэша
|
||||
cached = await redis.get(f"author:follows-topics:{author_id}")
|
||||
if cached:
|
||||
topics_ids = json.loads(cached)
|
||||
else:
|
||||
# Загрузка тем из базы данных и их кэширование
|
||||
with local_session() as session:
|
||||
topics_ids = [
|
||||
t[0]
|
||||
for t in session.query(Topic.id)
|
||||
.join(TopicFollower, TopicFollower.topic == Topic.id)
|
||||
.where(TopicFollower.follower == author_id)
|
||||
.all()
|
||||
]
|
||||
await redis.execute("SET", f"author:follows-topics:{author_id}", json.dumps(topics_ids))
|
||||
|
||||
topics = []
|
||||
for topic_id in topics_ids:
|
||||
topic_str = await redis.get(f"topic:id:{topic_id}")
|
||||
if topic_str:
|
||||
topic = json.loads(topic_str)
|
||||
if topic and topic not in topics:
|
||||
topics.append(topic)
|
||||
|
||||
logger.debug(f"Cached topics for author#{author_id}: {len(topics)}")
|
||||
return topics
|
||||
|
||||
|
||||
async def get_cached_author_by_user_id(user_id: str):
|
||||
"""
|
||||
Получает информацию об авторе по его user_id, сначала проверяя кэш, а затем базу данных.
|
||||
|
||||
Args:
|
||||
user_id (str): Идентификатор пользователя, по которому нужно получить автора.
|
||||
|
||||
Returns:
|
||||
dict: Словарь с данными автора или None, если автор не найден.
|
||||
"""
|
||||
# Пытаемся найти ID автора по user_id в кэше Redis
|
||||
author_id = await redis.get(f"author:user:{user_id.strip()}")
|
||||
if author_id:
|
||||
# Если ID найден, получаем полные данные автора по его ID
|
||||
author_data = await redis.get(f"author:id:{author_id}")
|
||||
if author_data:
|
||||
return json.loads(author_data)
|
||||
|
||||
# Если данные в кэше не найдены, выполняем запрос к базе данных
|
||||
with local_session() as session:
|
||||
author = session.execute(select(Author).where(Author.user == user_id)).scalar_one_or_none()
|
||||
|
||||
if author:
|
||||
# Кэшируем полученные данные автора
|
||||
author_dict = author.dict()
|
||||
await asyncio.gather(
|
||||
redis.execute("SET", f"author:user:{user_id.strip()}", str(author.id)),
|
||||
redis.execute("SET", f"author:id:{author.id}", json.dumps(author_dict)),
|
||||
)
|
||||
return author_dict
|
||||
|
||||
# Возвращаем None, если автор не найден
|
||||
return None
|
||||
|
||||
|
||||
async def get_cached_topic_authors(topic_id: int):
|
||||
"""
|
||||
Получает список авторов для заданной темы, используя кэш или базу данных.
|
||||
|
||||
Args:
|
||||
topic_id (int): Идентификатор темы, для которой нужно получить авторов.
|
||||
|
||||
Returns:
|
||||
List[dict]: Список словарей, содержащих данные авторов.
|
||||
"""
|
||||
# Пытаемся получить список ID авторов из кэша
|
||||
rkey = f"topic:authors:{topic_id}"
|
||||
cached_authors_ids = await redis.get(rkey)
|
||||
if cached_authors_ids:
|
||||
authors_ids = json.loads(cached_authors_ids)
|
||||
else:
|
||||
# Если кэш пуст, получаем данные из базы данных
|
||||
with local_session() as session:
|
||||
query = (
|
||||
select(ShoutAuthor.author)
|
||||
.select_from(join(ShoutTopic, Shout, ShoutTopic.shout == Shout.id))
|
||||
.join(ShoutAuthor, ShoutAuthor.shout == Shout.id)
|
||||
.where(and_(ShoutTopic.topic == topic_id, Shout.published_at.is_not(None), Shout.deleted_at.is_(None)))
|
||||
)
|
||||
authors_ids = [author_id for (author_id,) in session.execute(query).all()]
|
||||
# Кэшируем полученные ID авторов
|
||||
await redis.execute("set", rkey, json.dumps(authors_ids))
|
||||
|
||||
# Получаем полные данные авторов по кэшированным ID
|
||||
if authors_ids:
|
||||
authors = await get_cached_authors_by_ids(authors_ids)
|
||||
logger.debug(f"Topic#{topic_id} authors fetched and cached: {len(authors)} authors found.")
|
||||
return authors
|
||||
|
||||
return []
|
@@ -10,7 +10,7 @@ from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.orm import Session, configure_mappers
|
||||
from sqlalchemy.sql.schema import Table
|
||||
|
||||
from services.logger import root_logger as logger
|
||||
from utils.logger import root_logger as logger
|
||||
from settings import DB_URL
|
||||
|
||||
# from sqlalchemy_searchable import make_searchable
|
||||
|
@@ -1,47 +0,0 @@
|
||||
import re
|
||||
from difflib import ndiff
|
||||
|
||||
|
||||
def get_diff(original, modified):
|
||||
"""
|
||||
Get the difference between two strings using difflib.
|
||||
|
||||
Parameters:
|
||||
- original: The original string.
|
||||
- modified: The modified string.
|
||||
|
||||
Returns:
|
||||
A list of differences.
|
||||
"""
|
||||
diff = list(ndiff(original.split(), modified.split()))
|
||||
return diff
|
||||
|
||||
|
||||
def apply_diff(original, diff):
|
||||
"""
|
||||
Apply the difference to the original string.
|
||||
|
||||
Parameters:
|
||||
- original: The original string.
|
||||
- diff: The difference obtained from get_diff function.
|
||||
|
||||
Returns:
|
||||
The modified string.
|
||||
"""
|
||||
result = []
|
||||
pattern = re.compile(r"^(\+|-) ")
|
||||
|
||||
for line in diff:
|
||||
match = pattern.match(line)
|
||||
if match:
|
||||
op = match.group(1)
|
||||
content = line[2:]
|
||||
if op == "+":
|
||||
result.append(content)
|
||||
elif op == "-":
|
||||
# Ignore deleted lines
|
||||
pass
|
||||
else:
|
||||
result.append(line)
|
||||
|
||||
return " ".join(result)
|
@@ -1,9 +0,0 @@
|
||||
import json
|
||||
from decimal import Decimal
|
||||
|
||||
|
||||
class CustomJSONEncoder(json.JSONEncoder):
|
||||
def default(self, obj):
|
||||
if isinstance(obj, Decimal):
|
||||
return str(obj)
|
||||
return super().default(obj)
|
@@ -1,72 +0,0 @@
|
||||
import logging
|
||||
|
||||
import colorlog
|
||||
|
||||
# Define the color scheme
|
||||
color_scheme = {
|
||||
"DEBUG": "cyan",
|
||||
"INFO": "green",
|
||||
"WARNING": "yellow",
|
||||
"ERROR": "red",
|
||||
"CRITICAL": "red,bg_white",
|
||||
"DEFAULT": "white",
|
||||
}
|
||||
|
||||
# Define secondary log colors
|
||||
secondary_colors = {
|
||||
"log_name": {"DEBUG": "blue"},
|
||||
"asctime": {"DEBUG": "cyan"},
|
||||
"process": {"DEBUG": "purple"},
|
||||
"module": {"DEBUG": "cyan,bg_blue"},
|
||||
"funcName": {"DEBUG": "light_white,bg_blue"},
|
||||
}
|
||||
|
||||
# Define the log format string
|
||||
fmt_string = "%(log_color)s%(levelname)s: %(log_color)s[%(module)s.%(funcName)s]%(reset)s %(white)s%(message)s"
|
||||
|
||||
# Define formatting configuration
|
||||
fmt_config = {
|
||||
"log_colors": color_scheme,
|
||||
"secondary_log_colors": secondary_colors,
|
||||
"style": "%",
|
||||
"reset": True,
|
||||
}
|
||||
|
||||
|
||||
class MultilineColoredFormatter(colorlog.ColoredFormatter):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.log_colors = kwargs.pop("log_colors", {})
|
||||
self.secondary_log_colors = kwargs.pop("secondary_log_colors", {})
|
||||
|
||||
def format(self, record):
|
||||
message = record.getMessage()
|
||||
if "\n" in message:
|
||||
lines = message.split("\n")
|
||||
first_line = lines[0]
|
||||
record.message = first_line
|
||||
formatted_first_line = super().format(record)
|
||||
formatted_lines = [formatted_first_line]
|
||||
for line in lines[1:]:
|
||||
formatted_lines.append(line)
|
||||
return "\n".join(formatted_lines)
|
||||
else:
|
||||
return super().format(record)
|
||||
|
||||
|
||||
# Create a MultilineColoredFormatter object for colorized logging
|
||||
formatter = MultilineColoredFormatter(fmt_string, **fmt_config)
|
||||
|
||||
# Create a stream handler for logging output
|
||||
stream = logging.StreamHandler()
|
||||
stream.setFormatter(formatter)
|
||||
|
||||
# Set up the root logger with the same formatting
|
||||
root_logger = logging.getLogger()
|
||||
root_logger.setLevel(logging.DEBUG)
|
||||
root_logger.addHandler(stream)
|
||||
|
||||
ignore_logs = ["_trace", "httpx", "_client", "_trace.atrace", "aiohttp", "_client", "base"]
|
||||
for lgr in ignore_logs:
|
||||
loggr = logging.getLogger(lgr)
|
||||
loggr.setLevel(logging.INFO)
|
@@ -1,11 +0,0 @@
|
||||
from dogpile.cache import make_region
|
||||
|
||||
from settings import REDIS_URL
|
||||
|
||||
# Создание региона кэша с TTL
|
||||
cache_region = make_region()
|
||||
cache_region.configure(
|
||||
"dogpile.cache.redis",
|
||||
arguments={"url": f"{REDIS_URL}/1"},
|
||||
expiration_time=3600, # Cache expiration time in seconds
|
||||
)
|
@@ -2,8 +2,8 @@ import json
|
||||
|
||||
from orm.notification import Notification
|
||||
from services.db import local_session
|
||||
from services.logger import root_logger as logger
|
||||
from services.rediscache import redis
|
||||
from utils.logger import root_logger as logger
|
||||
from cache.rediscache import redis
|
||||
|
||||
|
||||
def save_notification(action: str, entity: str, payload):
|
||||
|
@@ -1,150 +0,0 @@
|
||||
import json
|
||||
|
||||
from sqlalchemy import and_, join, select
|
||||
|
||||
from orm.author import Author, AuthorFollower
|
||||
from orm.shout import Shout, ShoutAuthor, ShoutTopic
|
||||
from orm.topic import Topic, TopicFollower
|
||||
from resolvers.stat import get_with_stat
|
||||
from services.cache import cache_author, cache_topic
|
||||
from services.db import local_session
|
||||
from services.encoders import CustomJSONEncoder
|
||||
from services.logger import root_logger as logger
|
||||
from services.rediscache import redis
|
||||
|
||||
|
||||
async def precache_authors_followers(author_id, session):
|
||||
# Precache author followers
|
||||
authors_followers = set()
|
||||
followers_query = select(AuthorFollower.follower).where(AuthorFollower.author == author_id)
|
||||
result = session.execute(followers_query)
|
||||
|
||||
for row in result:
|
||||
follower_id = row[0]
|
||||
if follower_id:
|
||||
authors_followers.add(follower_id)
|
||||
|
||||
followers_payload = json.dumps(
|
||||
[f for f in authors_followers],
|
||||
cls=CustomJSONEncoder,
|
||||
)
|
||||
await redis.execute("SET", f"author:followers:{author_id}", followers_payload)
|
||||
|
||||
|
||||
async def precache_authors_follows(author_id, session):
|
||||
# Precache topics followed by author
|
||||
follows_topics = set()
|
||||
follows_topics_query = select(TopicFollower.topic).where(TopicFollower.follower == author_id)
|
||||
result = session.execute(follows_topics_query)
|
||||
|
||||
for row in result:
|
||||
followed_topic_id = row[0]
|
||||
if followed_topic_id:
|
||||
follows_topics.add(followed_topic_id)
|
||||
|
||||
topics_payload = json.dumps([t for t in follows_topics], cls=CustomJSONEncoder)
|
||||
await redis.execute("SET", f"author:follows-topics:{author_id}", topics_payload)
|
||||
|
||||
# Precache authors followed by author
|
||||
follows_authors = set()
|
||||
follows_authors_query = select(AuthorFollower.author).where(AuthorFollower.follower == author_id)
|
||||
result = session.execute(follows_authors_query)
|
||||
|
||||
for row in result:
|
||||
followed_author_id = row[0]
|
||||
if followed_author_id:
|
||||
follows_authors.add(followed_author_id)
|
||||
|
||||
authors_payload = json.dumps([a for a in follows_authors], cls=CustomJSONEncoder)
|
||||
await redis.execute("SET", f"author:follows-authors:{author_id}", authors_payload)
|
||||
|
||||
|
||||
async def precache_topics_authors(topic_id: int, session):
|
||||
# Precache topic authors
|
||||
topic_authors = set()
|
||||
topic_authors_query = (
|
||||
select(ShoutAuthor.author)
|
||||
.select_from(join(ShoutTopic, Shout, ShoutTopic.shout == Shout.id))
|
||||
.join(ShoutAuthor, ShoutAuthor.shout == Shout.id)
|
||||
.filter(
|
||||
and_(
|
||||
ShoutTopic.topic == topic_id,
|
||||
Shout.published_at.is_not(None),
|
||||
Shout.deleted_at.is_(None),
|
||||
)
|
||||
)
|
||||
)
|
||||
result = session.execute(topic_authors_query)
|
||||
|
||||
for row in result:
|
||||
author_id = row[0]
|
||||
if author_id:
|
||||
topic_authors.add(author_id)
|
||||
|
||||
authors_payload = json.dumps([a for a in topic_authors], cls=CustomJSONEncoder)
|
||||
await redis.execute("SET", f"topic:authors:{topic_id}", authors_payload)
|
||||
|
||||
|
||||
async def precache_topics_followers(topic_id: int, session):
|
||||
# Precache topic followers
|
||||
topic_followers = set()
|
||||
followers_query = select(TopicFollower.follower).where(TopicFollower.topic == topic_id)
|
||||
result = session.execute(followers_query)
|
||||
|
||||
for row in result:
|
||||
follower_id = row[0]
|
||||
if follower_id:
|
||||
topic_followers.add(follower_id)
|
||||
|
||||
followers_payload = json.dumps([f for f in topic_followers], cls=CustomJSONEncoder)
|
||||
await redis.execute("SET", f"topic:followers:{topic_id}", followers_payload)
|
||||
|
||||
|
||||
async def precache_data():
|
||||
try:
|
||||
# cache reset
|
||||
await redis.execute("FLUSHDB")
|
||||
logger.info("redis flushed")
|
||||
|
||||
# topics
|
||||
topics_by_id = {}
|
||||
topics = get_with_stat(select(Topic))
|
||||
for topic in topics:
|
||||
topic_profile = topic.dict() if not isinstance(topic, dict) else topic
|
||||
await cache_topic(topic_profile)
|
||||
logger.info(f"{len(topics)} topics precached")
|
||||
|
||||
# followings for topics
|
||||
with local_session() as session:
|
||||
for topic_id in topics_by_id.keys():
|
||||
await precache_topics_followers(topic_id, session)
|
||||
await precache_topics_authors(topic_id, session)
|
||||
logger.info("topics followings precached")
|
||||
|
||||
# authors
|
||||
authors_by_id = {}
|
||||
authors = get_with_stat(select(Author).where(Author.user.is_not(None)))
|
||||
logger.debug(f"{len(authors)} authors found in database")
|
||||
c = 0
|
||||
for author in authors:
|
||||
if isinstance(author, Author):
|
||||
profile = author.dict()
|
||||
author_id = profile.get("id")
|
||||
user_id = profile.get("user", "").strip()
|
||||
if author_id and user_id:
|
||||
authors_by_id[author_id] = profile
|
||||
await cache_author(profile)
|
||||
c += 1
|
||||
else:
|
||||
logger.error(f"fail caching {author}")
|
||||
|
||||
logger.info(f"{c} authors precached")
|
||||
|
||||
# followings for authors
|
||||
with local_session() as session:
|
||||
for author_id in authors_by_id.keys():
|
||||
await precache_authors_followers(author_id, session)
|
||||
await precache_authors_follows(author_id, session)
|
||||
logger.info("authors followings precached")
|
||||
except Exception as exc:
|
||||
logger.error(exc)
|
@@ -1,63 +0,0 @@
|
||||
import logging
|
||||
|
||||
import redis.asyncio as aredis
|
||||
|
||||
from settings import REDIS_URL
|
||||
|
||||
# Set redis logging level to suppress DEBUG messages
|
||||
logger = logging.getLogger("redis")
|
||||
logger.setLevel(logging.WARNING)
|
||||
|
||||
|
||||
class RedisCache:
|
||||
def __init__(self, uri=REDIS_URL):
|
||||
self._uri: str = uri
|
||||
self.pubsub_channels = []
|
||||
self._client = None
|
||||
|
||||
async def connect(self):
|
||||
self._client = aredis.Redis.from_url(self._uri, decode_responses=True)
|
||||
|
||||
async def disconnect(self):
|
||||
if self._client:
|
||||
await self._client.close()
|
||||
|
||||
async def execute(self, command, *args, **kwargs):
|
||||
if self._client:
|
||||
try:
|
||||
logger.debug(f"{command}") # {args[0]}") # {args} {kwargs}")
|
||||
for arg in args:
|
||||
if isinstance(arg, dict):
|
||||
if arg.get("_sa_instance_state"):
|
||||
del arg["_sa_instance_state"]
|
||||
r = await self._client.execute_command(command, *args, **kwargs)
|
||||
# logger.debug(type(r))
|
||||
# logger.debug(r)
|
||||
return r
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
|
||||
async def subscribe(self, *channels):
|
||||
if self._client:
|
||||
async with self._client.pubsub() as pubsub:
|
||||
for channel in channels:
|
||||
await pubsub.subscribe(channel)
|
||||
self.pubsub_channels.append(channel)
|
||||
|
||||
async def unsubscribe(self, *channels):
|
||||
if not self._client:
|
||||
return
|
||||
async with self._client.pubsub() as pubsub:
|
||||
for channel in channels:
|
||||
await pubsub.unsubscribe(channel)
|
||||
self.pubsub_channels.remove(channel)
|
||||
|
||||
async def publish(self, channel, data):
|
||||
if not self._client:
|
||||
return
|
||||
await self._client.publish(channel, data)
|
||||
|
||||
|
||||
redis = RedisCache()
|
||||
|
||||
__all__ = ["redis"]
|
@@ -1,48 +0,0 @@
|
||||
import asyncio
|
||||
from services.logger import root_logger as logger
|
||||
from services.cache import get_cached_author, cache_author, cache_topic, get_cached_topic
|
||||
|
||||
|
||||
class CacheRevalidationManager:
|
||||
"""Управление периодической ревалидацией кэша."""
|
||||
|
||||
def __init__(self):
|
||||
self.items_to_revalidate = {"authors": set(), "topics": set()}
|
||||
self.revalidation_interval = 60 # Интервал ревалидации в секундах
|
||||
self.loop = None
|
||||
|
||||
def start(self):
|
||||
self.loop = asyncio.get_event_loop()
|
||||
self.loop.run_until_complete(self.revalidate_cache())
|
||||
self.loop.run_forever()
|
||||
logger.info("[services.revalidator] started infinite loop")
|
||||
|
||||
async def revalidate_cache(self):
|
||||
"""Периодическая ревалидация кэша."""
|
||||
while True:
|
||||
await asyncio.sleep(self.revalidation_interval)
|
||||
await self.process_revalidation()
|
||||
|
||||
async def process_revalidation(self):
|
||||
"""Ревалидация кэша для отмеченных сущностей."""
|
||||
for entity_type, ids in self.items_to_revalidate.items():
|
||||
for entity_id in ids:
|
||||
if entity_type == "authors":
|
||||
# Ревалидация кэша автора
|
||||
author = await get_cached_author(entity_id)
|
||||
if author:
|
||||
await cache_author(author)
|
||||
elif entity_type == "topics":
|
||||
# Ревалидация кэша темы
|
||||
topic = await get_cached_topic(entity_id)
|
||||
if topic:
|
||||
await cache_topic(topic)
|
||||
ids.clear()
|
||||
|
||||
def mark_for_revalidation(self, entity_id, entity_type):
|
||||
"""Отметить сущность для ревалидации."""
|
||||
self.items_to_revalidate[entity_type].add(entity_id)
|
||||
|
||||
|
||||
# Инициализация менеджера ревалидации
|
||||
revalidation_manager = CacheRevalidationManager()
|
@@ -5,8 +5,8 @@ import os
|
||||
|
||||
from opensearchpy import OpenSearch
|
||||
|
||||
from services.encoders import CustomJSONEncoder
|
||||
from services.rediscache import redis
|
||||
from utils.encoders import CustomJSONEncoder
|
||||
from cache.rediscache import redis
|
||||
|
||||
# Set redis logging level to suppress DEBUG messages
|
||||
logger = logging.getLogger("search")
|
||||
|
@@ -1,85 +0,0 @@
|
||||
from sqlalchemy import event
|
||||
from orm.author import Author, AuthorFollower
|
||||
from orm.reaction import Reaction
|
||||
from orm.shout import Shout, ShoutAuthor
|
||||
from orm.topic import Topic, TopicFollower
|
||||
from services.revalidator import revalidation_manager
|
||||
from services.logger import root_logger as logger
|
||||
|
||||
|
||||
def after_update_handler(mapper, connection, target):
|
||||
"""Обработчик обновления сущности."""
|
||||
entity_type = "authors" if isinstance(target, Author) else "topics" if isinstance(target, Topic) else "shouts"
|
||||
revalidation_manager.mark_for_revalidation(target.id, entity_type)
|
||||
|
||||
|
||||
def after_follower_insert_update_handler(mapper, connection, target):
|
||||
"""Обработчик добавления или обновления подписки."""
|
||||
if isinstance(target, AuthorFollower):
|
||||
# Пометить автора и подписчика для ревалидации
|
||||
revalidation_manager.mark_for_revalidation(target.author_id, "authors")
|
||||
revalidation_manager.mark_for_revalidation(target.follower_id, "authors")
|
||||
elif isinstance(target, TopicFollower):
|
||||
# Пометить тему и подписчика для ревалидации
|
||||
revalidation_manager.mark_for_revalidation(target.topic_id, "topics")
|
||||
revalidation_manager.mark_for_revalidation(target.follower_id, "authors")
|
||||
|
||||
|
||||
def after_follower_delete_handler(mapper, connection, target):
|
||||
"""Обработчик удаления подписки."""
|
||||
if isinstance(target, AuthorFollower):
|
||||
# Пометить автора и подписчика для ревалидации
|
||||
revalidation_manager.mark_for_revalidation(target.author_id, "authors")
|
||||
revalidation_manager.mark_for_revalidation(target.follower_id, "authors")
|
||||
elif isinstance(target, TopicFollower):
|
||||
# Пометить тему и подписчика для ревалидации
|
||||
revalidation_manager.mark_for_revalidation(target.topic_id, "topics")
|
||||
revalidation_manager.mark_for_revalidation(target.follower_id, "authors")
|
||||
|
||||
|
||||
def after_reaction_update_handler(mapper, connection, reaction):
|
||||
"""Обработчик изменений реакций."""
|
||||
# Пометить shout для ревалидации
|
||||
revalidation_manager.mark_for_revalidation(reaction.shout_id, "shouts")
|
||||
# Пометить автора реакции для ревалидации
|
||||
revalidation_manager.mark_for_revalidation(reaction.created_by, "authors")
|
||||
|
||||
|
||||
def after_shout_author_insert_update_handler(mapper, connection, target):
|
||||
"""Обработчик добавления или обновления авторства публикации."""
|
||||
# Пометить shout и автора для ревалидации
|
||||
revalidation_manager.mark_for_revalidation(target.shout_id, "shouts")
|
||||
revalidation_manager.mark_for_revalidation(target.author_id, "authors")
|
||||
|
||||
|
||||
def after_shout_author_delete_handler(mapper, connection, target):
|
||||
"""Обработчик удаления авторства публикации."""
|
||||
# Пометить shout и автора для ревалидации
|
||||
revalidation_manager.mark_for_revalidation(target.shout_id, "shouts")
|
||||
revalidation_manager.mark_for_revalidation(target.author_id, "authors")
|
||||
|
||||
|
||||
def events_register():
|
||||
"""Регистрация обработчиков событий для всех сущностей."""
|
||||
event.listen(ShoutAuthor, "after_insert", after_shout_author_insert_update_handler)
|
||||
event.listen(ShoutAuthor, "after_update", after_shout_author_insert_update_handler)
|
||||
event.listen(ShoutAuthor, "after_delete", after_shout_author_delete_handler)
|
||||
|
||||
event.listen(AuthorFollower, "after_insert", after_follower_insert_update_handler)
|
||||
event.listen(AuthorFollower, "after_update", after_follower_insert_update_handler)
|
||||
event.listen(AuthorFollower, "after_delete", after_follower_delete_handler)
|
||||
event.listen(TopicFollower, "after_insert", after_follower_insert_update_handler)
|
||||
event.listen(TopicFollower, "after_update", after_follower_insert_update_handler)
|
||||
event.listen(TopicFollower, "after_delete", after_follower_delete_handler)
|
||||
event.listen(Reaction, "after_update", after_reaction_update_handler)
|
||||
|
||||
event.listen(Author, "after_update", after_update_handler)
|
||||
event.listen(Topic, "after_update", after_update_handler)
|
||||
event.listen(Shout, "after_update", after_update_handler)
|
||||
event.listen(
|
||||
Reaction,
|
||||
"after_update",
|
||||
lambda mapper, connection, target: revalidation_manager.mark_for_revalidation(target.shout, "shouts"),
|
||||
)
|
||||
|
||||
logger.info("Event handlers registered successfully.")
|
@@ -1,24 +0,0 @@
|
||||
import json
|
||||
|
||||
from services.rediscache import redis
|
||||
|
||||
|
||||
async def get_unread_counter(chat_id: str, author_id: int) -> int:
|
||||
r = await redis.execute("LLEN", f"chats/{chat_id}/unread/{author_id}")
|
||||
if isinstance(r, str):
|
||||
return int(r)
|
||||
elif isinstance(r, int):
|
||||
return r
|
||||
else:
|
||||
return 0
|
||||
|
||||
|
||||
async def get_total_unread_counter(author_id: int) -> int:
|
||||
chats_set = await redis.execute("SMEMBERS", f"chats_by_author/{author_id}")
|
||||
s = 0
|
||||
if isinstance(chats_set, str):
|
||||
chats_set = json.loads(chats_set)
|
||||
if isinstance(chats_set, list):
|
||||
for chat_id in chats_set:
|
||||
s += await get_unread_counter(chat_id, author_id)
|
||||
return s
|
@@ -10,7 +10,7 @@ from starlette.responses import JSONResponse
|
||||
|
||||
from orm.author import Author
|
||||
from resolvers.stat import get_with_stat
|
||||
from services.cache import cache_author
|
||||
from cache.cache import cache_author
|
||||
from services.db import local_session
|
||||
|
||||
|
||||
|
Reference in New Issue
Block a user