44from models_library .projects import ProjectID
55from pydantic import TypeAdapter
66from servicelib .common_headers import UNDEFINED_DEFAULT_SIMCORE_USER_AGENT_VALUE
7- from servicelib .utils import logged_gather
7+ from servicelib .logging_utils import log_catch , log_context
88
99from ..projects import _projects_service
10- from ..projects .exceptions import ProjectLockError , ProjectNotFoundError
1110from ..redis import get_redis_lock_manager_client
1211from ..resource_manager .registry import (
1312 RedisResourceRegistry ,
1413)
15- from .settings import GUEST_USER_RC_LOCK_FORMAT
1614
1715_logger = logging .getLogger (__name__ )
1816
@@ -22,15 +20,12 @@ async def remove_disconnected_user_resources(
2220) -> None :
2321 lock_manager = get_redis_lock_manager_client (app )
2422
25- #
26- # In redis jargon, every entry is denoted as "key"
27- # - A key can contain one or more fields: name-value pairs
28- # - A key can have a limited livespan by setting the Time-to-live (TTL) which
29- # is automatically decreasing
30- # - Every user can open multiple sessions (e.g. in different tabs and/or browser) and
31- # each session is hierarchically represented in the redis registry with two keys:
32- # - "alive" is a string that keeps a TLL of the user session
33- # - "resources" is a hash toto keep project and websocket ids
23+ # NOTE:
24+ # Each user session is represented in the redis registry with two keys:
25+ # - "alive" is a string that keeps a TTL of the user session
26+ # - "resources" is a redis hash to keep project and websocket ids attached to the user session
27+ # when the alive key expires, it means the user session is disconnected
28+ # and the resources attached to that user session shall be closed and removed
3429 #
3530
3631 _ , dead_user_sessions = await registry .get_all_resource_keys ()
@@ -40,38 +35,15 @@ async def remove_disconnected_user_resources(
4035 for dead_session in dead_user_sessions :
4136 user_id = int (dead_session ["user_id" ])
4237
43- if await lock_manager .lock (
44- GUEST_USER_RC_LOCK_FORMAT .format (user_id = user_id )
45- ).locked ():
46- _logger .info (
47- "Skipping garbage-collecting %s since it is still locked" ,
48- f"{ user_id = } " ,
49- )
50- continue
51-
5238 # (0) If key has no resources => remove from registry and continue
5339 resources = await registry .get_resources (dead_session )
5440 if not resources :
5541 await registry .remove_key (dead_session )
5642 continue
5743
58- # (1,2) CAREFULLY releasing every resource acquired by the expired key
59- _logger .info (
60- "%s expired. Checking resources to cleanup" ,
61- f"{ dead_session = } " ,
62- )
63-
6444 for resource_name , resource_value in resources .items ():
65- # Releasing a resource consists of two steps
66- # - (1) release actual resource (e.g. stop service, close project, deallocate memory, etc)
67- # - (2) remove resource field entry in expired key registry after (1) is completed.
68-
69- # collects a list of keys for (2)
70- keys_to_update = [
71- dead_session ,
72- ]
73-
74- # (1) releasing acquired resources
45+ # (1) releasing acquired resources (currently only projects),
46+ # that means closing project for the disconnected user
7547 _logger .info (
7648 "(1) Releasing resource %s:%s acquired by expired %s" ,
7749 f"{ resource_name = } " ,
@@ -82,46 +54,17 @@ async def remove_disconnected_user_resources(
8254 if resource_name == "project_id" :
8355 # inform that the project can be closed on the backend side
8456 #
85- try :
86- _logger .info (
87- "Closing project '%s' of user %s" , resource_value , user_id
88- )
57+ project_id = TypeAdapter (ProjectID ).validate_python (resource_value )
58+ with log_catch (_logger , reraise = False ), log_context (
59+ _logger ,
60+ logging .INFO ,
61+ "Closing project {project_id} for user {user_id=}" ,
62+ ):
8963 await _projects_service .close_project_for_user (
90- user_id ,
91- TypeAdapter ( ProjectID ). validate_python ( f" { resource_value } " ) ,
92- dead_session ["client_session_id" ],
93- app ,
64+ user_id = user_id ,
65+ project_uuid = project_id ,
66+ client_session_id = dead_session ["client_session_id" ],
67+ app = app ,
9468 simcore_user_agent = UNDEFINED_DEFAULT_SIMCORE_USER_AGENT_VALUE ,
9569 wait_for_service_closed = True ,
9670 )
97-
98- except (ProjectNotFoundError , ProjectLockError ) as err :
99- _logger .warning (
100- (
101- "Could not remove project interactive services user_id=%s "
102- "project_uuid=%s. Check the logs above for details [%s]"
103- ),
104- user_id ,
105- resource_value ,
106- err ,
107- )
108-
109- # (2) remove resource field in collected keys since (1) is completed
110- _logger .info (
111- "(2) Removing field for released resource %s:%s from registry keys: %s" ,
112- f"{ resource_name = } " ,
113- f"{ resource_value = } " ,
114- keys_to_update ,
115- )
116- await logged_gather (
117- * [
118- registry .remove_resource (key , resource_name )
119- for key in keys_to_update
120- ],
121- reraise = False ,
122- )
123-
124- # NOTE:
125- # - if releasing a resource (1) fails, the resource is not removed from the registry and it allows GC to try in next round
126- # - if any task in (2) fails, GC will clean them up in next round as well
127- # - if all resource fields are removed from a key, next GC iteration will remove the key (see (0))
0 commit comments