Commit 8ad4021d authored by Vitaly Lipatov's avatar Vitaly Lipatov

gateway: serialize concurrent autoban updates

parent fbb077d7
...@@ -14,6 +14,7 @@ AutoBanManager - управление автоматической разбло ...@@ -14,6 +14,7 @@ AutoBanManager - управление автоматической разбло
import time import time
import logging import logging
from redis.exceptions import WatchError
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -73,63 +74,70 @@ class AutoBanManager: ...@@ -73,63 +74,70 @@ class AutoBanManager:
if not self.enabled: if not self.enabled:
return None return None
try: meta_key = f"{self.META_PREFIX}{ip}"
meta_key = f"{self.META_PREFIX}{ip}" for _ in range(3):
now = int(time.time()) pipeline = self.r.pipeline()
try:
# Получаем существующие метаданные pipeline.watch(meta_key, self.PERMANENT_KEY)
existing = self.r.hgetall(meta_key) now = int(time.time())
is_permanent = self.r.sismember(self.PERMANENT_KEY, ip) existing = pipeline.hgetall(meta_key)
is_permanent = pipeline.sismember(self.PERMANENT_KEY, ip)
if existing:
offense_count = int(existing.get(b'offense_count', 0)) if existing:
last_offense = int(existing.get(b'last_offense', 0)) offense_count = int(existing.get(b'offense_count', 0))
last_offense = int(existing.get(b'last_offense', 0))
# Проверяем, прошёл ли период сброса (1 год)
if now - last_offense > self.reset_period_seconds: # Проверяем, прошёл ли период сброса (1 год)
offense_count = 0 if now - last_offense > self.reset_period_seconds:
offense_count = 0
offense_count += 1
else: offense_count += 1
# Permanent state is authoritative even if old metadata was else:
# removed by a previous release's TTL or an admin reset. # Permanent state is authoritative even if old metadata was
offense_count = self.max_offense_level + 1 if is_permanent else 1 # removed by a previous release's TTL or an admin reset.
offense_count = self.max_offense_level + 1 if is_permanent else 1
# Рассчитываем время бана
ban_duration = self.calculate_ban_duration(offense_count) # Рассчитываем время бана
unban_time = now + ban_duration if ban_duration > 0 else 0 ban_duration = self.calculate_ban_duration(offense_count)
unban_time = now + ban_duration if ban_duration > 0 else 0
# Сохраняем метаданные
metadata = { # Сохраняем метаданные
'offense_count': offense_count, metadata = {
'ban_time': now, 'offense_count': offense_count,
'unban_time': unban_time, 'ban_time': now,
'last_offense': now, 'unban_time': unban_time,
'source': source, 'last_offense': now,
'reason': reason 'source': source,
} 'reason': reason
pipeline = self.r.pipeline(transaction=True) }
pipeline.hset(meta_key, mapping=metadata) pipeline.multi()
pipeline.hset(meta_key, mapping=metadata)
# Добавляем в расписание авто-разбана или в постоянные
if unban_time > 0: # Добавляем в расписание авто-разбана или в постоянные
# Temporary ban history can be reset after a clean period. if unban_time > 0:
pipeline.expire(meta_key, self.reset_period_seconds) # Temporary ban history can be reset after a clean period.
pipeline.zadd(self.SCHEDULE_KEY, {ip: unban_time}) pipeline.expire(meta_key, self.reset_period_seconds)
pipeline.srem(self.PERMANENT_KEY, ip) # На случай если был permanent pipeline.zadd(self.SCHEDULE_KEY, {ip: unban_time})
else: pipeline.srem(self.PERMANENT_KEY, ip) # На случай если был permanent
# Permanent state must not disappear when temporary metadata else:
# would otherwise reach its one-year TTL. # Permanent state must not disappear when temporary metadata
pipeline.persist(meta_key) # would otherwise reach its one-year TTL.
pipeline.sadd(self.PERMANENT_KEY, ip) pipeline.persist(meta_key)
pipeline.zrem(self.SCHEDULE_KEY, ip) # Убираем из расписания pipeline.sadd(self.PERMANENT_KEY, ip)
pipeline.execute() pipeline.zrem(self.SCHEDULE_KEY, ip) # Убираем из расписания
pipeline.execute()
return metadata
return metadata
except Exception as e:
log.error(f"AutoBanManager.on_ban error: {e}") except WatchError:
return None continue
except Exception as e:
log.error(f"AutoBanManager.on_ban error: {e}")
return None
finally:
pipeline.reset()
log.error("AutoBanManager.on_ban conflict retries exhausted")
return None
def on_unban(self, ip, reset_counter=False): def on_unban(self, ip, reset_counter=False):
""" """
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment