From abba807d2ae40d08936fe5a7db26bbf3247e729c Mon Sep 17 00:00:00 2001 From: S0okJu <65109465+D7MeKz@users.noreply.github.com> Date: Tue, 11 Feb 2025 10:25:57 +0900 Subject: [PATCH 01/15] =?UTF-8?q?[Feat]=20/status=20running=EC=8B=9C=20por?= =?UTF-8?q?t=20=EB=B0=98=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/__pycache__/__init__.cpython-39.pyc | Bin 0 -> 206 bytes app/__pycache__/factory.cpython-39.pyc | Bin 0 -> 6118 bytes app/api/challenge.py | 8 ++--- app/extensions/db/repository.py | 10 ++++-- app/extensions/db/repository_test.py | 32 ++++++++++++++++++ .../__pycache__/__init__.cpython-39.pyc | Bin 0 -> 153 bytes .../__pycache__/async_handler.cpython-39.pyc | Bin 0 -> 1839 bytes .../ctf_metrics_collector.cpython-39.pyc | Bin 0 -> 2252 bytes .../__pycache__/loki_logger.cpython-39.pyc | Bin 0 -> 3016 bytes 9 files changed, 44 insertions(+), 6 deletions(-) create mode 100644 app/__pycache__/__init__.cpython-39.pyc create mode 100644 app/__pycache__/factory.cpython-39.pyc create mode 100644 app/extensions/db/repository_test.py create mode 100644 app/monitoring/__pycache__/__init__.cpython-39.pyc create mode 100644 app/monitoring/__pycache__/async_handler.cpython-39.pyc create mode 100644 app/monitoring/__pycache__/ctf_metrics_collector.cpython-39.pyc create mode 100644 app/monitoring/__pycache__/loki_logger.cpython-39.pyc diff --git a/app/__pycache__/__init__.cpython-39.pyc b/app/__pycache__/__init__.cpython-39.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2a3b394cd9571f96403f50e72103b8bf2bb3e31c GIT binary patch literal 206 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 bool: """ return challenge.status == 'Running' - def get_status(self, challenge_id, username) -> Optional[str]: + def get_status(self, challenge_id, username) -> Optional[dict]: """ 챌린지 상태 조회 @@ -132,7 +132,13 @@ def get_status(self, challenge_id, username) -> Optional[str]: str: 챌린지 상태 """ challenge = UserChallenges.query.filter_by(C_idx=challenge_id, username=username).first() - return challenge.status if challenge else None + if not challenge: + return None + + if challenge.status == 'Running': + return {'status': challenge.status, 'port': int(challenge.port)} + return {'status': challenge.status} + class ChallengeRepository: @staticmethod diff --git a/app/extensions/db/repository_test.py b/app/extensions/db/repository_test.py new file mode 100644 index 0000000..9a47d30 --- /dev/null +++ b/app/extensions/db/repository_test.py @@ -0,0 +1,32 @@ +import pytest +from unittest.mock import MagicMock +from app.repositories.user_challenges_repository import UserChallengesRepository +from app.extensions.db.models import UserChallenges + +@pytest.fixture +def mock_session(): + return MagicMock() + +@pytest.fixture +def user_challenges_repo(mock_session): + return UserChallengesRepository(session=mock_session) + +@pytest.fixture +def mock_challenge(): + return UserChallenges(C_idx=1, username="test_user", status="Running", port=8080) + +def test_get_status_running(user_challenges_repo, mock_session, mock_challenge): + mock_session.query.return_value.filter_by.return_value.first.return_value = mock_challenge + result = user_challenges_repo.get_status(challenge_id=1, username="test_user") + assert result == {'status': 'Running', 'port': 8080} + +def test_get_status_not_running(user_challenges_repo, mock_session): + mock_challenge = UserChallenges(C_idx=1, username="test_user", status="Stopped", port=8080) + mock_session.query.return_value.filter_by.return_value.first.return_value = mock_challenge + result = user_challenges_repo.get_status(challenge_id=1, username="test_user") + assert result == {'status': 'Stopped'} + +def test_get_status_not_found(user_challenges_repo, mock_session): + mock_session.query.return_value.filter_by.return_value.first.return_value = None + result = user_challenges_repo.get_status(challenge_id=1, username="nonexistent_user") + assert result is None diff --git a/app/monitoring/__pycache__/__init__.cpython-39.pyc b/app/monitoring/__pycache__/__init__.cpython-39.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8f77c97dc507caab9570e81554a1bd6c1d424bd7 GIT binary patch literal 153 zcmYe~<>g`kg4by)(?RrO5P=LBfgA@QE@lA|DGb33nv8xc8Hzx{2;!HWerR!OQL%nX zVs2`7mA*@Aadt_5fqrsEVopwKUV5r-VnL>UVnKm^Zhl^7Nq$jgUb=pKd?t_`AFo$X Vd5gm)H$SB`C)EyQ?q?uo008T2B}xDQ literal 0 HcmV?d00001 diff --git a/app/monitoring/__pycache__/async_handler.cpython-39.pyc b/app/monitoring/__pycache__/async_handler.cpython-39.pyc new file mode 100644 index 0000000000000000000000000000000000000000..60e9b7680c8026b2de3735dda7a2f4c3cbb1bcd9 GIT binary patch literal 1839 zcmZWqOK%)S5bo}I?Ch@Bm|!3x291b>wLF5bLA=2M3V&1%2g&_y?S*>X|6(Fj99{Kdb7iuWI66uS4Mb=ip-e zt53*ZI9dH%n0yXR4WQ$M(~OMjk&dXHvl07}aK@c8!W}^-&Z#qUIlV{v?%!~S>?PQ* zeoiw{AA=wWzB6J^$pPh*v$J62aEH5R1YYsD52I)IT0DR~|0!|Fs0FXL&Ds}go+n?$ zInRVl*oq32(Q?$J?m(x>DOu8ULdk+INW`?Wbk7M$p7xh5D+Su0v=?O3K`Wjy*z0l! z=G|xw`(+PrbAo>Djn=K|JH>dvkViuHnelZj$3h!7kEh~(f8BU$Rtjl?C`$8GN0DhA z)|MLYcqV4TcwgJ0ofYG#UTNz?DPo@HW8>|9FLG@p~35x+k^gOIg!Iu*rCSJ!r> zEM$m)>C17t5$;vjALQb3DH1JsDC|K9zU3+^8pDMfVZSST0I2a(6)B;c&_b%(7nQ)S)oLq*1m7T(hIiB zZJ53fO<^rSCp)yda>=Foc7SydDIy_eH*yK&u~eP;I# zgyjbyMyd*hR&5o7oGL+8Doa6fp-4gEFS%Eqh|m83QV}zE_Srbeo^E$%cV>6z`(|g& z>FG%VrMuQH{*Wf*FBC?L0>TCO#UfOcP{}4`Ntep9E|(QuDJS%VL{mUoZ++FE1PK zzf)LVEm&wZS`r{!fM2`|6(hPti7r!#$_=6`RG|rI6EsOv&?Z?5zBEm@6sppRMnYF{ zgif|(dW=qiJVB@7Y%~73+%?m-S*6H6U@k9NBIkf;xem7^*k@3%pM*aLzgU3E1sJGY z1b2Uy)|EOjBv+>4_B36T!x4oN*h0Vd4J!|}(&9OdGvV2;@F(ISKNXE-5**3p zz&hawuz&in#oLmf5X@d{6Mhnu{&_F^5u8b6DV*v?EqjrP8?IB$#-^{C)l$~1RV{y%iMv}U1Ofmx?4Ac$6p4w9`$bRb$51yjXPSm zeLoa_2{s#A_uiB4_GWCE&&XVcrF@mMVo5+e^Wz|fNJZ5w&Yl>|4p2A)qXG>{qV~$s z&W)QLa1uk)!nF{(I=N0hCK9$1wtZJWhpG6$Bd*55t_7QWy+)_^eKXj63a)GWzwh>% z;A{JS&}en;H%ZH=DuZyS)d^U}IP7cAC8(o3TE^6NVRZa40hAemJ~< zPGAs_q$<^3JQ|X|K37523JuT*3ik02IiG_L!f{=yOLa&Tx8*Ilu7v!{z`x@C0SfUk zgD2m0cRIkk7Oa2OTW@K>#y8#D9Y6z!kF}PnCO6B&+Y0w{NgJA8x;U;mxVxbB^=|M} zJ9u~ppnlrjX^d-zcN^5A-4|Y8(oD-Oea2qZ7MHYAB@!~R6`(o+=jWKY^BEP8vrWzZ5+P?2;Buj0QcMSfOWJercj zmubr!7va-TSqd)KFW8TXIt#)$QQH{15lx#g9lJfmI(T>&DAM+S?d<>dr?$V>8NHKz zp5lB5xJK$hYpd6Mpei7zW%8LxOt1KO7=}M- z7-ffgHuk3s<5SPHqZxh*Hsa4=g-Hp*$K^h_;PP`gHjmXXZo(d>G@gPAnG_g=s-(`U zDOH`F3di%Aw12W1Cso6;OYq=@i86X}kbaI&YN)Qy4^xBI5 literal 0 HcmV?d00001 diff --git a/app/monitoring/__pycache__/loki_logger.cpython-39.pyc b/app/monitoring/__pycache__/loki_logger.cpython-39.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bd26880c6b51d091b19a9883508aac92acc2d93d GIT binary patch literal 3016 zcmb7G&2J<}6|buPoblM}-7F!54RXYnM!y-lmj_&;a|u97FBN^aXS~0ux}@F?Awi8yhZMH z{K)6T>*PTvj6%+CGU-Zhk4bM=L^aHO8DJJDu~px7q8Z8Wux9uOC$aS=H;t8IrB?Mb%xBhm+u3xQ64UwSU~&z;_&SJU1TGTFk^F#5A)P(IQjYSreCZwt=}8|efeh$C zb73`4ifxj<)@!%zSz(S@JkWVcf=?QaslSF^JP$JBBNnp}oA8l1q%mv(k$k=Bm@w)0 z<8IPXW;QNVIq1i^m2Uj%4Hmah`G)o(NP8ehOw(=rdu8!%p>(k*v3YyAcwH6SWv{=O zZYFuIx^4B1q@OK9`eLWo&B~t6y6r`aw-Vgy@0c)-Gkl9X@P$ul+B#ut z3X8z_8c20F6|`g82p_?-THnS%Lenjl@v$g5EFj3h?>b{=4qZxWQ=R!HA5u8rJk1dF+YOAt&~u<-cs;G+-zx^TSr=i|xmjRQRme+9;J za2v)glXhW(`!G+|>o)6^@#s!W14dr$=}uCXN}G!$>xV_Jd%C1aClip3U@%i_t8%Tn zkPti9)P0p3@A{2b@7^@^M9LGt^K}jTtabE7D`B(p$Y?ep|BROASaUkORGP+eQV{7H(p=A8LzIqzIONa4RSG=rN|k>nf2bn$)xE- z^GGp>nIw;=D5Lp}D$o!}aV2n0tF)(OELE9gdC_$AR|&A6C-+odSX9l1CJ!=B)-9nF z5Ssu&hE=KPduFTv9@-ErDS~?d>6h+3bY(eT4 z-vnV?h|6xn^||}8@B6}r_>+3rpwP$4B{A<@;=b^akmsJbPyE{Og=dmgs}lN%V$V85 zYI8V=N24)iW%^o-K=#=_-}ZEaDkz0U$#;dm`j9>355EnC3FD3y(EZpb&sMnumht3_G0_P4X<_F}P$A5bNqrd&)c=FEi z!QYX>7s!>4|G2ks{FiqgPu~CMAAUD{Zs}I?U_ruZHy5-@(oH3oQ}XeXM(R+ucn-bz z5(uK0y>;bhVvPJS;sC)$!a`sK;kV*#_YOPa`io#a;^qR%tj>}=8!GF_2p!S7YMgDg zW4!xGK2S%FnN0^;<7(lkz+q~g(P06&2!p_%cN-{!hfGGW6F-JlS@=Su^#l$hycd-BcNuEMq*vP#Bjs7!Ut9xiSvQ zmI*eKf`VEvlK_&^bgOW5Wl|>kIxW9O#A3Hv*m^|#KS8gmr3kr;+Bhftk9~I@Zv>B9 z=aoC-J=>(fXJ^gtaUm@}mk?#<(=q!RWVqs7P`wIHBEQYVxnuhGvqHW(I zV(Y6@>=ZUHMOW~X(mMMYYd!sAK(`$=rvDxI3Ea*!dAcpOrnY+W%VJMCmt4*+He9L- V{xWG(%!7pZb+8X*7kBQ({{jR|34H(n literal 0 HcmV?d00001 From 47969b6290c56ddd3db846cdf39cd931c820acd6 Mon Sep 17 00:00:00 2001 From: S0okJu Date: Wed, 12 Feb 2025 13:13:07 +0900 Subject: [PATCH 02/15] =?UTF-8?q?[Feat]=20update=20port=20=EC=B5=9C?= =?UTF-8?q?=EC=8B=A0=20challenge=20=EC=A0=95=EB=B3=B4=EA=B0=92=20=EA=B0=80?= =?UTF-8?q?=EC=A0=B8=EC=98=A4=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/extensions/db/repository.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/extensions/db/repository.py b/app/extensions/db/repository.py index 72bf5ac..cca8bf0 100644 --- a/app/extensions/db/repository.py +++ b/app/extensions/db/repository.py @@ -70,7 +70,11 @@ def update_status(self, challenge: UserChallenges, new_status: str) -> bool: bool: 업데이트 성공 여부 """ try: - challenge.status = new_status + fresh_challenge = self.session.merge(challenge) + + fresh_challenge = self.session.merge(challenge) + self.session.refresh(fresh_challenge) + fresh_challenge.status = new_status # self.session.add(challenge) # Add this line to track the object # self.session.flush() self.session.commit() From da77b17bd4ed7f95b752de82980d2829ca081d6c Mon Sep 17 00:00:00 2001 From: S0okJu Date: Wed, 12 Feb 2025 13:22:43 +0900 Subject: [PATCH 03/15] =?UTF-8?q?[Fix]=20=EC=9D=BD=EA=B8=B0=20=EC=B6=A9?= =?UTF-8?q?=EB=8F=8C=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/extensions/db/repository.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/app/extensions/db/repository.py b/app/extensions/db/repository.py index cca8bf0..4d60968 100644 --- a/app/extensions/db/repository.py +++ b/app/extensions/db/repository.py @@ -99,12 +99,20 @@ def update_port(self, challenge: UserChallenges, port: int) -> bool: """ try: # 1) 먼저 challenge 객체를 세션에 맞게 merge - fresh_challenge = self.session.merge(challenge) + # fresh_challenge = self.session.merge(challenge) - fresh_challenge = self.session.merge(challenge) - self.session.refresh(fresh_challenge) # Ensure fresh data - fresh_challenge.port = port + # self.session.refresh(fresh_challenge) # Ensure fresh data + # fresh_challenge.port = port + # self.session.commit() + # Set session isolation level to avoid stale reads + self.session.execute("SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED") + + # Acquire a lock on the row to prevent race conditions + fresh_challenge = self.session.query(UserChallenges).with_for_update().filter_by(id=challenge.id).one() + + # Update the port + fresh_challenge.port = port self.session.commit() return True except SQLAlchemyError as e: From bccdce7720ed34afd8dc07b73a6d56f6d7b7d1ef Mon Sep 17 00:00:00 2001 From: S0okJu Date: Wed, 12 Feb 2025 13:27:40 +0900 Subject: [PATCH 04/15] =?UTF-8?q?[Fix]=20=EC=9D=BD=EA=B8=B0=20=EC=B6=A9?= =?UTF-8?q?=EB=8F=8C=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/extensions/db/repository.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/extensions/db/repository.py b/app/extensions/db/repository.py index 4d60968..db95f35 100644 --- a/app/extensions/db/repository.py +++ b/app/extensions/db/repository.py @@ -5,7 +5,7 @@ from app.exceptions.api import InternalServerError from app.extensions_manager import db from app.extensions.db.models import Challenges, UserChallenges - +from sqlalchemy.sql import text class UserChallengesRepository: def __init__(self, session=None): @@ -106,11 +106,11 @@ def update_port(self, challenge: UserChallenges, port: int) -> bool: # self.session.commit() # Set session isolation level to avoid stale reads - self.session.execute("SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED") - + self.session.execute(text("SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED")) + # Acquire a lock on the row to prevent race conditions fresh_challenge = self.session.query(UserChallenges).with_for_update().filter_by(id=challenge.id).one() - + # Update the port fresh_challenge.port = port self.session.commit() From 3e1f3dcbc2159fd3bfa3b038d76497710ace49b3 Mon Sep 17 00:00:00 2001 From: S0okJu <65109465+D7MeKz@users.noreply.github.com> Date: Wed, 12 Feb 2025 14:15:13 +0900 Subject: [PATCH 05/15] [Feat] Acquire a lock on teh row to prevent race conditions --- app/extensions/db/repository.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/extensions/db/repository.py b/app/extensions/db/repository.py index db95f35..7f6c17a 100644 --- a/app/extensions/db/repository.py +++ b/app/extensions/db/repository.py @@ -109,7 +109,7 @@ def update_port(self, challenge: UserChallenges, port: int) -> bool: self.session.execute(text("SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED")) # Acquire a lock on the row to prevent race conditions - fresh_challenge = self.session.query(UserChallenges).with_for_update().filter_by(id=challenge.id).one() + fresh_challenge = self.session.query(UserChallenges).with_for_update().filter_by(userChallengeName=challenge.userChallengeName).one() # Update the port fresh_challenge.port = port From 60a50b5230bee02be7912d13a4add5469e9ac6bc Mon Sep 17 00:00:00 2001 From: S0okJu <65109465+D7MeKz@users.noreply.github.com> Date: Sat, 15 Feb 2025 15:04:45 +0900 Subject: [PATCH 06/15] =?UTF-8?q?[Feat]=20update=20=EC=8B=9C=20session=20c?= =?UTF-8?q?ommit?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/extensions/db/repository.py | 169 +++++++++++--------------------- 1 file changed, 58 insertions(+), 111 deletions(-) diff --git a/app/extensions/db/repository.py b/app/extensions/db/repository.py index 7f6c17a..90bee45 100644 --- a/app/extensions/db/repository.py +++ b/app/extensions/db/repository.py @@ -1,31 +1,34 @@ - import logging from typing import List, Optional from sqlalchemy.exc import SQLAlchemyError +from sqlalchemy.orm import Session +from sqlalchemy.sql import text +from contextlib import contextmanager from app.exceptions.api import InternalServerError from app.extensions_manager import db from app.extensions.db.models import Challenges, UserChallenges -from sqlalchemy.sql import text class UserChallengesRepository: - def __init__(self, session=None): - self.session = session or db.session + def __init__(self): + """세션을 직접 관리하는 Repository""" + self.db_session = db.session # Flask-SQLAlchemy 세션 + + @contextmanager + def get_session(self) -> Session: + """독립적인 세션을 생성하고 자동 종료하는 컨텍스트 매니저""" + session = db.session() # 새로운 세션 생성 + try: + yield session # 세션 제공 + session.commit() # 성공하면 커밋 + except SQLAlchemyError as e: + session.rollback() # 예외 발생 시 롤백 + raise InternalServerError(error_msg=f"Database transaction failed: {e}") from e + finally: + session.close() # 세션 종료 def create(self, username: str, C_idx: int, userChallengeName: str, port: int, status: str = 'None') -> Optional[UserChallenges]: - """ - 새로운 사용자 챌린지 생성 - - Args: - username (str): 사용자 이름 - C_idx (int): 챌린지 ID - userChallengeName (str): 챌린지 이름 - port (int): 챌린지 포트 - status (str): 챌린지 상태 - - Returns: - UserChallenges: 생성된 챌린지 - """ + """새로운 사용자 챌린지 생성""" try: challenge = UserChallenges( username=username, @@ -34,122 +37,66 @@ def create(self, username: str, C_idx: int, userChallengeName: str, port=port, status=status ) - self.session.add(challenge) - self.session.commit() + with self.get_session() as session: + session.add(challenge) return challenge except SQLAlchemyError as e: - - self.session.rollback() raise InternalServerError(error_msg=f"Error creating challenge in db: {e}") from e def get_by_user_challenge_name(self, userChallengeName: str) -> Optional[UserChallenges]: - """ - 사용자 챌린지 이름 조회 - - Args: - userChallengeName (str): 사용자 챌린지 이름 - - Returns: - UserChallenges: 사용자 챌린지 - """ - user_challenge = UserChallenges.query.filter_by(userChallengeName=userChallengeName).first() - if not user_challenge: - return None - return user_challenge - + """사용자 챌린지 이름으로 조회""" + with self.get_session() as session: + return session.query(UserChallenges).filter_by(userChallengeName=userChallengeName).first() def update_status(self, challenge: UserChallenges, new_status: str) -> bool: - """ - 사용자 챌린지 상태 업데이트 - - Args: - challenge (UserChallenges): 사용자 챌린지 - new_status (str): 새로운 상태 - - Returns: - bool: 업데이트 성공 여부 - """ + """사용자 챌린지 상태 업데이트""" try: - fresh_challenge = self.session.merge(challenge) - - fresh_challenge = self.session.merge(challenge) - self.session.refresh(fresh_challenge) - fresh_challenge.status = new_status - # self.session.add(challenge) # Add this line to track the object - # self.session.flush() - self.session.commit() + with self.get_session() as session: + fresh_challenge = session.merge(challenge) # 세션과 동기화 + session.refresh(fresh_challenge) + fresh_challenge.status = new_status + session.commit() return True except SQLAlchemyError as e: - # logger.error(f"Error updating challenge status: {e}") - - self.session.rollback() raise InternalServerError(error_msg=f"Error updating challenge status: {e}") from e - def update_port(self, challenge: UserChallenges, port: int) -> bool: - """ - 챌린지 포트 업데이트 - - Args: - challenge (UserChallenges): 사용자 챌린지 - port (int): 새로운 포트 - - Returns: - bool: 업데이트 성공 여부 - """ + """챌린지 포트 업데이트""" try: - # 1) 먼저 challenge 객체를 세션에 맞게 merge - # fresh_challenge = self.session.merge(challenge) - - # self.session.refresh(fresh_challenge) # Ensure fresh data - # fresh_challenge.port = port + with self.get_session() as session: + session.execute(text("SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED")) - # self.session.commit() - # Set session isolation level to avoid stale reads - self.session.execute(text("SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED")) - - # Acquire a lock on the row to prevent race conditions - fresh_challenge = self.session.query(UserChallenges).with_for_update().filter_by(userChallengeName=challenge.userChallengeName).one() - - # Update the port - fresh_challenge.port = port - self.session.commit() + # `.with_for_update()`를 적용하여 동시 수정 방지 + fresh_challenge = session.query(UserChallenges).with_for_update().filter_by(userChallengeName=challenge.userChallengeName).one() + fresh_challenge.port = port + session.commit() return True except SQLAlchemyError as e: - self.session.rollback() raise InternalServerError(error_msg=f"Error updating challenge port: {e}") from e - def is_running(self, challenge: UserChallenges) -> bool: - """ - 챌린지 실행 여부 확인 - - Args: - challenge (UserChallenges): 사용자 챌린지 - - Returns: - bool: 챌린지 실행 여부 - """ + """챌린지 실행 여부 확인""" return challenge.status == 'Running' - def get_status(self, challenge_id, username) -> Optional[dict]: - """ - 챌린지 상태 조회 - - Args: - challenge_id (int): 챌린지 아이디 - username (str): 사용자 이름 - - Returns: - str: 챌린지 상태 - """ - challenge = UserChallenges.query.filter_by(C_idx=challenge_id, username=username).first() - if not challenge: - return None - - if challenge.status == 'Running': - return {'status': challenge.status, 'port': int(challenge.port)} - return {'status': challenge.status} + def get_status(self, challenge_id: int, username: str) -> Optional[dict]: + """챌린지 상태 조회""" + with self.get_session() as session: + challenge = session.query(UserChallenges).filter_by(C_idx=challenge_id, username=username).first() + if not challenge: + return None + return {'status': challenge.status, 'port': int(challenge.port)} if challenge.status == 'Running' else {'status': challenge.status} + + +# class ChallengeRepository: +# def __init__(self): +# self.db_session = db.session + +# def get_challenge_name(self, challenge_id: int) -> Optional[str]: +# """챌린지 ID로 챌린지 조회""" +# with self.get_session() as session: +# challenge = session.query(Challenges).get(challenge_id) +# return challenge.title if challenge else None + class ChallengeRepository: From 34d5fcf7be569bd6ba5c191d34da8e92160c6859 Mon Sep 17 00:00:00 2001 From: S0okJu <65109465+D7MeKz@users.noreply.github.com> Date: Sat, 15 Feb 2025 15:19:28 +0900 Subject: [PATCH 07/15] =?UTF-8?q?[Feat]=20load=20only=EB=A5=BC=20=ED=86=B5?= =?UTF-8?q?=ED=95=B4=20=ED=95=84=EC=9A=94=ED=95=9C=20=EC=86=8D=EC=84=B1?= =?UTF-8?q?=EB=A7=8C=20=EB=A1=9C=EB=93=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/extensions/db/repository.py | 63 ++++++++++++++++++++------------- 1 file changed, 39 insertions(+), 24 deletions(-) diff --git a/app/extensions/db/repository.py b/app/extensions/db/repository.py index 90bee45..51fa2c1 100644 --- a/app/extensions/db/repository.py +++ b/app/extensions/db/repository.py @@ -1,7 +1,8 @@ import logging +from sqlite3 import OperationalError from typing import List, Optional from sqlalchemy.exc import SQLAlchemyError -from sqlalchemy.orm import Session +from sqlalchemy.orm import Session, load_only from sqlalchemy.sql import text from contextlib import contextmanager from app.exceptions.api import InternalServerError @@ -11,7 +12,7 @@ class UserChallengesRepository: def __init__(self): """세션을 직접 관리하는 Repository""" - self.db_session = db.session # Flask-SQLAlchemy 세션 + self.db_session = db.session # Flask-SQLAlchemy Scoped Session @contextmanager def get_session(self) -> Session: @@ -44,49 +45,63 @@ def create(self, username: str, C_idx: int, userChallengeName: str, raise InternalServerError(error_msg=f"Error creating challenge in db: {e}") from e def get_by_user_challenge_name(self, userChallengeName: str) -> Optional[UserChallenges]: - """사용자 챌린지 이름으로 조회""" + """사용자 챌린지 이름으로 조회 (DetachedInstanceError 방지)""" with self.get_session() as session: - return session.query(UserChallenges).filter_by(userChallengeName=userChallengeName).first() + return session.query(UserChallenges) \ + .options(load_only("status", "port")) \ + .filter_by(userChallengeName=userChallengeName).first() def update_status(self, challenge: UserChallenges, new_status: str) -> bool: """사용자 챌린지 상태 업데이트""" try: with self.get_session() as session: - fresh_challenge = session.merge(challenge) # 세션과 동기화 - session.refresh(fresh_challenge) - fresh_challenge.status = new_status - session.commit() + fresh_challenge = session.query(UserChallenges).filter_by(userChallengeName=challenge.userChallengeName).first() + if fresh_challenge: + fresh_challenge.status = new_status + else: + raise InternalServerError(error_msg=f"Challenge not found: {challenge.userChallengeName}") return True except SQLAlchemyError as e: raise InternalServerError(error_msg=f"Error updating challenge status: {e}") from e - def update_port(self, challenge: UserChallenges, port: int) -> bool: - """챌린지 포트 업데이트""" - try: - with self.get_session() as session: - session.execute(text("SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED")) - - # `.with_for_update()`를 적용하여 동시 수정 방지 - fresh_challenge = session.query(UserChallenges).with_for_update().filter_by(userChallengeName=challenge.userChallengeName).one() - fresh_challenge.port = port - session.commit() - return True - except SQLAlchemyError as e: - raise InternalServerError(error_msg=f"Error updating challenge port: {e}") from e + def update_port(self, challenge: UserChallenges, port: int, max_retries: int = 3) -> bool: + """챌린지 포트 업데이트 (트랜잭션 충돌 시 재시도)""" + retries = 0 + while retries < max_retries: + try: + with self.get_session() as session: + session.execute(text("SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED")) + + # `.with_for_update()`를 적용하여 동시 수정 방지 + fresh_challenge = session.query(UserChallenges).with_for_update().filter_by(userChallengeName=challenge.userChallengeName).one() + fresh_challenge.port = port + return True + except OperationalError as e: + if "Record has changed since last read" in str(e): + retries += 1 + continue # 재시도 + raise InternalServerError(error_msg=f"Error updating challenge port: {e}") from e + raise InternalServerError(error_msg="Error updating challenge port after multiple retries") def is_running(self, challenge: UserChallenges) -> bool: - """챌린지 실행 여부 확인""" - return challenge.status == 'Running' + """챌린지가 실행 중인지 확인 (DetachedInstanceError 방지)""" + with self.get_session() as session: + fresh_challenge = session.merge(challenge) # 세션에 다시 연결 + return fresh_challenge.status == 'Running' def get_status(self, challenge_id: int, username: str) -> Optional[dict]: """챌린지 상태 조회""" with self.get_session() as session: - challenge = session.query(UserChallenges).filter_by(C_idx=challenge_id, username=username).first() + challenge = session.query(UserChallenges) \ + .options(load_only("status", "port")) \ + .filter_by(C_idx=challenge_id, username=username).first() if not challenge: return None return {'status': challenge.status, 'port': int(challenge.port)} if challenge.status == 'Running' else {'status': challenge.status} + + # class ChallengeRepository: # def __init__(self): # self.db_session = db.session From 1fb67bd8de629311676a501beef98f7d38847836 Mon Sep 17 00:00:00 2001 From: S0okJu <65109465+D7MeKz@users.noreply.github.com> Date: Sat, 15 Feb 2025 15:26:30 +0900 Subject: [PATCH 08/15] =?UTF-8?q?[Feat]=20load=20only=20argument=20?= =?UTF-8?q?=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/extensions/db/repository.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/extensions/db/repository.py b/app/extensions/db/repository.py index 51fa2c1..4bfdc52 100644 --- a/app/extensions/db/repository.py +++ b/app/extensions/db/repository.py @@ -48,7 +48,7 @@ def get_by_user_challenge_name(self, userChallengeName: str) -> Optional[UserCha """사용자 챌린지 이름으로 조회 (DetachedInstanceError 방지)""" with self.get_session() as session: return session.query(UserChallenges) \ - .options(load_only("status", "port")) \ + .options(load_only(UserChallenges.status, UserChallenges.port)) \ .filter_by(userChallengeName=userChallengeName).first() def update_status(self, challenge: UserChallenges, new_status: str) -> bool: @@ -93,7 +93,7 @@ def get_status(self, challenge_id: int, username: str) -> Optional[dict]: """챌린지 상태 조회""" with self.get_session() as session: challenge = session.query(UserChallenges) \ - .options(load_only("status", "port")) \ + .options(load_only(UserChallenges.status, UserChallenges.port)) \ .filter_by(C_idx=challenge_id, username=username).first() if not challenge: return None From 9dc9094043a2197f62fd812753036d605bb59b18 Mon Sep 17 00:00:00 2001 From: S0okJu <65109465+D7MeKz@users.noreply.github.com> Date: Sat, 15 Feb 2025 15:34:59 +0900 Subject: [PATCH 09/15] =?UTF-8?q?[Feat]=20Session=20=EB=81=8A=EA=B9=80=20?= =?UTF-8?q?=ED=98=84=EC=83=81=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/extensions/db/models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/extensions/db/models.py b/app/extensions/db/models.py index f29b18a..790c007 100644 --- a/app/extensions/db/models.py +++ b/app/extensions/db/models.py @@ -97,8 +97,8 @@ class UserChallenges(db.Model): createdAt = db.Column(db.DateTime, default=current_time_kst, nullable=False) # 관계 설정 - user = relationship('Users', backref='challenges') - challenge = relationship('Challenges', backref='user_challenges') + user = relationship('Users', backref='challenges', lazy='joined') + challenge = relationship('Challenges', backref='user_challenges', lazy='joined') # __table_args__ = ( # CheckConstraint('(port == 0) OR (port >= 15000 AND port <= 30000)', name='checkPort'), From 2beec19da917ef40f7f9f8d673d6f9602a654f99 Mon Sep 17 00:00:00 2001 From: S0okJu <65109465+D7MeKz@users.noreply.github.com> Date: Sat, 15 Feb 2025 16:00:29 +0900 Subject: [PATCH 10/15] =?UTF-8?q?[Feat]=20lazy=3Djoined=20=EC=86=8D?= =?UTF-8?q?=EC=84=B1=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/extensions/db/models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/extensions/db/models.py b/app/extensions/db/models.py index 790c007..f29b18a 100644 --- a/app/extensions/db/models.py +++ b/app/extensions/db/models.py @@ -97,8 +97,8 @@ class UserChallenges(db.Model): createdAt = db.Column(db.DateTime, default=current_time_kst, nullable=False) # 관계 설정 - user = relationship('Users', backref='challenges', lazy='joined') - challenge = relationship('Challenges', backref='user_challenges', lazy='joined') + user = relationship('Users', backref='challenges') + challenge = relationship('Challenges', backref='user_challenges') # __table_args__ = ( # CheckConstraint('(port == 0) OR (port >= 15000 AND port <= 30000)', name='checkPort'), From 691a6fff4550b2957ec3431df61183849d048e22 Mon Sep 17 00:00:00 2001 From: S0okJu <65109465+D7MeKz@users.noreply.github.com> Date: Sat, 15 Feb 2025 16:05:37 +0900 Subject: [PATCH 11/15] =?UTF-8?q?[Feat]=20with=20for=20update=EB=A5=BC=20?= =?UTF-8?q?=ED=99=9C=EC=9A=A9=ED=95=9C=20=EB=8F=99=EC=8B=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=EB=B0=A9=EC=A7=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/extensions/db/repository.py | 61 +++++++++++++-------------------- 1 file changed, 23 insertions(+), 38 deletions(-) diff --git a/app/extensions/db/repository.py b/app/extensions/db/repository.py index 4bfdc52..493e995 100644 --- a/app/extensions/db/repository.py +++ b/app/extensions/db/repository.py @@ -12,7 +12,7 @@ class UserChallengesRepository: def __init__(self): """세션을 직접 관리하는 Repository""" - self.db_session = db.session # Flask-SQLAlchemy Scoped Session + self.db_session = db.session # Flask-SQLAlchemy 세션 @contextmanager def get_session(self) -> Session: @@ -45,62 +45,48 @@ def create(self, username: str, C_idx: int, userChallengeName: str, raise InternalServerError(error_msg=f"Error creating challenge in db: {e}") from e def get_by_user_challenge_name(self, userChallengeName: str) -> Optional[UserChallenges]: - """사용자 챌린지 이름으로 조회 (DetachedInstanceError 방지)""" + """사용자 챌린지 이름으로 조회""" with self.get_session() as session: - return session.query(UserChallenges) \ - .options(load_only(UserChallenges.status, UserChallenges.port)) \ - .filter_by(userChallengeName=userChallengeName).first() + return session.query(UserChallenges).filter_by(userChallengeName=userChallengeName).first() def update_status(self, challenge: UserChallenges, new_status: str) -> bool: """사용자 챌린지 상태 업데이트""" try: with self.get_session() as session: - fresh_challenge = session.query(UserChallenges).filter_by(userChallengeName=challenge.userChallengeName).first() - if fresh_challenge: - fresh_challenge.status = new_status - else: - raise InternalServerError(error_msg=f"Challenge not found: {challenge.userChallengeName}") + fresh_challenge = session.merge(challenge) # 세션과 동기화 + session.refresh(fresh_challenge) + fresh_challenge.status = new_status + session.commit() return True except SQLAlchemyError as e: raise InternalServerError(error_msg=f"Error updating challenge status: {e}") from e - def update_port(self, challenge: UserChallenges, port: int, max_retries: int = 3) -> bool: - """챌린지 포트 업데이트 (트랜잭션 충돌 시 재시도)""" - retries = 0 - while retries < max_retries: - try: - with self.get_session() as session: - session.execute(text("SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED")) - - # `.with_for_update()`를 적용하여 동시 수정 방지 - fresh_challenge = session.query(UserChallenges).with_for_update().filter_by(userChallengeName=challenge.userChallengeName).one() - fresh_challenge.port = port - return True - except OperationalError as e: - if "Record has changed since last read" in str(e): - retries += 1 - continue # 재시도 - raise InternalServerError(error_msg=f"Error updating challenge port: {e}") from e - raise InternalServerError(error_msg="Error updating challenge port after multiple retries") + def update_port(self, challenge: UserChallenges, port: int) -> bool: + """챌린지 포트 업데이트""" + try: + with self.get_session() as session: + session.execute(text("SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED")) + + # .with_for_update()를 적용하여 동시 수정 방지 + fresh_challenge = session.query(UserChallenges).with_for_update().filter_by(userChallengeName=challenge.userChallengeName).one() + fresh_challenge.port = port + session.commit() + return True + except SQLAlchemyError as e: + raise InternalServerError(error_msg=f"Error updating challenge port: {e}") from e def is_running(self, challenge: UserChallenges) -> bool: - """챌린지가 실행 중인지 확인 (DetachedInstanceError 방지)""" - with self.get_session() as session: - fresh_challenge = session.merge(challenge) # 세션에 다시 연결 - return fresh_challenge.status == 'Running' + """챌린지 실행 여부 확인""" + return challenge.status == 'Running' def get_status(self, challenge_id: int, username: str) -> Optional[dict]: """챌린지 상태 조회""" with self.get_session() as session: - challenge = session.query(UserChallenges) \ - .options(load_only(UserChallenges.status, UserChallenges.port)) \ - .filter_by(C_idx=challenge_id, username=username).first() + challenge = session.query(UserChallenges).filter_by(C_idx=challenge_id, username=username).first() if not challenge: return None return {'status': challenge.status, 'port': int(challenge.port)} if challenge.status == 'Running' else {'status': challenge.status} - - # class ChallengeRepository: # def __init__(self): @@ -113,7 +99,6 @@ def get_status(self, challenge_id: int, username: str) -> Optional[dict]: # return challenge.title if challenge else None - class ChallengeRepository: @staticmethod def get_challenge_name(challenge_id: int) -> Optional[str]: From d1dc1950031ac2132fdf47ec3ef36b0223e66523 Mon Sep 17 00:00:00 2001 From: S0okJu <65109465+D7MeKz@users.noreply.github.com> Date: Sat, 15 Feb 2025 16:19:46 +0900 Subject: [PATCH 12/15] [Feat] Rollback --- app/extensions/db/repository.py | 141 ++++++++++++++++++++++---------- 1 file changed, 96 insertions(+), 45 deletions(-) diff --git a/app/extensions/db/repository.py b/app/extensions/db/repository.py index 493e995..e3ee17f 100644 --- a/app/extensions/db/repository.py +++ b/app/extensions/db/repository.py @@ -10,26 +10,24 @@ from app.extensions.db.models import Challenges, UserChallenges class UserChallengesRepository: - def __init__(self): - """세션을 직접 관리하는 Repository""" - self.db_session = db.session # Flask-SQLAlchemy 세션 - - @contextmanager - def get_session(self) -> Session: - """독립적인 세션을 생성하고 자동 종료하는 컨텍스트 매니저""" - session = db.session() # 새로운 세션 생성 - try: - yield session # 세션 제공 - session.commit() # 성공하면 커밋 - except SQLAlchemyError as e: - session.rollback() # 예외 발생 시 롤백 - raise InternalServerError(error_msg=f"Database transaction failed: {e}") from e - finally: - session.close() # 세션 종료 + def __init__(self, session=None): + self.session = session or db.session def create(self, username: str, C_idx: int, userChallengeName: str, port: int, status: str = 'None') -> Optional[UserChallenges]: - """새로운 사용자 챌린지 생성""" + """ + 새로운 사용자 챌린지 생성 + + Args: + username (str): 사용자 이름 + C_idx (int): 챌린지 ID + userChallengeName (str): 챌린지 이름 + port (int): 챌린지 포트 + status (str): 챌린지 상태 + + Returns: + UserChallenges: 생성된 챌린지 + """ try: challenge = UserChallenges( username=username, @@ -38,55 +36,108 @@ def create(self, username: str, C_idx: int, userChallengeName: str, port=port, status=status ) - with self.get_session() as session: - session.add(challenge) + self.session.add(challenge) + self.session.commit() return challenge except SQLAlchemyError as e: + + self.session.rollback() raise InternalServerError(error_msg=f"Error creating challenge in db: {e}") from e def get_by_user_challenge_name(self, userChallengeName: str) -> Optional[UserChallenges]: - """사용자 챌린지 이름으로 조회""" - with self.get_session() as session: - return session.query(UserChallenges).filter_by(userChallengeName=userChallengeName).first() + """ + 사용자 챌린지 이름 조회 + + Args: + userChallengeName (str): 사용자 챌린지 이름 + + Returns: + UserChallenges: 사용자 챌린지 + """ + user_challenge = UserChallenges.query.filter_by(userChallengeName=userChallengeName).first() + if not user_challenge: + return None + return user_challenge + def update_status(self, challenge: UserChallenges, new_status: str) -> bool: - """사용자 챌린지 상태 업데이트""" + """ + 사용자 챌린지 상태 업데이트 + + Args: + challenge (UserChallenges): 사용자 챌린지 + new_status (str): 새로운 상태 + + Returns: + bool: 업데이트 성공 여부 + """ try: - with self.get_session() as session: - fresh_challenge = session.merge(challenge) # 세션과 동기화 - session.refresh(fresh_challenge) - fresh_challenge.status = new_status - session.commit() + db.session.execute(text("SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED")) + fresh_challenge = self.session.merge(challenge) + fresh_challenge = self.session.merge(challenge) + self.session.refresh(fresh_challenge) + fresh_challenge.status = new_status + # self.session.add(challenge) # Add this line to track the object + # self.session.flush() + self.session.commit() return True except SQLAlchemyError as e: + # logger.error(f"Error updating challenge status: {e}") + + self.session.rollback() raise InternalServerError(error_msg=f"Error updating challenge status: {e}") from e + def update_port(self, challenge: UserChallenges, port: int) -> bool: - """챌린지 포트 업데이트""" + """ + 챌린지 포트 업데이트 + + Args: + challenge (UserChallenges): 사용자 챌린지 + port (int): 새로운 포트 + + Returns: + bool: 업데이트 성공 여부 + """ try: - with self.get_session() as session: - session.execute(text("SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED")) - - # .with_for_update()를 적용하여 동시 수정 방지 - fresh_challenge = session.query(UserChallenges).with_for_update().filter_by(userChallengeName=challenge.userChallengeName).one() + with db.session.begin(): # 트랜잭션 자동 관리 + db.session.execute(text("SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED")) + fresh_challenge = db.session.query(UserChallenges).with_for_update().filter_by(userChallengeName=challenge.userChallengeName).one() fresh_challenge.port = port - session.commit() return True except SQLAlchemyError as e: + db.session.rollback() raise InternalServerError(error_msg=f"Error updating challenge port: {e}") from e - def is_running(self, challenge: UserChallenges) -> bool: - """챌린지 실행 여부 확인""" + """ + 챌린지 실행 여부 확인 + + Args: + challenge (UserChallenges): 사용자 챌린지 + + Returns: + bool: 챌린지 실행 여부 + """ return challenge.status == 'Running' - def get_status(self, challenge_id: int, username: str) -> Optional[dict]: - """챌린지 상태 조회""" - with self.get_session() as session: - challenge = session.query(UserChallenges).filter_by(C_idx=challenge_id, username=username).first() - if not challenge: - return None - return {'status': challenge.status, 'port': int(challenge.port)} if challenge.status == 'Running' else {'status': challenge.status} - + def get_status(self, challenge_id, username) -> Optional[dict]: + """ + 챌린지 상태 조회 + + Args: + challenge_id (int): 챌린지 아이디 + username (str): 사용자 이름 + + Returns: + str: 챌린지 상태 + """ + challenge = UserChallenges.query.filter_by(C_idx=challenge_id, username=username).first() + if not challenge: + return None + + if challenge.status == 'Running': + return {'status': challenge.status, 'port': int(challenge.port)} + return {'status': challenge.status} # class ChallengeRepository: # def __init__(self): From 944297be25edea6ccfc5bcda1007e95d2ebad700 Mon Sep 17 00:00:00 2001 From: S0okJu <65109465+D7MeKz@users.noreply.github.com> Date: Sat, 15 Feb 2025 16:24:17 +0900 Subject: [PATCH 13/15] [Feat] Rollback --- app/extensions/db/repository.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/extensions/db/repository.py b/app/extensions/db/repository.py index e3ee17f..c5d729d 100644 --- a/app/extensions/db/repository.py +++ b/app/extensions/db/repository.py @@ -74,7 +74,6 @@ def update_status(self, challenge: UserChallenges, new_status: str) -> bool: try: db.session.execute(text("SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED")) fresh_challenge = self.session.merge(challenge) - fresh_challenge = self.session.merge(challenge) self.session.refresh(fresh_challenge) fresh_challenge.status = new_status # self.session.add(challenge) # Add this line to track the object @@ -100,10 +99,11 @@ def update_port(self, challenge: UserChallenges, port: int) -> bool: bool: 업데이트 성공 여부 """ try: - with db.session.begin(): # 트랜잭션 자동 관리 - db.session.execute(text("SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED")) - fresh_challenge = db.session.query(UserChallenges).with_for_update().filter_by(userChallengeName=challenge.userChallengeName).one() - fresh_challenge.port = port + db.session.execute(text("SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED")) + fresh_challenge = self.session.merge(challenge) + self.session.refresh(fresh_challenge) + fresh_challenge.status = port + self.session.commit() return True except SQLAlchemyError as e: db.session.rollback() From a67b37550123f72a8ad0deb390b8c2088b8b3edc Mon Sep 17 00:00:00 2001 From: S0okJu <65109465+D7MeKz@users.noreply.github.com> Date: Sat, 15 Feb 2025 21:20:08 +0900 Subject: [PATCH 14/15] =?UTF-8?q?[Feat]=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=EC=BF=BC=EB=A6=AC=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/extensions/db/repository.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/extensions/db/repository.py b/app/extensions/db/repository.py index c5d729d..064ef4c 100644 --- a/app/extensions/db/repository.py +++ b/app/extensions/db/repository.py @@ -72,7 +72,6 @@ def update_status(self, challenge: UserChallenges, new_status: str) -> bool: bool: 업데이트 성공 여부 """ try: - db.session.execute(text("SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED")) fresh_challenge = self.session.merge(challenge) self.session.refresh(fresh_challenge) fresh_challenge.status = new_status @@ -99,7 +98,6 @@ def update_port(self, challenge: UserChallenges, port: int) -> bool: bool: 업데이트 성공 여부 """ try: - db.session.execute(text("SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED")) fresh_challenge = self.session.merge(challenge) self.session.refresh(fresh_challenge) fresh_challenge.status = port From d3b53c917a2ab3eab57ff150de5283b1aec14c95 Mon Sep 17 00:00:00 2001 From: S0okJu <65109465+D7MeKz@users.noreply.github.com> Date: Sat, 15 Feb 2025 21:28:43 +0900 Subject: [PATCH 15/15] =?UTF-8?q?[Fix]=20=EC=9E=98=EB=AA=BB=EB=90=9C=20?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=A0=80=EC=9E=A5=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/extensions/db/repository.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/extensions/db/repository.py b/app/extensions/db/repository.py index 064ef4c..fc48a0a 100644 --- a/app/extensions/db/repository.py +++ b/app/extensions/db/repository.py @@ -72,6 +72,7 @@ def update_status(self, challenge: UserChallenges, new_status: str) -> bool: bool: 업데이트 성공 여부 """ try: + db.session.execute(text("SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED")) fresh_challenge = self.session.merge(challenge) self.session.refresh(fresh_challenge) fresh_challenge.status = new_status @@ -98,9 +99,10 @@ def update_port(self, challenge: UserChallenges, port: int) -> bool: bool: 업데이트 성공 여부 """ try: + db.session.execute(text("SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED")) fresh_challenge = self.session.merge(challenge) self.session.refresh(fresh_challenge) - fresh_challenge.status = port + fresh_challenge.port = port self.session.commit() return True except SQLAlchemyError as e: