Skip to content

Commit 2cf573a

Browse files
committed
Merge branch 'add-socketio-events-for-conversations' of github.com:giancarloromeo/osparc-simcore into feature/listen-to-conv-ws
2 parents 532ccf4 + 525d1f0 commit 2cf573a

File tree

8 files changed

+284
-37
lines changed

8 files changed

+284
-37
lines changed

packages/models-library/src/models_library/conversations.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,20 @@
11
from datetime import datetime
22
from enum import auto
3-
from typing import TypeAlias
3+
from typing import Annotated, TypeAlias
44
from uuid import UUID
55

66
from models_library.groups import GroupID
77
from models_library.projects import ProjectID
8-
from pydantic import BaseModel, ConfigDict
8+
from pydantic import BaseModel, ConfigDict, StringConstraints
99

1010
from .products import ProductName
1111
from .utils.enums import StrAutoEnum
1212

1313
ConversationID: TypeAlias = UUID
14+
ConversationName: TypeAlias = Annotated[
15+
str, StringConstraints(strip_whitespace=True, min_length=1, max_length=255)
16+
]
17+
1418
ConversationMessageID: TypeAlias = UUID
1519

1620

@@ -36,7 +40,7 @@ class ConversationMessageType(StrAutoEnum):
3640
class ConversationGetDB(BaseModel):
3741
conversation_id: ConversationID
3842
product_name: ProductName
39-
name: str
43+
name: ConversationName
4044
project_uuid: ProjectID | None
4145
user_group_id: GroupID
4246
type: ConversationType
@@ -63,7 +67,7 @@ class ConversationMessageGetDB(BaseModel):
6367

6468

6569
class ConversationPatchDB(BaseModel):
66-
name: str | None = None
70+
name: ConversationName | None = None
6771

6872

6973
class ConversationMessagePatchDB(BaseModel):

packages/pytest-simcore/src/pytest_simcore/simcore_services.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
"whoami",
4141
"sto-worker",
4242
"sto-worker-cpu-bound",
43+
"traefik-configuration-placeholder",
4344
}
4445
# TODO: unify healthcheck policies see https://github.com/ITISFoundation/osparc-simcore/pull/2281
4546
DEFAULT_SERVICE_HEALTHCHECK_ENTRYPOINT: Final[str] = "/v0/"

services/docker-compose.yml

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ services:
5757
- traefik.http.services.${SWARM_STACK_NAME}_api-server.loadbalancer.healthcheck.path=/
5858
- traefik.http.services.${SWARM_STACK_NAME}_api-server.loadbalancer.healthcheck.interval=2000ms
5959
- traefik.http.services.${SWARM_STACK_NAME}_api-server.loadbalancer.healthcheck.timeout=1000ms
60+
# NOTE: keep in sync with fallback router (rule and entrypoint)
6061
- traefik.http.routers.${SWARM_STACK_NAME}_api-server.rule=(Path(`/`) || Path(`/v0`) || PathPrefix(`/v0/`) || Path(`/api/v0/openapi.json`))
6162
- traefik.http.routers.${SWARM_STACK_NAME}_api-server.entrypoints=simcore_api
6263
- traefik.http.routers.${SWARM_STACK_NAME}_api-server.priority=3
@@ -628,6 +629,7 @@ services:
628629
- traefik.http.services.${SWARM_STACK_NAME}_static_webserver.loadbalancer.healthcheck.interval=2000ms
629630
- traefik.http.services.${SWARM_STACK_NAME}_static_webserver.loadbalancer.healthcheck.timeout=1000ms
630631
- traefik.http.middlewares.${SWARM_STACK_NAME}_static_webserver_retry.retry.attempts=2
632+
# NOTE: keep in sync with fallback router (rule and entrypoint)
631633
- traefik.http.routers.${SWARM_STACK_NAME}_static_webserver.rule=(Path(`/osparc`) || Path(`/s4l`) || Path(`/s4llite`) || Path(`/s4lacad`) || Path(`/s4lengine`) || Path(`/s4ldesktop`) || Path(`/s4ldesktopacad`) || Path(`/tis`) || Path(`/tiplite`) || Path(`/transpiled`) || Path(`/resource`) || PathPrefix(`/osparc/`) || PathPrefix(`/s4l/`) || PathPrefix(`/s4llite/`) || PathPrefix(`/s4lacad/`) || PathPrefix(`/s4lengine/`) || PathPrefix(`/s4ldesktop/`) || PathPrefix(`/s4ldesktopacad/`) || PathPrefix(`/tis/`) || PathPrefix(`/tiplite/`) || PathPrefix(`/transpiled/`) || PathPrefix(`/resource/`))
632634
- traefik.http.routers.${SWARM_STACK_NAME}_static_webserver.service=${SWARM_STACK_NAME}_static_webserver
633635
- traefik.http.routers.${SWARM_STACK_NAME}_static_webserver.entrypoints=http
@@ -871,6 +873,7 @@ services:
871873
# NOTE: stickyness must remain only for specific endpoints, see https://github.com/ITISFoundation/osparc-simcore/pull/4180
872874
- traefik.http.middlewares.${SWARM_STACK_NAME}_webserver_retry.retry.attempts=2
873875
- traefik.http.routers.${SWARM_STACK_NAME}_webserver.service=${SWARM_STACK_NAME}_webserver
876+
# NOTE: keep in sync with fallback router (rule and entrypoint)
874877
- traefik.http.routers.${SWARM_STACK_NAME}_webserver.rule=(Path(`/`) || Path(`/v0`) || Path(`/socket.io/`) || Path(`/static-frontend-data.json`) || PathRegexp(`^/study/(?P<study_uuid>\b[0-9a-f]{8}\b-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-\b[0-9a-f]{12}\b)`) || Path(`/view`) || Path(`/#/view`) || Path(`/#/error`) || PathPrefix(`/v0/`))
875878
- traefik.http.routers.${SWARM_STACK_NAME}_webserver.entrypoints=http
876879
- traefik.http.routers.${SWARM_STACK_NAME}_webserver.priority=6
@@ -1456,6 +1459,62 @@ services:
14561459
- default
14571460
- interactive_services_subnet # for legacy dynamic services
14581461

1462+
# use to define fallback routes for simcore services
1463+
# if docker healthcheck fails, container's traefik configuration is removed
1464+
# leading to 404 https://github.com/traefik/traefik/issues/7842
1465+
#
1466+
# use fallback routes to return proper 503 (instead of 404)
1467+
# this service must be running at all times
1468+
traefik-configuration-placeholder:
1469+
image: busybox:1.35.0
1470+
command: sleep infinity
1471+
networks:
1472+
- default
1473+
deploy:
1474+
labels:
1475+
# route to internal traefik
1476+
- traefik.enable=true
1477+
- io.simcore.zone=${TRAEFIK_SIMCORE_ZONE}
1478+
1479+
### Fallback for api-server
1480+
- traefik.http.routers.${SWARM_STACK_NAME}_api-server_fallback.rule=(Path(`/`) || Path(`/v0`) || PathPrefix(`/v0/`) || Path(`/api/v0/openapi.json`))
1481+
- traefik.http.routers.${SWARM_STACK_NAME}_api-server_fallback.service=${SWARM_STACK_NAME}_api-server_fallback
1482+
- traefik.http.routers.${SWARM_STACK_NAME}_api-server_fallback.entrypoints=simcore_api
1483+
- traefik.http.routers.${SWARM_STACK_NAME}_api-server_fallback.priority=1
1484+
# always fail and return 503 via unhealthy loadbalancer healthcheck
1485+
- traefik.http.services.${SWARM_STACK_NAME}_api-server_fallback.loadbalancer.server.port=0 # port is required (otherwise traefik service is not created)
1486+
- traefik.http.services.${SWARM_STACK_NAME}_api-server_fallback.loadbalancer.healthcheck.path=/some/invalid/path/to/generate/a/503
1487+
- traefik.http.services.${SWARM_STACK_NAME}_api-server_fallback.loadbalancer.healthcheck.interval=10s
1488+
- traefik.http.services.${SWARM_STACK_NAME}_api-server_fallback.loadbalancer.healthcheck.timeout=1ms
1489+
1490+
### Fallback for webserver
1491+
- traefik.http.routers.${SWARM_STACK_NAME}_webserver_fallback.service=${SWARM_STACK_NAME}_webserver_fallback
1492+
- traefik.http.routers.${SWARM_STACK_NAME}_webserver_fallback.rule=(Path(`/`) || Path(`/v0`) || Path(`/socket.io/`) || Path(`/static-frontend-data.json`) || PathRegexp(`^/study/(?P<study_uuid>\b[0-9a-f]{8}\b-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-\b[0-9a-f]{12}\b)`) || Path(`/view`) || Path(`/#/view`) || Path(`/#/error`) || PathPrefix(`/v0/`))
1493+
- traefik.http.routers.${SWARM_STACK_NAME}_webserver_fallback.entrypoints=http
1494+
- traefik.http.routers.${SWARM_STACK_NAME}_webserver_fallback.priority=1
1495+
# always fail and return 503 via unhealthy loadbalancer healthcheck
1496+
- traefik.http.services.${SWARM_STACK_NAME}_webserver_fallback.loadbalancer.server.port=0
1497+
- traefik.http.services.${SWARM_STACK_NAME}_webserver_fallback.loadbalancer.healthcheck.path=/v0/
1498+
- traefik.http.services.${SWARM_STACK_NAME}_webserver_fallback.loadbalancer.healthcheck.interval=10s
1499+
- traefik.http.services.${SWARM_STACK_NAME}_webserver_fallback.loadbalancer.healthcheck.timeout=1ms
1500+
1501+
### Fallback for static-webserver
1502+
- traefik.http.routers.${SWARM_STACK_NAME}_static_webserver_fallback.rule=(Path(`/osparc`) || Path(`/s4l`) || Path(`/s4llite`) || Path(`/s4lacad`) || Path(`/s4lengine`) || Path(`/s4ldesktop`) || Path(`/s4ldesktopacad`) || Path(`/tis`) || Path(`/tiplite`) || Path(`/transpiled`) || Path(`/resource`) || PathPrefix(`/osparc/`) || PathPrefix(`/s4l/`) || PathPrefix(`/s4llite/`) || PathPrefix(`/s4lacad/`) || PathPrefix(`/s4lengine/`) || PathPrefix(`/s4ldesktop/`) || PathPrefix(`/s4ldesktopacad/`) || PathPrefix(`/tis/`) || PathPrefix(`/tiplite/`) || PathPrefix(`/transpiled/`) || PathPrefix(`/resource/`))
1503+
- traefik.http.routers.${SWARM_STACK_NAME}_static_webserver_fallback.service=${SWARM_STACK_NAME}_static_webserver_fallback
1504+
- traefik.http.routers.${SWARM_STACK_NAME}_static_webserver_fallback.entrypoints=http
1505+
- traefik.http.routers.${SWARM_STACK_NAME}_static_webserver_fallback.priority=1
1506+
# always fail and return 503 via unhealthy loadbalancer healthcheck
1507+
- traefik.http.services.${SWARM_STACK_NAME}_static_webserver_fallback.loadbalancer.server.port=0
1508+
- traefik.http.services.${SWARM_STACK_NAME}_static_webserver_fallback.loadbalancer.healthcheck.path=/some/invalid/path/to/generate/a/503
1509+
- traefik.http.services.${SWARM_STACK_NAME}_static_webserver_fallback.loadbalancer.healthcheck.interval=10s
1510+
- traefik.http.services.${SWARM_STACK_NAME}_static_webserver_fallback.loadbalancer.healthcheck.timeout=1ms
1511+
healthcheck:
1512+
test: command -v sleep
1513+
interval: 10s
1514+
timeout: 1s
1515+
start_period: 1s
1516+
retries: 3
1517+
14591518
volumes:
14601519
postgres_data:
14611520
name: ${SWARM_STACK_NAME}_postgres_data

services/web/server/src/simcore_service_webserver/conversations/_conversation_message_service.py

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,10 @@
1616
from models_library.rest_pagination import PageTotalCount
1717
from models_library.users import UserID
1818

19-
from ..projects._groups_repository import list_project_groups
20-
from ..users._users_service import get_users_in_group
21-
2219
# Import or define SocketMessageDict
2320
from ..users.api import get_user_primary_group_id
2421
from . import _conversation_message_repository
22+
from ._conversation_service import _get_recipients
2523
from ._socketio import (
2624
notify_conversation_message_created,
2725
notify_conversation_message_deleted,
@@ -31,16 +29,6 @@
3129
_logger = logging.getLogger(__name__)
3230

3331

34-
async def _get_recipients(app: web.Application, project_id: ProjectID) -> set[UserID]:
35-
groups = await list_project_groups(app, project_id=project_id)
36-
return {
37-
user
38-
for group in groups
39-
if group.read
40-
for user in await get_users_in_group(app, gid=group.gid)
41-
}
42-
43-
4432
async def create_message(
4533
app: web.Application,
4634
*,

services/web/server/src/simcore_service_webserver/conversations/_conversation_service.py

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,29 @@
1616
from models_library.rest_pagination import PageTotalCount
1717
from models_library.users import UserID
1818

19+
from ..conversations._socketio import (
20+
notify_conversation_created,
21+
notify_conversation_deleted,
22+
notify_conversation_updated,
23+
)
24+
from ..projects._groups_repository import list_project_groups
25+
from ..users._users_service import get_users_in_group
1926
from ..users.api import get_user_primary_group_id
2027
from . import _conversation_repository
2128

2229
_logger = logging.getLogger(__name__)
2330

2431

32+
async def _get_recipients(app: web.Application, project_id: ProjectID) -> set[UserID]:
33+
groups = await list_project_groups(app, project_id=project_id)
34+
return {
35+
user
36+
for group in groups
37+
if group.read
38+
for user in await get_users_in_group(app, gid=group.gid)
39+
}
40+
41+
2542
async def create_conversation(
2643
app: web.Application,
2744
*,
@@ -37,7 +54,7 @@ async def create_conversation(
3754

3855
_user_group_id = await get_user_primary_group_id(app, user_id=user_id)
3956

40-
return await _conversation_repository.create(
57+
created_conversation = await _conversation_repository.create(
4158
app,
4259
name=name,
4360
project_uuid=project_uuid,
@@ -46,6 +63,15 @@ async def create_conversation(
4663
product_name=product_name,
4764
)
4865

66+
await notify_conversation_created(
67+
app,
68+
recipients=await _get_recipients(app, project_uuid),
69+
project_id=project_uuid,
70+
conversation=created_conversation,
71+
)
72+
73+
return created_conversation
74+
4975

5076
async def get_conversation(
5177
app: web.Application,
@@ -61,27 +87,51 @@ async def get_conversation(
6187
async def update_conversation(
6288
app: web.Application,
6389
*,
90+
project_id: ProjectID,
6491
conversation_id: ConversationID,
6592
# Update attributes
6693
updates: ConversationPatchDB,
6794
) -> ConversationGetDB:
68-
return await _conversation_repository.update(
95+
updated_conversation = await _conversation_repository.update(
6996
app,
7097
conversation_id=conversation_id,
7198
updates=updates,
7299
)
73100

101+
await notify_conversation_updated(
102+
app,
103+
recipients=await _get_recipients(app, project_id),
104+
project_id=project_id,
105+
conversation=updated_conversation,
106+
)
107+
108+
return updated_conversation
109+
74110

75111
async def delete_conversation(
76112
app: web.Application,
77113
*,
114+
product_name: ProductName,
115+
project_id: ProjectID,
116+
user_id: UserID,
78117
conversation_id: ConversationID,
79118
) -> None:
80119
await _conversation_repository.delete(
81120
app,
82121
conversation_id=conversation_id,
83122
)
84123

124+
_user_group_id = await get_user_primary_group_id(app, user_id=user_id)
125+
126+
await notify_conversation_deleted(
127+
app,
128+
recipients=await _get_recipients(app, project_id),
129+
product_name=product_name,
130+
user_group_id=_user_group_id,
131+
project_id=project_id,
132+
conversation_id=conversation_id,
133+
)
134+
85135

86136
async def list_conversations_for_project(
87137
app: web.Application,

0 commit comments

Comments
 (0)