CreaRack-SL

org_connections · cerrar las conexiones en vivo de una organización suspendida o borrada

Propósito

Punto único que cierra, justo después del commit, las conexiones WebSocket que ya estaban abiertas de una organización que se acaba de suspender o borrar. Antes de este cambio (mega-auditoría B-31), suspender una organización impedía abrir conexiones NUEVAS, pero las que ya estaban vivas seguían funcionando hasta la siguiente reconexión: un Agente seguía mandando medidas y un navegador seguía recibiendo el canal en vivo de una organización ya suspendida.

Contrato

close_organization_connections(org_ids) -> None

Programa el aviso con transaction.on_commit (para no avisar antes de que el cambio esté realmente guardado) y, para cada id, manda un mensaje {"type": "organization_suspended"} por el channel layer a dos grupos:

  • agent_targets_<id> — los Agentes de esa organización. FleetLifecycleMixin.organization_suspended cierra con el código 4005, sin relevo de Primary; el Agente reintenta solo y connect lo rechaza mientras la organización siga suspendida.
  • monitoring_<id> — los navegadores conectados al panel de monitorización. MonitoringConsumer.organization_suspended corta la conexión (el propio connect del consumer ya rechaza a quien intente entrar a una organización suspendida).

Un fallo al avisar a un grupo concreto (excepción del channel layer) se registra como aviso en el log y no interrumpe el resto — la organización ya quedó guardada como suspendida, así que no hay nada que deshacer.

Dependencias

Lo llaman:

  • core/signals.py::close_connections_on_suspension — post_save de Organization, solo cuando pasa a inactiva o borrada (mira los campos is_active/deleted_at en update_fields para no avisar en un guardado que no toca el estado, como el que hace el webhook de Stripe).
  • core/signals.py::close_connections_on_delete — post_delete de Organization.
  • Las acciones masivas del admin (Disable, Soft-delete), que usan queryset.update() y por eso NO disparan señales — llaman al servicio directamente.

Usa:

  • channels.layers.get_channel_layer() + asgiref.sync.async_to_sync para mandar el mensaje al grupo desde código síncrono (señales, admin).
  • Los consumers FleetLifecycleMixin (terminal) y MonitoringConsumer (monitoring), que son quienes de verdad cierran el socket al recibir el mensaje.

Ejemplo de uso

from core.services.org_connections import close_organization_connections

close_organization_connections([org.pk])  # tras guardar o borrar la organización

Límite conocido

Si el channel layer no logra alcanzar a un consumer que vive en OTRO proceso Daphne, ese aviso se pierde en silencio (solo queda un warning en el log, sin reintento) — la conexión sigue viva hasta su próximo evento natural (ping, reconexión). No hay test de ese escenario concreto.

Véase también

  • [[entity—core—model—organization]]
  • [[concept—general—como-esta-implementado-el-aislamiento-multi-tenant]]