Skip to content

Commit e807742

Browse files
committed
Merge branch 'development'
2 parents db480e7 + 0904265 commit e807742

7 files changed

Lines changed: 76 additions & 30 deletions

File tree

core/data/impl/nodeimpl.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
from packaging.version import parse
3939
from pathlib import Path
4040
from psycopg import sql
41-
from psycopg.errors import InFailedSqlTransaction, ConnectionTimeout, UniqueViolation, UndefinedTable, UndefinedColumn
41+
from psycopg.errors import ConnectionTimeout, UniqueViolation, UndefinedTable, UndefinedColumn
4242
from psycopg.types.json import Json
4343
from psycopg_pool import ConnectionPool, AsyncConnectionPool
4444
from typing import Awaitable, Callable, Any
@@ -651,6 +651,10 @@ async def upgrade(self):
651651
async with self.cpool.connection() as conn:
652652
await self._upgrade(self.master, conn)
653653

654+
@override
655+
async def is_alive(self, timeout: int = 30) -> bool:
656+
return True
657+
654658
@override
655659
async def get_dcs_branch_and_version(self) -> tuple[str, str]:
656660
if not self.dcs_branch or not self.dcs_version:
@@ -1097,8 +1101,8 @@ async def get_master() -> tuple[str | None, str | None, str, bool]:
10971101
except psycopg.errors.UndefinedColumn:
10981102
# add the column if missing
10991103
await conn.rollback()
1100-
await conn.execute("ALTER TABLE cluster ADD COLUMN IF NOT EXISTS takeover_requested_by TEXT NULL")
1101-
await conn.commit()
1104+
async with conn.transaction():
1105+
await conn.execute("ALTER TABLE cluster ADD COLUMN IF NOT EXISTS takeover_requested_by TEXT NULL")
11021106
return await get_master()
11031107

11041108
async def is_node_alive(node: str, timeout: int) -> bool:
@@ -1283,8 +1287,9 @@ async def check_nodes():
12831287
ON CONFLICT (guild_id, node) DO UPDATE
12841288
SET last_seen = (NOW() AT TIME ZONE 'UTC')
12851289
""", (self.guild_id, self.name))
1286-
except InFailedSqlTransaction:
1287-
# we should only be here when the CLUSTER table does not exist yet
1290+
except UndefinedTable:
1291+
# we should only be here when the CLUSTER table does not exist
1292+
# it will be created directly afterward
12881293
return True
12891294

12901295
@tasks.loop(seconds=5.0)

core/data/node.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,3 +280,7 @@ async def info(self) -> dict:
280280
@abstractmethod
281281
async def get_config(self) -> dict:
282282
raise NotImplementedError()
283+
284+
@abstractmethod
285+
async def is_alive(self, timeout: int = 30) -> bool:
286+
raise NotImplementedError()

core/data/proxy/nodeproxy.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from core.services.registry import ServiceRegistry
88
from core.utils import async_cache, cache_with_expiration
99
from pathlib import Path
10+
from psycopg import sql
1011
from typing import TYPE_CHECKING
1112
from typing_extensions import override
1213

@@ -29,6 +30,7 @@ def __init__(self, local_node: NodeImpl, name: str, public_ip: str, dcs_version:
2930
self.local_node = local_node
3031
self.pool = self.local_node.pool
3132
self.apool = self.local_node.apool
33+
self.cpool = self.local_node.cpool
3234
self.log = self.local_node.log
3335
self._public_ip = public_ip
3436
self.locals = self.read_locals()
@@ -467,3 +469,14 @@ async def get_config(self) -> dict:
467469
"object": "Node",
468470
"method": "get_config"
469471
}, timeout=timeout, node=self.name)
472+
473+
@override
474+
async def is_alive(self, timeout: int = 30) -> bool:
475+
async with self.cpool.connection() as conn:
476+
query = sql.SQL("""
477+
SELECT COUNT(*) FROM nodes
478+
WHERE guild_id = %s AND node = %s
479+
AND last_seen > (NOW() AT TIME ZONE 'UTC' - interval {interval})
480+
""").format(interval=sql.Literal(f"{timeout} seconds"))
481+
cursor = await conn.execute(query, (self.guild_id, self.name))
482+
return (await cursor.fetchone())[0] == 1

requirements.in

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ discord.py==2.7.1
2727
eyeD3==0.9.9
2828

2929
# FastAPI: A modern, fast (high-performance), web framework for building APIs with Python 3.6+ based on standard Python type hints
30-
fastapi==0.135.3
30+
fastapi==0.136.0
3131

3232
# FuzzyWuzzy: Python library that uses Levenshtein Distance to calculate the differences between strings
3333
fuzzywuzzy==0.18.0
@@ -45,7 +45,7 @@ jinja2==3.1.6
4545
jsonschema==4.26.0
4646

4747
# Fast LUA parser
48-
lupa==2.7
48+
lupa==2.8
4949

5050
# Matplotlib: Python 2D plotting library
5151
matplotlib==3.10.8

requirements.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ discord-py==2.7.1
3131
docopt==0.6.2
3232
et-xmlfile==2.0.0
3333
eyed3==0.9.9
34-
fastapi==0.135.3
34+
fastapi==0.136.0
3535
filetype==1.2.0
3636
fonttools==4.62.1
3737
frozenlist==1.8.0
@@ -47,7 +47,7 @@ jsonschema==4.26.0
4747
jsonschema-specifications==2025.9.1
4848
kiwisolver==1.5.0
4949
levenshtein==0.27.3
50-
lupa==2.7
50+
lupa==2.8
5151
markdown-it-py==4.0.0
5252
markupsafe==3.0.3
5353
matplotlib==3.10.8

services/bot/service.py

Lines changed: 21 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -190,25 +190,28 @@ async def stop(self):
190190
await super().stop()
191191

192192
async def alert(self, title: str, message: str, server: Server | None = None) -> None:
193-
# if we have dedicated managers of a server, send the alerts to them
194-
if server and server.locals.get('managed_by'):
195-
alert_roles = server.locals['managed_by']
196-
# use the default Alert role otherwise
197-
else:
198-
alert_roles = self.bot.roles['Alert']
199193
try:
200-
mentions = ''.join([self.bot.get_role(role).mention for role in alert_roles if role is not None])
201-
except AttributeError:
202-
self.log.error(f"Alert-Role {alert_roles} not found.")
203-
mentions = ""
204-
embed = utils.create_warning_embed(title=title, text=utils.escape_string(message))
205-
admin_channel = self.bot.get_admin_channel(server)
206-
audit_channel = self.bot.get_channel(self.bot.locals.get('channels', {}).get('audit', -1))
207-
channel = admin_channel or audit_channel
208-
if channel:
209-
await channel.send(content=mentions, embed=embed)
210-
else:
211-
self.log.critical(f"{title}: {message}")
194+
# if we have dedicated managers of a server, send the alerts to them
195+
if server and server.locals.get('managed_by'):
196+
alert_roles = server.locals['managed_by']
197+
# use the default Alert role otherwise
198+
else:
199+
alert_roles = self.bot.roles['Alert']
200+
try:
201+
mentions = ''.join([self.bot.get_role(role).mention for role in alert_roles if role is not None])
202+
except AttributeError:
203+
self.log.error(f"Alert-Role {alert_roles} not found.")
204+
mentions = ""
205+
embed = utils.create_warning_embed(title=title, text=utils.escape_string(message))
206+
admin_channel = self.bot.get_admin_channel(server)
207+
audit_channel = self.bot.get_channel(self.bot.locals.get('channels', {}).get('audit', -1))
208+
channel = admin_channel or audit_channel
209+
if channel:
210+
await channel.send(content=mentions, embed=embed)
211+
else:
212+
self.log.critical(f"{title}: {message}")
213+
except Exception:
214+
self.log.warning("Audit message discarded due to master takeover: " + message)
212215

213216
async def install_fonts(self):
214217
font_dir = Path('fonts')

services/servicebus/service.py

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,19 @@ async def switch(self, master: bool):
117117
}
118118
}, node=node)
119119
else:
120+
# figure the master node
121+
async with self.node.cpool.connection() as conn:
122+
cursor = await conn.execute("SELECT master FROM cluster WHERE guild_id = %s", (self.node.guild_id, ))
123+
row = await cursor.fetchone()
124+
if not row:
125+
self.log.warning("No master available, can't register.")
126+
return
127+
master = row[0]
128+
129+
if master not in await self.node.get_active_nodes():
130+
self.log.debug(f"Master node {master} is not active (yet), waiting ...")
131+
return
132+
120133
await self.send_to_node({
121134
"command": "rpc",
122135
"service": self.__class__.__name__,
@@ -274,8 +287,16 @@ async def register_remote_node(self, name: str, public_ip: str, dcs_version: str
274287
if name == self.node.name:
275288
return
276289

277-
self.log.info(f"- Registering remote node {name} ...")
290+
if self.node.all_nodes.get(name):
291+
self.log.debug(f"Node {name} already registered, skipping registration request.")
292+
return
293+
278294
node = NodeProxy(self.node, name, public_ip, dcs_version)
295+
if not await node.is_alive(self.node.config.get('cluster', {}).get('heartbeat', 30)):
296+
self.log.warning(f"Node {name} is not alive (anymore). Skipping registration request.")
297+
return
298+
299+
self.log.info(f"- Registering remote node {name} ...")
279300
# we did not find a configuration for this node, load it from remote
280301
if not node.locals:
281302
node.locals = await node.get_config()
@@ -466,12 +487,12 @@ async def is_banned(self, ucid: str) -> dict | None:
466487
return await cursor.fetchone()
467488

468489
async def init_remote_server(self, server_name: str, status: str, instance: str, home: str,
469-
settings: dict, options: dict, node: Node, channels: dict, dcs_port: int,
490+
settings: dict, options: dict, node: Node | str, channels: dict, dcs_port: int,
470491
webgui_port: int, maintenance: bool) -> None:
471492
from core import InstanceProxy
472493

473494
# init event for an unregistered remote node received or a race condition due to master switches, ignoring
474-
if not node or node == self.node:
495+
if not node or node == self.node or isinstance(node, str):
475496
return
476497
try:
477498
server: ServerProxy = cast(ServerProxy, self.servers.get(server_name))

0 commit comments

Comments
 (0)