mirror of
https://github.com/KeymonSoft/jRDC-Multi.git
synced 2026-04-19 21:59:23 +00:00
- VERSION 5.09.18
- feat(manager): Implementa recarga granular (Hot-Swap). - Actualiza manager.html para solicitar la DB Key a recargar (ej: DB2). - Se modifica Manager.bas para leer este parámetro y ejecutar el Hot-Swap de forma atómica solo en el pool de conexión especificado, lo cual mejora la eficiencia y la disponibilidad del servicio.
This commit is contained in:
187
SSE.bas
Normal file
187
SSE.bas
Normal file
@@ -0,0 +1,187 @@
|
||||
B4J=true
|
||||
Group=Default Group
|
||||
ModulesStructureVersion=1
|
||||
Type=StaticCode
|
||||
Version=10.3
|
||||
@EndOfDesignText@
|
||||
' Módulo para gestionar conexiones y transmisiones de Server-Sent Events (SSE).
|
||||
|
||||
' Declaración de variables globales a nivel de proceso.
|
||||
Sub Process_Globals
|
||||
' 'Connections' es un mapa (diccionario) para almacenar las conexiones SSE activas.
|
||||
' La clave será una combinación del 'path' y un GUID único, y el valor será el OutputStream de la respuesta.
|
||||
' Se usará un 'ThreadSafeMap' para evitar problemas de concurrencia entre hilos.
|
||||
Dim Connections As Map
|
||||
|
||||
|
||||
' Timer #1 ("El Vigilante"): Se encarga de detectar y eliminar conexiones muertas.
|
||||
Private RemoveTimer As Timer
|
||||
|
||||
' Timer #2 ("El Informante"): Se encarga de recolectar y enviar los datos de estadísticas.
|
||||
Dim StatsTimer As Timer
|
||||
Dim const UPDATE_INTERVAL_MS As Long = 2000 ' Intervalo de envío de estadísticas: 2 segundos.
|
||||
End Sub
|
||||
|
||||
' Subrutina de inicialización del módulo. Se llama una vez cuando el objeto es creado.
|
||||
public Sub Initialize()
|
||||
' Crea el mapa 'Connections' como un mapa seguro para hilos (ThreadSafeMap).
|
||||
' Esto es fundamental porque múltiples peticiones (hilos) pueden intentar agregar o remover conexiones simultáneamente.
|
||||
Connections = Main.srvr.CreateThreadSafeMap
|
||||
|
||||
' Inicializa el temporizador 'RemoveTimer' para que dispare el evento "RemoveTimer" cada 5000 milisegundos (5 segundos).
|
||||
RemoveTimer.Initialize("RemoveTimer", 5000)
|
||||
|
||||
' Habilita el temporizador para que comience a funcionar.
|
||||
RemoveTimer.Enabled = True
|
||||
|
||||
|
||||
Log("Stats SSE Handler Initialized (Singleton Mode)")
|
||||
|
||||
' Crea el mapa de conexiones, asegurando que sea seguro para el manejo de múltiples hilos.
|
||||
Connections = Main.srvr.CreateThreadSafeMap
|
||||
|
||||
' Configura y activa el timer para la limpieza de conexiones cada 5 segundos.
|
||||
' NOTA: El EventName "RemoveTimer" debe coincidir con el nombre de la subrutina del tick.
|
||||
RemoveTimer.Initialize("RemoveTimer", 5000)
|
||||
RemoveTimer.Enabled = True
|
||||
|
||||
' Configura y activa el timer para el envío de estadísticas.
|
||||
' NOTA: El EventName "StatsTimer" debe coincidir con el nombre de la subrutina del tick.
|
||||
StatsTimer.Initialize("StatsTimer", UPDATE_INTERVAL_MS)
|
||||
StatsTimer.Enabled = True
|
||||
End Sub
|
||||
|
||||
' Subrutina para agregar un nuevo cliente (target) al stream de eventos SSE.
|
||||
' Se llama cuando un cliente se conecta al endpoint SSE.
|
||||
' Registra formalmente a un nuevo cliente en el sistema.
|
||||
Sub AddTarget(path As String, resp As ServletResponse)
|
||||
' Genera una clave única para esta conexión específica.
|
||||
Dim connectionKey As String = path & "|" & GetGUID
|
||||
Log("--- [SSE] Cliente conectado: " & connectionKey & " ---")
|
||||
|
||||
' Configura las cabeceras HTTP necesarias para que el navegador mantenga la conexión abierta.
|
||||
resp.ContentType = "text/event-stream"
|
||||
resp.SetHeader("Cache-Control", "no-cache")
|
||||
resp.SetHeader("Connection", "keep-alive")
|
||||
resp.CharacterEncoding = "UTF-8"
|
||||
resp.Status = 200
|
||||
|
||||
' Añade al cliente y su canal de comunicación al mapa central.
|
||||
Connections.Put(connectionKey, resp.OutputStream)
|
||||
' Envía un primer mensaje de bienvenida para confirmar la conexión.
|
||||
SendMessage(resp.OutputStream, "open", "Connection established", 0, connectionKey)
|
||||
End Sub
|
||||
|
||||
' Envía un mensaje a todos los clientes suscritos a un "path" específico.
|
||||
Sub Broadcast(Path As String, EventName As String, Message As String, Retry As Long)
|
||||
' Itera sobre la lista de clientes activos.
|
||||
For Each key As String In Connections.Keys
|
||||
' Log(key)
|
||||
' Filtra para enviar solo a los clientes del path correcto (en este caso, "stats").
|
||||
If key.StartsWith(Path & "|") Then
|
||||
Try
|
||||
' Llama a la función de bajo nivel para enviar el mensaje formateado.
|
||||
SendMessage(Connections.Get(key), EventName, Message, Retry, DateTime.Now)
|
||||
Catch
|
||||
' Si el envío falla, asume que el cliente se desconectó y lo elimina.
|
||||
Log("######################")
|
||||
Log("## Removing (broadcast failed): " & key)
|
||||
Log("######################")
|
||||
Connections.Remove(key)
|
||||
End Try
|
||||
End If
|
||||
Next
|
||||
End Sub
|
||||
|
||||
' Formatea y envía un único mensaje SSE a un cliente específico.
|
||||
Sub SendMessage(out As OutputStream, eventName As String, message As String, retry As Int, id As String)
|
||||
' Construye el mensaje siguiendo el formato oficial del protocolo SSE.
|
||||
Dim sb As StringBuilder
|
||||
sb.Initialize
|
||||
sb.Append("id: " & id).Append(CRLF)
|
||||
sb.Append("event: " & eventName).Append(CRLF)
|
||||
If message <> "" Then
|
||||
sb.Append("data: " & message).Append(CRLF)
|
||||
End If
|
||||
If retry > 0 Then
|
||||
sb.Append("retry: " & retry).Append(CRLF)
|
||||
End If
|
||||
sb.Append(CRLF) ' El doble salto de línea final es obligatorio.
|
||||
|
||||
' Convierte el texto a bytes y lo escribe en el canal de comunicación del cliente.
|
||||
Dim Bytes() As Byte = sb.ToString.GetBytes("UTF-8")
|
||||
out.WriteBytes(Bytes, 0, Bytes.Length)
|
||||
out.Flush ' Fuerza el envío inmediato de los datos.
|
||||
End Sub
|
||||
|
||||
' Genera un Identificador Único Global (GUID) para cada conexión.
|
||||
Private Sub GetGUID() As String
|
||||
Dim jo As JavaObject
|
||||
Return jo.InitializeStatic("java.util.UUID").RunMethod("randomUUID", Null)
|
||||
End Sub
|
||||
|
||||
|
||||
' Evento que se dispara cada vez que el 'RemoveTimer' completa su intervalo (cada 5 segundos).
|
||||
' Su propósito es proactivamente limpiar conexiones muertas.
|
||||
Sub RemoveTimer_Tick
|
||||
' Log("remove timer")
|
||||
' Itera sobre todas las conexiones activas.
|
||||
For Each key As String In Connections.Keys
|
||||
' Intenta enviar un mensaje de prueba ("ping" o "heartbeat") a cada cliente.
|
||||
Try
|
||||
' Obtiene el OutputStream del cliente.
|
||||
Dim out As OutputStream = Connections.Get(key)
|
||||
' Envía un evento de tipo "Test" sin datos. Si la conexión está viva, esto no hará nada visible.
|
||||
SendMessage(out, "Test", "", 0, "")
|
||||
Catch
|
||||
' Si el 'SendMessage' falla, significa que el socket está cerrado (el cliente se desconectó).
|
||||
' Registra en el log que se está eliminando una conexión muerta.
|
||||
Log("######################")
|
||||
Log("## Removing (timer): " & key)
|
||||
Log("######################")
|
||||
' Elimina la conexión del mapa para liberar recursos.
|
||||
Connections.Remove(key)
|
||||
End Try
|
||||
Next
|
||||
End Sub
|
||||
|
||||
' Evento del Timer #2 ("El Informante"): se dispara cada 2 segundos.
|
||||
public Sub StatsTimer_Tick
|
||||
' Optimización: si no hay nadie conectado, no realiza el trabajo pesado.
|
||||
' Log($"Conexiones: ${Connections.Size}"$)
|
||||
If Connections.Size = 0 Then Return
|
||||
|
||||
Try
|
||||
' Prepara un mapa para almacenar las estadísticas recolectadas.
|
||||
Dim allPoolStats As Map
|
||||
allPoolStats.Initialize
|
||||
|
||||
' Bloquea el acceso a los conectores para leer sus datos de forma segura.
|
||||
Main.MainConnectorsLock.RunMethod("lock", Null)
|
||||
For Each dbKey As String In Main.listaDeCP
|
||||
Dim connector As RDCConnector
|
||||
If Main.Connectors.ContainsKey(dbKey) Then
|
||||
connector = Main.Connectors.Get(dbKey)
|
||||
If connector.IsInitialized Then
|
||||
allPoolStats.Put(dbKey, connector.GetPoolStats)
|
||||
Else
|
||||
allPoolStats.Put(dbKey, CreateMap("Error": "Conector no inicializado"))
|
||||
End If
|
||||
End If
|
||||
Next
|
||||
' Libera el bloqueo para que otras partes del programa puedan usar los conectores.
|
||||
Main.MainConnectorsLock.RunMethod("unlock", Null)
|
||||
|
||||
' Convierte el mapa de estadísticas a un formato de texto JSON.
|
||||
Dim j As JSONGenerator
|
||||
j.Initialize(allPoolStats)
|
||||
Dim jsonStats As String = j.ToString
|
||||
|
||||
' Llama al "locutor" para enviar el JSON a todos los clientes conectados.
|
||||
Broadcast("stats", "stats_update", jsonStats, 0)
|
||||
|
||||
Catch
|
||||
' Captura y registra cualquier error que ocurra durante la recolección de datos.
|
||||
Log($"[SSE] Error CRÍTICO durante la adquisición de estadísticas: ${LastException.Message}"$)
|
||||
End Try
|
||||
End Sub
|
||||
Reference in New Issue
Block a user