diff --git a/src/main/kotlin/org/jitsi/jicofo/bridge/ExternalBridgeSelectionStrategy.kt b/src/main/kotlin/org/jitsi/jicofo/bridge/ExternalBridgeSelectionStrategy.kt new file mode 100644 index 0000000000..54717c9562 --- /dev/null +++ b/src/main/kotlin/org/jitsi/jicofo/bridge/ExternalBridgeSelectionStrategy.kt @@ -0,0 +1,110 @@ +package org.jitsi.jicofo.bridge + +import org.jitsi.utils.logging2.Logger +import org.jitsi.utils.logging2.LoggerImpl +import org.json.simple.JSONObject +import org.json.simple.JSONValue +import java.net.URI +import java.net.http.HttpClient +import java.net.http.HttpRequest +import java.net.http.HttpResponse + +@Suppress("unused") +class ExternalBridgeSelectionStrategy() : BridgeSelectionStrategy() { + private val httpClient: HttpClient = HttpClient + .newBuilder() + .connectTimeout(ExternalBridgeSelectionStrategyConfig.config.timeout) + .build() + + private val logger: Logger = LoggerImpl(ExternalBridgeSelectionStrategy::class.simpleName) + + private val fallbackStrategy: BridgeSelectionStrategy? by lazy { + val fallbackStrategyName = ExternalBridgeSelectionStrategyConfig.config.fallbackStrategy ?: return@lazy null + try { + val clazz = Class.forName("${javaClass.getPackage().name}.$fallbackStrategyName") + clazz.getConstructor().newInstance() as BridgeSelectionStrategy + } catch (e: Exception) { + val clazz = Class.forName(fallbackStrategyName) + clazz.getConstructor().newInstance() as BridgeSelectionStrategy + } + } + + private fun fallback( + bridges: MutableList?, + conferenceBridges: MutableMap?, + participantRegion: String? + ): Bridge { + if (fallbackStrategy == null) { + throw Exception("External bridge selection failed and no fallbackStrategy was provided.") + } + return fallbackStrategy!!.doSelect(bridges, conferenceBridges, participantRegion) + } + + override fun doSelect( + bridges: MutableList?, + conferenceBridges: MutableMap?, + participantRegion: String? + ): Bridge { + val url = ExternalBridgeSelectionStrategyConfig.config.url + ?: throw Exception("ExternalBridgeSelectionStrategy requires url to be provided") + + val requestBody = JSONObject() + requestBody["bridges"] = bridges?.map { + mapOf( + "jid" to it.jid.toString(), + "version" to it.version, + "colibri2" to it.supportsColibri2(), + "relay_id" to it.relayId, + "region" to it.region, + "stress" to it.stress, + "operational" to it.isOperational, + "graceful_shutdown" to it.isInGracefulShutdown, + "draining" to it.isDraining, + ) + } + requestBody["conference_bridges"] = conferenceBridges?.mapKeys { it.key.jid.toString() } + requestBody["participant_region"] = participantRegion + requestBody["fallback_strategy"] = ExternalBridgeSelectionStrategyConfig.config.fallbackStrategy + + val request = HttpRequest + .newBuilder() + .POST(HttpRequest.BodyPublishers.ofString(requestBody.toJSONString())) + .uri(URI.create(url)) + .headers("Content-Type", "application/json") + .timeout(ExternalBridgeSelectionStrategyConfig.config.timeout) + .build() + + val response: HttpResponse + try { + response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()) + } catch (exc: Exception) { + logger.error("ExternalBridgeSelectionStrategy: HTTP request failed with ${exc}, using fallback strategy") + return fallback(bridges, conferenceBridges, participantRegion) + } + + val statusCode = response.statusCode() + if (statusCode !in 200..299) { + logger.error("ExternalBridgeSelectionStrategy: HTTP request failed with ${statusCode}, using fallback strategy") + return fallback(bridges, conferenceBridges, participantRegion) + } + + val responseBody: JSONObject + try { + responseBody = JSONValue.parseWithException(response.body()) as JSONObject + } catch (exc: Exception) { + logger.error("ExternalBridgeSelectionStrategy: HTTP response parsing failed with ${exc}, using fallback strategy") + return fallback(bridges, conferenceBridges, participantRegion) + } + + val selectedBridgeIndex = responseBody["selected_bridge_index"] as? Int + + if (selectedBridgeIndex == null) { + logger.error("ExternalBridgeSelectionStrategy: HTTP response selectedBridgeIndex missing or invalid, using fallback strategy") + return fallback(bridges, conferenceBridges, participantRegion) + } + + val bridge = bridges!![selectedBridgeIndex] + logger.info("ExternalBridgeSelectionStrategy: participantRegion=${participantRegion}, bridge=${bridge}") + return bridge + } +} \ No newline at end of file diff --git a/src/main/kotlin/org/jitsi/jicofo/bridge/ExternalBridgeSelectionStrategyConfig.kt b/src/main/kotlin/org/jitsi/jicofo/bridge/ExternalBridgeSelectionStrategyConfig.kt new file mode 100644 index 0000000000..f824ed6541 --- /dev/null +++ b/src/main/kotlin/org/jitsi/jicofo/bridge/ExternalBridgeSelectionStrategyConfig.kt @@ -0,0 +1,29 @@ +package org.jitsi.jicofo.bridge + +import org.jitsi.config.JitsiConfig +import org.jitsi.metaconfig.optionalconfig +import java.time.Duration + +class ExternalBridgeSelectionStrategyConfig private constructor() { + val url: String? by optionalconfig { + "${BASE}.url".from(JitsiConfig.newConfig) + } + fun url() = url + + val timeout: Duration? by optionalconfig { + "${BASE}.timeout".from(JitsiConfig.newConfig) + } + fun timeout() = timeout + + val fallbackStrategy: String? by optionalconfig { + "${BASE}.fallback-strategy".from(JitsiConfig.newConfig) + } + fun fallbackStrategy() = fallbackStrategy + + companion object { + const val BASE = "jicofo.bridge.external-selection-strategy" + + @JvmField + val config = ExternalBridgeSelectionStrategyConfig() + } +} \ No newline at end of file