Skip to content

Commit 4075255

Browse files
committed
feat: Add load_usage_logs parameter to node retrieval functions and update related calls
1 parent 758c6cb commit 4075255

12 files changed

Lines changed: 155 additions & 76 deletions

File tree

app/db/crud/admin.py

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -530,9 +530,7 @@ async def get_usage_percentage_reached_admins(
530530
return list((await db.execute(stmt)).scalars().all())
531531

532532

533-
async def bulk_create_admin_notification_reminders(
534-
db: AsyncSession, reminder_data: list[dict]
535-
) -> list[dict]:
533+
async def bulk_create_admin_notification_reminders(db: AsyncSession, reminder_data: list[dict]) -> list[dict]:
536534
"""Bulk-insert admin reminder rows after successful sends."""
537535
if not reminder_data:
538536
return []
@@ -550,11 +548,7 @@ async def bulk_create_admin_notification_reminders(
550548
types = {d["type"] for d in unique_reminder_data}
551549

552550
# Lock the Admin rows to serialize reminder checks/creation for these admins
553-
await db.execute(
554-
select(Admin.id)
555-
.where(Admin.id.in_(list(admin_ids)))
556-
.with_for_update()
557-
)
551+
await db.execute(select(Admin.id).where(Admin.id.in_(list(admin_ids))).with_for_update())
558552

559553
# Fetch existing reminders that match these criteria
560554
stmt = select(AdminNotificationReminder).where(

app/db/crud/node.py

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -46,14 +46,16 @@ def _build_node_simple_sort_clause(sort_option: NodeSimpleSortOption):
4646
return column.desc() if sort_option.value.startswith("-") else column.asc()
4747

4848

49-
async def load_node_attrs(node: Node):
49+
async def load_node_attrs(node: Node, *, load_usage_logs: bool = True):
50+
if not load_usage_logs:
51+
return
5052
try:
5153
await node.awaitable_attrs.usage_logs
5254
except AttributeError:
5355
pass
5456

5557

56-
async def get_node(db: AsyncSession, name: str) -> Node | None:
58+
async def get_node(db: AsyncSession, name: str, *, load_usage_logs: bool = True) -> Node | None:
5759
"""
5860
Retrieves a node by its name.
5961
@@ -66,11 +68,11 @@ async def get_node(db: AsyncSession, name: str) -> Node | None:
6668
"""
6769
node = (await db.execute(select(Node).where(Node.name == name))).unique().scalar_one_or_none()
6870
if node:
69-
await load_node_attrs(node)
71+
await load_node_attrs(node, load_usage_logs=load_usage_logs)
7072
return node
7173

7274

73-
async def get_node_by_id(db: AsyncSession, node_id: int) -> Node | None:
75+
async def get_node_by_id(db: AsyncSession, node_id: int, *, load_usage_logs: bool = True) -> Node | None:
7476
"""
7577
Retrieves a node by its ID.
7678
@@ -83,13 +85,15 @@ async def get_node_by_id(db: AsyncSession, node_id: int) -> Node | None:
8385
"""
8486
node = (await db.execute(select(Node).where(Node.id == node_id))).unique().scalar_one_or_none()
8587
if node:
86-
await load_node_attrs(node)
88+
await load_node_attrs(node, load_usage_logs=load_usage_logs)
8789
return node
8890

8991

9092
async def get_nodes(
9193
db: AsyncSession,
9294
query: NodeListQuery,
95+
*,
96+
load_usage_logs: bool = True,
9397
) -> tuple[list[Node], int]:
9498
"""
9599
Retrieves nodes based on optional status, enabled, id, and search filters.
@@ -143,8 +147,9 @@ async def get_nodes(
143147
# Order by created_at and id for consistent results
144148
stmt = stmt.order_by(Node.created_at.asc(), Node.id.asc())
145149

146-
# Eagerly load usage_logs to avoid N+1 queries (one extra SELECT per node)
147-
stmt = stmt.options(selectinload(Node.usage_logs))
150+
# Eagerly load usage_logs for API lifetime_* fields (skip for jobs/connect)
151+
if load_usage_logs:
152+
stmt = stmt.options(selectinload(Node.usage_logs))
148153

149154
db_nodes = (await db.execute(stmt)).unique().scalars().all()
150155

@@ -208,7 +213,7 @@ async def get_limited_nodes(db: AsyncSession) -> list[Node]:
208213
Returns:
209214
list[Node]: Nodes that should be limited
210215
"""
211-
query = select(Node).options(selectinload(Node.usage_logs)).where(
216+
query = select(Node).where(
212217
and_(
213218
Node.status.in_([NodeStatus.error, NodeStatus.connected, NodeStatus.connecting]),
214219
Node.is_limited,
@@ -503,7 +508,6 @@ async def update_node_status(
503508
# If the instance was detached (e.g., used across sessions), re-fetch it
504509
db_node = (await db.execute(select(Node).where(Node.id == db_node.id))).scalar_one()
505510

506-
await load_node_attrs(db_node)
507511
return db_node
508512

509513

@@ -799,10 +803,11 @@ async def bulk_reset_node_usage(db: AsyncSession, nodes: list[Node]) -> list[Nod
799803
# Re-fetch all nodes in a single query instead of N individual refreshes
800804
node_ids = [node.id for node in nodes]
801805
refreshed = (
802-
await db.execute(
803-
select(Node).options(selectinload(Node.usage_logs)).where(Node.id.in_(node_ids))
804-
)
805-
).unique().scalars().all()
806+
(await db.execute(select(Node).options(selectinload(Node.usage_logs)).where(Node.id.in_(node_ids))))
807+
.unique()
808+
.scalars()
809+
.all()
810+
)
806811
# Preserve input order
807812
refreshed_by_id = {n.id: n for n in refreshed}
808813
return [refreshed_by_id[nid] for nid in node_ids if nid in refreshed_by_id]

app/db/models.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -751,7 +751,9 @@ class AdminNotificationReminder(Base, CreatedAtUTCMixin):
751751
__tablename__ = "admin_notification_reminders"
752752
__table_args__ = (
753753
Index("ix_admin_notification_reminders_admin_id_type", "admin_id", "type"),
754-
UniqueConstraint("admin_id", "type", "threshold", name="uq_admin_notification_reminders_admin_id_type_threshold"),
754+
UniqueConstraint(
755+
"admin_id", "type", "threshold", name="uq_admin_notification_reminders_admin_id_type_threshold"
756+
),
755757
)
756758
admin_id: Mapped[int] = fk_id_column("admins.id", ondelete="CASCADE")
757759
admin: Mapped[Admin] = relationship(back_populates="notification_reminders", init=False)

app/jobs/node_checker.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,7 @@ async def node_health_check():
209209
if not runtime_settings.role.runs_node:
210210
return
211211
async with GetDB() as db:
212-
db_nodes, _ = await get_nodes(db=db, query=NodeListQuery(status=ACTIVE_NODE_STATUSES))
212+
db_nodes, _ = await get_nodes(db=db, query=NodeListQuery(status=ACTIVE_NODE_STATUSES), load_usage_logs=False)
213213

214214
dict_nodes = await node_manager.get_nodes()
215215
check_tasks = [process_node_health_check(db_node, dict_nodes.get(db_node.id)) for db_node in db_nodes]
@@ -224,7 +224,7 @@ async def initialize_nodes():
224224
logger.info("Starting nodes' cores...")
225225

226226
async with GetDB() as db:
227-
db_nodes, _ = await get_nodes(db=db, query=NodeListQuery(status=ACTIVE_NODE_STATUSES))
227+
db_nodes, _ = await get_nodes(db=db, query=NodeListQuery(status=ACTIVE_NODE_STATUSES), load_usage_logs=False)
228228

229229
if not db_nodes:
230230
logger.warning("Attention: You have no node, you need to have at least one node")

app/jobs/review_admins.py

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -79,16 +79,11 @@ async def _send_usage_limit_warning_notifications(db):
7979
candidate_ids = [admin.id for admin in candidate_admins]
8080

8181
# Lock the Admin rows to serialize reminder checks/creation for these admins
82-
await db.execute(
83-
select(Admin.id)
84-
.where(Admin.id.in_(candidate_ids))
85-
.with_for_update()
86-
)
82+
await db.execute(select(Admin.id).where(Admin.id.in_(candidate_ids)).with_for_update())
8783

8884
# Fetch existing reminders for this threshold to avoid duplicate notifications
8985
result = await db.execute(
90-
select(AdminNotificationReminder.admin_id)
91-
.where(
86+
select(AdminNotificationReminder.admin_id).where(
9287
AdminNotificationReminder.admin_id.in_(candidate_ids),
9388
AdminNotificationReminder.type == ReminderType.data_usage,
9489
AdminNotificationReminder.threshold == threshold,

app/node/worker.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ async def _update_node(self, data: dict):
154154
if not node_id:
155155
return
156156
async with GetDB() as db:
157-
db_node = await get_node_by_id(db, node_id)
157+
db_node = await get_node_by_id(db, node_id, load_usage_logs=False)
158158
if db_node:
159159
await node_manager.update_node(db_node)
160160

@@ -180,14 +180,15 @@ async def _connect_nodes_bulk(self, data: dict):
180180
await core_manager._reload_from_cache()
181181
async with GetDB() as db:
182182
if node_ids:
183-
nodes, _ = await get_nodes(db, query=NodeListQuery(ids=node_ids))
183+
nodes, _ = await get_nodes(db, query=NodeListQuery(ids=node_ids), load_usage_logs=False)
184184
else:
185185
nodes, _ = await get_nodes(
186186
db,
187187
query=NodeListQuery(
188188
core_id=core_id,
189189
status=[NodeStatus.connected, NodeStatus.connecting, NodeStatus.error],
190190
),
191+
load_usage_logs=False,
191192
)
192193
await self._node_operator.connect_nodes_bulk(db, nodes)
193194

app/operation/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -254,9 +254,9 @@ async def get_validated_user_template(self, db: AsyncSession, template_id: int)
254254
await self.raise_error("User Template not found", 404)
255255
return dbuser_template
256256

257-
async def get_validated_node(self, db: AsyncSession, node_id) -> Node:
257+
async def get_validated_node(self, db: AsyncSession, node_id, *, load_usage_logs: bool = True) -> Node:
258258
"""Dependency: Fetch node or return not found error."""
259-
db_node = await get_node_by_id(db, node_id)
259+
db_node = await get_node_by_id(db, node_id, load_usage_logs=load_usage_logs)
260260
if not db_node:
261261
await self.raise_error(message="Node not found", code=404)
262262
return db_node

app/operation/node.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ async def _update_single_node_status(
154154
node_version (str): Node version.
155155
send_notification (bool): Whether to send notification.
156156
"""
157-
db_node = await get_node_by_id(db, node_id)
157+
db_node = await get_node_by_id(db, node_id, load_usage_logs=False)
158158
if not db_node:
159159
return
160160

@@ -540,15 +540,15 @@ async def clear_usage_data(self, db: AsyncSession, table: UsageTable, query: Nod
540540
await self.raise_error(code=400, message=f"Deletion failed due to server error: {e!s}")
541541

542542
async def update_node(self, db: AsyncSession, node_id: int) -> dict:
543-
await self.get_validated_node(db, node_id)
543+
await self.get_validated_node(db, node_id, load_usage_logs=False)
544544
return await self._update_node_api_impl(node_id)
545545

546546
async def update_core(self, db: AsyncSession, node_id: int, node_core_update: NodeCoreUpdate) -> dict:
547-
await self.get_validated_node(db, node_id)
547+
await self.get_validated_node(db, node_id, load_usage_logs=False)
548548
return await self._update_core_impl(node_id, node_core_update)
549549

550550
async def update_geofiles(self, db: AsyncSession, node_id: int, node_geofiles_update: NodeGeoFilesUpdate) -> dict:
551-
await self.get_validated_node(db, node_id)
551+
await self.get_validated_node(db, node_id, load_usage_logs=False)
552552
return await self._update_geofiles_impl(node_id, node_geofiles_update)
553553

554554
async def _update_node_local(self, db_node: Node) -> None:
@@ -635,7 +635,7 @@ async def _connect_nodes_bulk_remote(self, db: AsyncSession, nodes: list[Node])
635635
await node_nats_client.publish("connect_nodes_bulk", {"node_ids": [node.id for node in nodes]})
636636

637637
async def _connect_single_node_local(self, db: AsyncSession, node_id: int) -> None:
638-
db_node = await get_node_by_id(db, node_id)
638+
db_node = await get_node_by_id(db, node_id, load_usage_logs=False)
639639
if db_node is None or db_node.status in (NodeStatus.disabled, NodeStatus.limited):
640640
return
641641

@@ -714,6 +714,7 @@ async def _restart_all_nodes_local(self, db: AsyncSession, admin: AdminDetails,
714714
core_id=core_id,
715715
status=[NodeStatus.connected, NodeStatus.connecting, NodeStatus.error],
716716
),
717+
load_usage_logs=False,
717718
)
718719
await self.connect_nodes_bulk(db, nodes)
719720

@@ -1011,7 +1012,7 @@ async def _get_validated_nodes(self, db: AsyncSession, node_ids: list[int] | set
10111012
return []
10121013

10131014
ids_list = list(node_ids)
1014-
db_nodes, _ = await get_nodes(db, NodeListQuery(ids=ids_list, limit=len(ids_list)))
1015+
db_nodes, _ = await get_nodes(db, NodeListQuery(ids=ids_list, limit=len(ids_list)), load_usage_logs=False)
10151016

10161017
found_ids = {n.id for n in db_nodes}
10171018
missing = set(ids_list) - found_ids

tests/api/test_bulk.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -127,9 +127,7 @@ def test_bulk_add_wireguard_group_allocates_existing_user_peer_ips(access_token)
127127
type="wg",
128128
fallbacks=[],
129129
)
130-
wg_group = create_group(
131-
access_token, name=unique_name("wg_bulk_add_group"), inbound_tags=[interface_name]
132-
)
130+
wg_group = create_group(access_token, name=unique_name("wg_bulk_add_group"), inbound_tags=[interface_name])
133131

134132
response = client.post(
135133
"/api/groups/bulk/add",

tests/api/test_host.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -371,4 +371,3 @@ def test_host_finalmask_new_types(access_token):
371371
finally:
372372
client.delete(f"/api/host/{host_id}", headers={"Authorization": f"Bearer {access_token}"})
373373
delete_core(access_token, core["id"])
374-

0 commit comments

Comments
 (0)