| 
15 | 15 | from models_library.rest_ordering import OrderBy, OrderDirection  | 
16 | 16 | from models_library.rest_pagination import PageTotalCount  | 
17 | 17 | from models_library.users import UserID  | 
 | 18 | +from servicelib.redis import exclusive  | 
18 | 19 | 
 
  | 
19 |  | -# Import or define SocketMessageDict  | 
 | 20 | +from ..redis import get_redis_lock_manager_client_sdk  | 
20 | 21 | from ..users import users_service  | 
21 | 22 | from . import _conversation_message_repository  | 
22 | 23 | from ._conversation_service import _get_recipients  | 
 | 
28 | 29 | 
 
  | 
29 | 30 | _logger = logging.getLogger(__name__)  | 
30 | 31 | 
 
  | 
 | 32 | +# Redis lock key for conversation message operations  | 
 | 33 | +CONVERSATION_MESSAGE_REDIS_LOCK_KEY = "conversation_message_update:{}"  | 
 | 34 | + | 
31 | 35 | 
 
  | 
32 | 36 | async def create_message(  | 
33 | 37 |     app: web.Application,  | 
@@ -70,30 +74,61 @@ async def create_support_message_and_check_if_it_is_first_message(  | 
70 | 74 |     content: str,  | 
71 | 75 |     type_: ConversationMessageType,  | 
72 | 76 | ) -> tuple[ConversationMessageGetDB, bool]:  | 
73 |  | -    created_message = await create_message(  | 
74 |  | -        app,  | 
75 |  | -        user_id=user_id,  | 
76 |  | -        project_id=project_id,  | 
77 |  | -        conversation_id=conversation_id,  | 
78 |  | -        content=content,  | 
79 |  | -        type_=type_,  | 
80 |  | -    )  | 
81 |  | -    _, messages = await _conversation_message_repository.list_(  | 
82 |  | -        app,  | 
83 |  | -        conversation_id=conversation_id,  | 
84 |  | -        offset=0,  | 
85 |  | -        limit=1,  | 
86 |  | -        order_by=OrderBy(  | 
87 |  | -            field=IDStr("created"), direction=OrderDirection.ASC  | 
88 |  | -        ),  # NOTE: ASC - first/oldest message first  | 
 | 77 | +    """Create a message and check if it's the first one with Redis lock protection.  | 
 | 78 | +
  | 
 | 79 | +    This function is protected by Redis exclusive lock because:  | 
 | 80 | +    - the message creation and first message check must be kept in sync  | 
 | 81 | +
  | 
 | 82 | +    Args:  | 
 | 83 | +        app: The web application instance  | 
 | 84 | +        user_id: ID of the user creating the message  | 
 | 85 | +        project_id: ID of the project (optional)  | 
 | 86 | +        conversation_id: ID of the conversation  | 
 | 87 | +        content: Content of the message  | 
 | 88 | +        type_: Type of the message  | 
 | 89 | +
  | 
 | 90 | +    Returns:  | 
 | 91 | +        Tuple containing the created message and whether it's the first message  | 
 | 92 | +    """  | 
 | 93 | + | 
 | 94 | +    @exclusive(  | 
 | 95 | +        get_redis_lock_manager_client_sdk(app),  | 
 | 96 | +        lock_key=CONVERSATION_MESSAGE_REDIS_LOCK_KEY.format(conversation_id),  | 
 | 97 | +        blocking=True,  | 
 | 98 | +        blocking_timeout=None,  # NOTE: this is a blocking call, a timeout has undefined effects  | 
89 | 99 |     )  | 
 | 100 | +    async def _create_support_message_and_check_if_it_is_first_message() -> (  | 
 | 101 | +        tuple[ConversationMessageGetDB, bool]  | 
 | 102 | +    ):  | 
 | 103 | +        """This function is protected because  | 
 | 104 | +        - the message creation and first message check must be kept in sync  | 
 | 105 | +        """  | 
 | 106 | +        created_message = await create_message(  | 
 | 107 | +            app,  | 
 | 108 | +            user_id=user_id,  | 
 | 109 | +            project_id=project_id,  | 
 | 110 | +            conversation_id=conversation_id,  | 
 | 111 | +            content=content,  | 
 | 112 | +            type_=type_,  | 
 | 113 | +        )  | 
 | 114 | +        _, messages = await _conversation_message_repository.list_(  | 
 | 115 | +            app,  | 
 | 116 | +            conversation_id=conversation_id,  | 
 | 117 | +            offset=0,  | 
 | 118 | +            limit=1,  | 
 | 119 | +            order_by=OrderBy(  | 
 | 120 | +                field=IDStr("created"), direction=OrderDirection.ASC  | 
 | 121 | +            ),  # NOTE: ASC - first/oldest message first  | 
 | 122 | +        )  | 
 | 123 | + | 
 | 124 | +        is_first_message = False  | 
 | 125 | +        if messages:  | 
 | 126 | +            first_message = messages[0]  | 
 | 127 | +            is_first_message = first_message.message_id == created_message.message_id  | 
90 | 128 | 
 
  | 
91 |  | -    is_first_message = False  | 
92 |  | -    if messages:  | 
93 |  | -        first_message = messages[0]  | 
94 |  | -        is_first_message = first_message.message_id == created_message.message_id  | 
 | 129 | +        return created_message, is_first_message  | 
95 | 130 | 
 
  | 
96 |  | -    return created_message, is_first_message  | 
 | 131 | +    return await _create_support_message_and_check_if_it_is_first_message()  | 
97 | 132 | 
 
  | 
98 | 133 | 
 
  | 
99 | 134 | async def get_message(  | 
 | 
0 commit comments