Skip to content

Conversation

@juliansalvador727
Copy link

Purpose

Closes #659 and #649
Converts the entire backend database layer from synchronous to asynchronous operations to improve concurrent request handling and FastAPI compatibility.

New Changes

  • Converted all database operations to async/await pattern
  • Updated database engine to use AsyncEngine and AsyncSession from SQLAlchemy
  • Changed database connection string to use postgresql+asyncpg:// driver
  • Added asyncpg as a dependency for async PostgreSQL support
  • Converted all database wrapper classes and methods to async
  • Updated all API endpoints that interact with the database to async
  • Modified migration script to use asyncio.run() for async execution

Modified Files (22 total):

Core Database Layer:

  • gs/backend/data/database/engine.py
  • gs/backend/config/config.py

Abstract & Main Wrappers:

  • gs/backend/data/data_wrappers/abstract_wrapper.py
  • gs/backend/data/data_wrappers/wrappers.py

MCC Wrappers (8 files):

  • gs/backend/data/data_wrappers/mcc_wrappers/commands_wrapper.py
  • gs/backend/data/data_wrappers/mcc_wrappers/main_command_wrapper.py
  • gs/backend/data/data_wrappers/mcc_wrappers/comms_session_wrapper.py
  • gs/backend/data/data_wrappers/mcc_wrappers/main_telemetry_wrapper.py
  • gs/backend/data/data_wrappers/mcc_wrappers/packet_commands_wrapper.py
  • gs/backend/data/data_wrappers/mcc_wrappers/packet_telemetry_wrapper.py
  • gs/backend/data/data_wrappers/mcc_wrappers/packet_wrapper.py
  • gs/backend/data/data_wrappers/mcc_wrappers/telemetry_wrapper.py

ARO Wrappers (4 files):

  • gs/backend/data/data_wrappers/aro_wrapper/aro_user_login_wrapper.py
  • gs/backend/data/data_wrappers/aro_wrapper/aro_user_auth_token_wrapper.py
  • gs/backend/data/data_wrappers/aro_wrapper/aro_user_data_wrapper.py
  • gs/backend/data/data_wrappers/aro_wrapper/aro_request_wrapper.py

Resource Utils & Migration:

  • gs/backend/data/resources/utils.py
  • gs/backend/migrate.py

API Layer (4 files):

  • gs/backend/api/lifespan.py
  • gs/backend/api/v1/aro/endpoints/user.py
  • gs/backend/api/v1/mcc/endpoints/commands.py
  • gs/backend/api/v1/mcc/endpoints/main_commands.py

Testing

  • I have tested this PR by running the MCC website (Backend starts successfully with async database operations)
  • I have tested this PR by running the ARO website (All user endpoints function correctly with async operations)
  • I have unit-tested this PR. (Backend database tests need to be written/updated for async compatibility)
  • I have tested this PR on a board if the code will run on a board. (N/A - backend only changes)

Testing Notes:

  • FastAPI server starts successfully with fastapi dev gs/backend/main.py
  • All database operations are now non-blocking
  • Migration script works correctly with python3 gs/backend/migrate.py
  • All modules load without errors (verified via pytest coverage report)

Outstanding Changes

  • Unit tests for async database operations need to be written or updated

@github-actions
Copy link

Pull reviewers stats

Stats of the last 120 days for UWOrbital:

User Total reviews Time to review Total comments
Adityya-K
🥇
36
▀▀▀▀
3d 5h 53m
106
▀▀▀
camspec
🥈
34
▀▀▀
3d 3h 23m
191
▀▀▀▀▀
proprogrammer504
🥉
13
7d 1h 30m
▀▀
44
Syzygicality
8
8h 20m
17
kepler452b123
5
1d 11h 18m
2
joannalauu
3
4d 21h 20m
19
c4bae
1
13d 1h 35m
▀▀▀▀
2
panthpatel2016
1
4h 30m
6

⚡️ Pull request stats

Copy link
Contributor

@Syzygicality Syzygicality left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please fix pytests before Kevin and I can make a proper code review! Most failures are due to incorrect typehinting in the pytests and use of .exec() on asyncsession, which should instead be .execute()

@juliansalvador727 juliansalvador727 self-assigned this Jan 21, 2026
@juliansalvador727
Copy link
Author

Summary of Changes

  • requirements.txt - added asyncpg==0.29.0 for SQLAlchemy's async PostgreSQL support
  • gs/backend/data/resources/utils.py - changed all exec(query) -> execute(query) and added .scalars() call
  • gs/backend/data/data_wrappers/abstract_wrapper.py - added attr-defined and unused-coroutine to suppress false positive mypy errors. also added a # type: ignore[unused-coroutine] comment on mcc and aro wrappers to session.delete() calls to show this.
  • I don't know if the changes to interfaces/obc_gs_interface/ax25/__init__.py are necessary
  • they were done in order to pass python_test/test_ax25.py::test_I_frame_encode_decode which is out of the scope of Refactor DB connection to be asynchronous #659 and Refactor backend endpoints (not db) to ensure full async #649.

Copy link
Contributor

@proprogrammer504 proprogrammer504 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

slightly terrified, it looks good mostly. there are some inconsistencies with usage of async and await. additionally, there is a change to the ax25 protocol that i do not understand. this may be a dangerous change. finally, i want another evaluation on whether or not the things which we asynced actually NEED to be asynced. i feel that if something doesnt require it, it should be left as is as it is safer to have less moving parts.

tldr, maybe dont touch ax25, fix inconsistencies.

DATABASE_CONNECTION_STRING: Final[
str
] = f"postgresql+psycopg2://{GS_DATABASE_USER}:{GS_DATABASE_PASSWORD}@{GS_DATABASE_LOCATION}:{GS_DATABASE_PORT}/{GS_DATABASE_NAME}"
] = f"postgresql+asyncpg://{GS_DATABASE_USER}:{GS_DATABASE_PASSWORD}@{GS_DATABASE_LOCATION}:{GS_DATABASE_PORT}/{GS_DATABASE_NAME}"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does this allow for the db to be asynchronous? i thought it already was asynchronous

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the new one is the async driver for psql connection

with get_db_session() as session:
async with get_db_session() as session:
obj = self.model(**data)
session.add(obj)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i mean since we're going nuts and awaiting everything, does this need await as well?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yep, though looking at it, we could just add session: AsyncSession = Depends(get_db_session) into the function args of the abstract wrapper

session.commit()
return obj
# Preserve object state before deleting
obj_copy = self.model(**{c.name: getattr(obj, c.name) for c in obj.__table__.columns}) # type: ignore[attr-defined]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not use session.delete(obj)?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah cursor bugged out here thinking that db deletion deletes the object from the memory, though it doesnt. pls return obj, not obj_copy

if auth_token:
session.delete(auth_token)
session.commit()
session.delete(auth_token) # type: ignore[unused-coroutine]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yea, this seems more logical. also the await seems inconsistent. sometimes session uses await yet other times it does not.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in terms of db connection, awaits should only be used on IO or expensive processes, so usually commit and refresh session methods

raise ValueError("Packet command not found.")
session.delete(command)
session.commit()
session.delete(command) # type: ignore[unused-coroutine]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i just realized that there r a bunch of the type: ignore unused coroutine comments. what r those about?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

linting error escape most likely

from contextlib import asynccontextmanager

from sqlalchemy import text
from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, create_async_engine
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we understand the side effects of using async engine and async session instead of the regular ones?

result = session.exec(query).first()
if not result:
result = await session.execute(query)
if not result.scalars().first():
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what does this check for exactly? would this prevent main commands from being added in some rare cases?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if the db has already been populated with the data, then this conditional ensures that they wont insert it again, so we dont have duplicated datasets in the tables.

if (data[-1] == 0 and len(data) > RS_ENCODED_DATA_SIZE + AX25_NON_INFO_BYTES) or (
data[-1] == 0 and len(data) < RS_ENCODED_DATA_SIZE
):
# Only remove the byte if we're certain it's padding (exactly 1 byte more than expected for I frames)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note that this change could be dangerous, it also is unrelated to making stuff asynchronous. my concern is that this is a part of the ax25 protocol, changing anything here could have unforeseen consequences especially when we dont understand how it works.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i agree. this change is out of the scope of the task

@@ -20,60 +20,97 @@
from gs.backend.data.enums.aro_requests import ARORequestStatus
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note that these endpoints are outdated

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what should be tested and asynced (if not already) are eddies abstracted data wrappers. also correction, they are not endpoints they are data wrappers.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, this is alr a task -> #635

Copy link
Contributor

@Syzygicality Syzygicality left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

some of my own changes for now, will check up on pytests later


if __name__ == "__main__":

async def main() -> None:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wrap this whole script in one async with get_db_session() as session: instead of multiple

obj = await session.get(self.model, obj_id)
if not obj:
raise ValueError(f"{self.model.__name__} with ID {obj_id} not found.")
session.delete(obj)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

revert this change to return obj, not make a obj copy

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Refactor DB connection to be asynchronous

3 participants