Skip to content

Commit c95b58e

Browse files
committed
CHANGES
- Massive DB change, all major tables have foreign keys now. Prune / cleanup scripts reduced.
1 parent 37bc75a commit c95b58e

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

82 files changed

+806
-377
lines changed

core/data/impl/instanceimpl.py

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,7 @@ def __post_init__(self):
5959
# dirty |= True
6060
if dirty:
6161
autoexec.net = net
62-
63-
server_name = None
62+
self._server_name = None
6463
settings_path = os.path.join(self.home, 'Config', 'serverSettings.lua')
6564
if os.path.exists(settings_path):
6665
settings = SettingsDict(self, settings_path, root='cfg')
@@ -69,21 +68,28 @@ def __post_init__(self):
6968
settings['port'] = dcs_port
7069
else:
7170
self.locals['dcs_port'] = settings.get('port', 10308)
72-
server_name = settings.get('name', 'DCS Server') if settings else None
73-
if server_name == 'n/a':
74-
server_name = None
75-
self.update_instance(server_name)
71+
self._server_name = settings.get('name', 'DCS Server') if settings else None
72+
if self._server_name == 'n/a':
73+
self._server_name = None
74+
self.update_instance()
75+
76+
@property
77+
def server_name(self) -> str:
78+
if self.server:
79+
return self.server.name
80+
else:
81+
return self._server_name
7682

77-
def update_instance(self, server_name: str | None = None):
83+
def update_instance(self):
7884
try:
7985
with self.pool.connection() as conn:
8086
with conn.transaction():
8187
conn.execute("""
82-
INSERT INTO instances (node, instance, port, server_name)
83-
VALUES (%s, %s, %s, %s)
88+
INSERT INTO instances (node, instance, port)
89+
VALUES (%s, %s, %s)
8490
ON CONFLICT (node, instance) DO UPDATE
85-
SET port=excluded.port, server_name=excluded.server_name
86-
""", (self.node.name, self.name, self.locals.get('bot_port', 6666), server_name))
91+
SET port=excluded.port
92+
""", (self.node.name, self.name, self.locals.get('bot_port', 6666)))
8793
except psycopg.errors.UniqueViolation:
8894
self.log.error(f"bot_port {self.locals.get('bot_port', 6666)} is already in use on node {self.node.name}!")
8995
raise

core/data/impl/nodeimpl.py

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -82,8 +82,8 @@
8282
"userstats",
8383
"missionstats",
8484
"monitoring",
85-
"creditsystem",
8685
"gamemaster",
86+
"creditsystem",
8787
"cloud"
8888
]
8989

@@ -1066,7 +1066,8 @@ async def check_nodes():
10661066
return True
10671067
# The master is not alive, take over
10681068
elif not master or not await is_node_alive(master, config.get('heartbeat', 30)):
1069-
self.log.warning("The master node is not responding, taking over ...")
1069+
if master is not None:
1070+
self.log.warning(f"The master node {master} is not responding, taking over ...")
10701071
await take_over()
10711072
return True
10721073
# Master is alive, but we are the preferred one
@@ -1211,17 +1212,12 @@ async def rename_file(self, old_name: str, new_name: str, *, force: bool | None
12111212

12121213
@override
12131214
async def rename_server(self, server: Server, new_name: str):
1214-
from services.bot import BotService
12151215
from services.servicebus import ServiceBus
12161216

12171217
if not self.master:
12181218
self.log.error(
12191219
f"Rename request received for server {server.name} that should have gone to the master node!")
12201220
return
1221-
# do not rename initially created servers (they should not be there anyway)
1222-
if server.name != 'n/a':
1223-
# we are doing the plugin changes, as we are the master
1224-
await ServiceRegistry.get(BotService).rename_server(server, new_name)
12251221
# update the ServiceBus
12261222
ServiceRegistry.get(ServiceBus).rename_server(server, new_name)
12271223
# change the proxy name for remote servers (ServerImpl will rename local ones)
@@ -1566,8 +1562,12 @@ def change_instance_in_path(data):
15661562
# rename the directory
15671563
os.rename(instance.home, new_home)
15681564
# rename the instance
1565+
old_name = instance.name
15691566
instance.name = new_name
15701567
instance.locals['home'] = new_home
1568+
self.instances[new_name] = self.instances.pop(old_name)
1569+
1570+
# write the new nodes.yaml
15711571
with open(config_file, mode='w', encoding='utf-8') as outfile:
15721572
yaml.dump(config, outfile)
15731573

@@ -1590,7 +1590,8 @@ def change_instance_in_path(data):
15901590
self.log.exception(f"Failed to start service {list(ServiceRegistry.services().keys())[i]}")
15911591
finally:
15921592
# re-init the attached server instance
1593-
await instance.server.reload()
1593+
if instance.server:
1594+
await instance.server.reload()
15941595

15951596
@override
15961597
async def find_all_instances(self) -> list[tuple[str, str]]:

core/data/impl/serverimpl.py

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -448,22 +448,8 @@ async def update_database(old_name: str, new_name: str):
448448
async with self.apool.connection() as conn:
449449
async with conn.transaction():
450450
# we need to remove any older server that might have had the same name
451-
await conn.execute('DELETE FROM servers WHERE server_name = %s', (new_name, ))
452-
await conn.execute("""
453-
UPDATE servers SET server_name = %s WHERE server_name IS NOT DISTINCT FROM %s
454-
""", (new_name, old_name))
455-
await conn.execute('DELETE FROM instances WHERE server_name = %s', (new_name, ))
456-
await conn.execute("""
457-
UPDATE instances
458-
SET server_name = %s
459-
WHERE instance = %s AND server_name IS NOT DISTINCT FROM %s
460-
""", (new_name, self.instance.name, old_name))
461-
await conn.execute('DELETE FROM message_persistence WHERE server_name = %s', (new_name, ))
462-
await conn.execute("""
463-
UPDATE message_persistence
464-
SET server_name = %s
465-
WHERE server_name IS NOT DISTINCT FROM %s
466-
""", (new_name, old_name))
451+
await conn.execute("UPDATE servers SET server_name = %s WHERE server_name = %s",
452+
(new_name, old_name))
467453

468454
async def update_cluster(new_name: str):
469455
# only the master can take care of a cluster-wide rename

core/data/instance.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,3 +86,7 @@ def server(self, server: Server | None):
8686

8787
def set_server(self, server: Server | None):
8888
self._server = server
89+
90+
@property
91+
def server_name(self) -> str:
92+
raise NotImplementedError()

core/plugin.py

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -332,8 +332,7 @@ async def before_dcs_update(self) -> None:
332332
async def after_dcs_update(self) -> None:
333333
pass
334334

335-
async def prune(self, conn: psycopg.AsyncConnection, *, days: int = -1, ucids: list[str] = None,
336-
server: str | None = None) -> None:
335+
async def prune(self, conn: psycopg.AsyncConnection, days: int) -> None:
337336
pass
338337

339338
async def _init_db(self) -> bool:
@@ -507,12 +506,8 @@ def get_config(self, server: Server | None = None, *, plugin_name: str | None =
507506
self._config[server.node.name][server.instance.name] = utils.deep_merge(default, specific)
508507
return self._config[server.node.name][server.instance.name]
509508

510-
async def rename(self, conn: psycopg.AsyncConnection, old_name: str, new_name: str) -> None:
511-
# this function has to be implemented in your own plugins, if a server rename takes place
512-
pass
513-
514509
async def update_ucid(self, conn: psycopg.AsyncConnection, old_ucid: str, new_ucid: str) -> None:
515-
# this function has to be implemented in your own plugins, if the ucid of a user changed (steam <=> standalone)
510+
# this function has to be implemented in your own plugin if the ucid of a user changed (steam <=> standalone)
516511
pass
517512

518513
async def on_ready(self) -> None:

plugins/README.md

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -128,17 +128,12 @@ class Sample(Plugin[SampleEventListener]):
128128
# do something after a DCS upgrade took place and before the servers are started
129129
...
130130

131-
async def prune(self, conn: psycopg.AsyncConnection, *, days: int = -1, ucids: list[str] = None,
132-
server: Optional[str] = None) -> None:
133-
# cleanup (the database) with data older than days and/or for specific users (ucids)
134-
...
135-
136-
async def rename(self, conn: psycopg.AsyncConnection, old_name: str, new_name: str) -> None:
137-
# this function has to be implemented in your own plugins if a server rename takes place
131+
async def prune(self, conn: psycopg.AsyncConnection, days: int) -> None:
132+
# Rare: cleanup (the database) with data older than days (if you have tables with timestamps)
138133
...
139134

140135
async def update_ucid(self, conn: psycopg.AsyncConnection, old_ucid: str, new_ucid: str) -> None:
141-
# this function has to be implemented in your own plugins if the ucid of a user changed (steam <=> standalone)
136+
# Rare: If you need to do something on an UCID change
142137
...
143138
```
144139
> [!NOTE]
@@ -512,10 +507,27 @@ optional "db" directory below your plugin directory.
512507
tables.sql:
513508
```sql
514509
CREATE TABLE IF NOT EXISTS bans (
515-
ucid TEXT PRIMARY KEY, banned_by TEXT NOT NULL, reason TEXT, banned_at TIMESTAMP NOT NULL DEFAULT NOW()
510+
ucid TEXT PRIMARY KEY,
511+
banned_by TEXT NOT NULL,
512+
reason TEXT,
513+
banned_at TIMESTAMP NOT NULL DEFAULT NOW()
516514
);
517515
```
518516

517+
> [!NOTE]
518+
> Whenever using server names (column name server_name) or UCIDs (column name player_ucid or ucid), use foreign keys
519+
> to the respective master tables.
520+
>
521+
> These are:
522+
> - servers (server_name) <-> yourtable (server_name)
523+
> - players (ucid) <-> yourtable (player_ucid)
524+
> - mission (id) <-> yourtable (mission_id)
525+
> - squadron (id) <-> yourtable (squadron_id)
526+
> - campaign (id) <-> yourtable (campaign_id)
527+
>
528+
> For server name and UCID I recommend using ON CASCADE UPDATE and ON CASCADE DELETE,
529+
> where you can use ON CASCADE DELETE for all others.
530+
519531
To interact with the database, it is recommended to use the asynchronous database pool offered by each common
520532
framework class:
521533
```python
@@ -533,6 +545,9 @@ class MyPlugin(Plugin):
533545
""", (player.ucid, self.plugin_name, reason))
534546
```
535547

548+
> [!NOTE]
549+
> There is also a synchronous pool, if needed: self.pool
550+
536551
## Third-party Python libraries
537552
If your solution needs additional third-party libraries, you can define them in a file named `requirements.local` at
538553
the root level of your DCSServerBot installation.
@@ -584,7 +599,7 @@ class Sample(Plugin[SampleEventListener]):
584599
if new_version == '1.1':
585600
# change the config.yaml file to represent the changes introduced in version 1.1
586601
...
587-
# don't forget to re-read the plugin configuration if you have changed any of it during migration.
602+
# remember to re-read the plugin configuration if you have changed any of it during migration.
588603
self.read_locals()
589604
```
590605
This function handles the tasks necessary for a migration to version `new_version`.

plugins/admin/commands.py

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -714,26 +714,23 @@ async def _prune(self, interaction: discord.Interaction,
714714
else:
715715
await interaction.followup.send("{} is not a valid UCID!".format(user))
716716
return
717-
for plugin in self.bot.cogs.values(): # type: Plugin
718-
await plugin.prune(conn, ucids=[ucid])
719-
await cursor.execute('DELETE FROM players WHERE ucid = %s', (ucid, ))
720-
await cursor.execute('DELETE FROM players_hist WHERE ucid = %s', (ucid, ))
717+
await cursor.execute('DELETE FROM players WHERE ucid = %s', (ucid, ))
721718
if isinstance(user, discord.Member):
722719
await interaction.followup.send(_("Data of user {} deleted.").format(user.display_name))
723720
else:
724721
await interaction.followup.send(_("Data of UCID {} deleted.").format(ucid))
725722
return
726723
elif _server:
727-
for plugin in self.bot.cogs.values(): # type: Plugin
728-
await plugin.prune(conn, server=_server)
729-
await cursor.execute('DELETE FROM servers WHERE server_name = %s', (_server, ))
730-
await cursor.execute('DELETE FROM instances WHERE server_name = %s', (_server, ))
731-
await cursor.execute('DELETE FROM message_persistence WHERE server_name = %s', (_server, ))
724+
await cursor.execute('DELETE FROM servers WHERE server_name = %s', (_server, ))
725+
await cursor.execute('VACCUM FULL statistics')
726+
await cursor.execute('VACUUM FULL missionstats')
732727
await interaction.followup.send(_("Data of server {} deleted.").format(_server))
733728
return
734729
elif view.what in ['users', 'non-members']:
735-
sql = (f"SELECT ucid FROM players "
736-
f"WHERE last_seen < (DATE((now() AT TIME ZONE 'utc')) - interval '{view.age} days')")
730+
sql = f"""
731+
SELECT ucid FROM players
732+
WHERE last_seen < (DATE((now() AT TIME ZONE 'utc')) - interval '{view.age} days')
733+
"""
737734
if view.what == 'non-members':
738735
sql += ' AND discord_id = -1'
739736
await cursor.execute(sql)
@@ -745,20 +742,20 @@ async def _prune(self, interaction: discord.Interaction,
745742
interaction, _("This will delete {} players incl. their stats from the database.\n"
746743
"Are you sure?").format(len(ucids)), ephemeral=ephemeral):
747744
return
748-
for plugin in self.bot.cogs.values(): # type: Plugin
749-
await plugin.prune(conn, ucids=ucids)
750745
for ucid in ucids:
751746
await cursor.execute('DELETE FROM players WHERE ucid = %s', (ucid, ))
752-
await cursor.execute('DELETE FROM players_hist WHERE ucid = %s', (ucid,))
753747
await interaction.followup.send(f"{len(ucids)} players pruned.", ephemeral=ephemeral)
754748
elif view.what == 'data':
755749
days = int(view.age)
756750
if not await utils.yn_question(
757751
interaction, _("This will delete all data older than {} days from the database.\n"
758752
"Are you sure?").format(days), ephemeral=ephemeral):
759753
return
754+
# some plugins need to prune their data based on the provided days
760755
for plugin in self.bot.cogs.values(): # type: Plugin
761-
await plugin.prune(conn, days=days)
756+
await plugin.prune(conn, days)
757+
await cursor.execute('VACCUM FULL statistics')
758+
await cursor.execute('VACUUM FULL missionstats')
762759
await interaction.followup.send(_("All data older than {} days pruned.").format(days),
763760
ephemeral=ephemeral)
764761
await self.bot.audit(f'pruned the database', user=interaction.user)

plugins/admin/db/tables.sql

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,12 @@
1-
CREATE TABLE nodestats (id SERIAL PRIMARY KEY, node TEXT NOT NULL, pool_available INTEGER NOT NULL, requests_queued INTEGER NOT NULL, requests_wait_ms INTEGER NOT NULL, dcs_queue INTEGER NOT NULL, asyncio_queue INTEGER NOT NULL, time TIMESTAMP NOT NULL DEFAULT (NOW() AT TIME ZONE 'utc'));
1+
CREATE TABLE nodestats (
2+
id SERIAL PRIMARY KEY,
3+
node TEXT NOT NULL,
4+
pool_available INTEGER NOT NULL,
5+
requests_queued INTEGER NOT NULL,
6+
requests_wait_ms INTEGER NOT NULL,
7+
dcs_queue INTEGER NOT NULL,
8+
asyncio_queue INTEGER NOT NULL,
9+
time TIMESTAMP NOT NULL DEFAULT (NOW() AT TIME ZONE 'utc')
10+
);
211
CREATE INDEX IF NOT EXISTS idx_nodestats_node ON nodestats(node);
312
CREATE INDEX IF NOT EXISTS idx_nodestats_time ON nodestats(time);

plugins/battleground/commands.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,15 @@
11
import discord
2-
import psycopg
3-
from discord import app_commands
4-
from discord.app_commands import Group
52

63
from core import Plugin, utils, Channel, Coalition, Server, get_translation
4+
from discord import app_commands
5+
from discord.app_commands import Group
76
from services.bot import DCSServerBot
87

98
_ = get_translation(__name__.split('.')[1])
109

1110

1211
class Battleground(Plugin):
1312

14-
async def rename(self, conn: psycopg.AsyncConnection, old_name: str, new_name: str) -> None:
15-
await conn.execute("UPDATE bg_geometry SET server = %s WHERE server= %s", (new_name, old_name))
16-
1713
battleground = Group(name="battleground", description=_("DCSBattleground commands"))
1814

1915
@battleground.command(description=_('Push MGRS coordinates with screenshots to DCS Battleground'))

plugins/battleground/db/tables.sql

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,18 @@
1-
CREATE TABLE IF NOT EXISTS bg_geometry(id INTEGER PRIMARY KEY, type TEXT NOT NULL, name TEXT, posmgrs TEXT, screenshot TEXT[] DEFAULT '{}'::TEXT[], side TEXT NOT NULL, server TEXT NOT NULL, "position" NUMERIC[] DEFAULT '{}'::NUMERIC[], points NUMERIC[] DEFAULT '{}'::NUMERIC[], center NUMERIC[] DEFAULT '{}'::NUMERIC[], radius NUMERIC DEFAULT 0, discordname TEXT NOT NULL, avatar TEXT NOT NULL);
1+
CREATE TABLE IF NOT EXISTS bg_geometry(
2+
id INTEGER PRIMARY KEY,
3+
type TEXT NOT NULL,
4+
name TEXT,
5+
posmgrs TEXT,
6+
screenshot TEXT[] DEFAULT '{}'::TEXT[],
7+
side TEXT NOT NULL,
8+
server TEXT NOT NULL,
9+
"position" NUMERIC[] DEFAULT '{}'::NUMERIC[],
10+
points NUMERIC[] DEFAULT '{}'::NUMERIC[],
11+
center NUMERIC[] DEFAULT '{}'::NUMERIC[],
12+
radius NUMERIC DEFAULT 0,
13+
discordname TEXT NOT NULL,
14+
avatar TEXT NOT NULL,
15+
FOREIGN KEY (server) REFERENCES servers (server_name) ON UPDATE CASCADE ON DELETE CASCADE
16+
);
217
CREATE INDEX IF NOT EXISTS "bg_geometry$server_side" ON bg_geometry USING btree (server ASC, side ASC);
318
CREATE SEQUENCE IF NOT EXISTS bg_geometry_id_seq INCREMENT 1 START 20000 MINVALUE 1 OWNED BY bg_geometry."id";

0 commit comments

Comments
 (0)