Skip to content

Commit

Permalink
Handle unreliable non-JSON Xirsys responses
Browse files Browse the repository at this point in the history
Closes #4
  • Loading branch information
Brutus5000 committed Nov 19, 2023
1 parent c6dd231 commit 8b7bc70
Show file tree
Hide file tree
Showing 6 changed files with 128 additions and 81 deletions.
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ val quarkusPlatformArtifactId: String by project
val quarkusPlatformVersion: String by project

dependencies {
implementation("io.quarkus:quarkus-smallrye-fault-tolerance")
implementation(enforcedPlatform("$quarkusPlatformGroupId:$quarkusPlatformArtifactId:$quarkusPlatformVersion"))
implementation("io.quarkus:quarkus-config-yaml")
implementation("io.quarkus:quarkus-scheduler")
Expand All @@ -31,7 +32,6 @@ dependencies {
implementation("io.quarkus:quarkus-smallrye-health")
implementation("io.quarkus:quarkus-rest-client-reactive-jackson")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("io.quarkus:quarkus-container-image-jib")
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
implementation("io.quarkus:quarkus-arc")
implementation("io.quarkus:quarkus-hibernate-orm")
Expand Down
7 changes: 4 additions & 3 deletions gradle.properties
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#Gradle properties
#Sun Nov 19 14:11:53 CET 2023
quarkusPlatformArtifactId=quarkus-bom
quarkusPlatformGroupId=io.quarkus.platform
quarkusPlatformVersion=3.5.0
quarkusPluginId=io.quarkus
quarkusPluginVersion=3.5.0
quarkusPlatformGroupId=io.quarkus.platform
quarkusPlatformArtifactId=quarkus-bom
quarkusPlatformVersion=3.5.0
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package com.faforever.icebreaker.service.xirsys

import com.faforever.icebreaker.config.FafProperties
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import jakarta.inject.Singleton
import org.eclipse.microprofile.faulttolerance.Retry
import org.eclipse.microprofile.rest.client.RestClientBuilder
import java.io.IOException
import java.net.URI

/**
* The Xirsys API does not comply to proper REST standards, thus we need an adapter.
*/
@Singleton
class XirsysApiAdapter(
private val fafProperties: FafProperties,
private val xirsysProperties: XirsysProperties,
private val objectMapper: ObjectMapper,
) {
private val xirsysApiClient: XirsysApiClient = RestClientBuilder.newBuilder()
.baseUri(URI.create(xirsysProperties.baseUrl()))
.register(
BasicAuthenticationRequestFilter(
username = xirsysProperties.ident(),
password = xirsysProperties.secret(),
),
)
.build(XirsysApiClient::class.java)

@Retry
fun listChannel(): List<String> =
parseAndUnwrap {
xirsysApiClient.listChannel(
namespace = xirsysProperties.channelNamespace(),
environment = fafProperties.environment(),
)
}

@Retry
fun createChannel(channelName: String): Map<String, String> =
parseAndUnwrap {
xirsysApiClient.createChannel(
namespace = xirsysProperties.channelNamespace(),
environment = fafProperties.environment(),
channelName = channelName,
)
}

@Retry
fun deleteChannel(channelName: String): Int =
parseAndUnwrap {
xirsysApiClient.deleteChannel(
namespace = xirsysProperties.channelNamespace(),
environment = fafProperties.environment(),
channelName = channelName,
)
}

@Retry
fun requestIceServers(channelName: String, turnRequest: TurnRequest = TurnRequest()): TurnResponse =
parseAndUnwrap {
xirsysApiClient.requestIceServers(
namespace = xirsysProperties.channelNamespace(),
environment = fafProperties.environment(),
channelName = channelName,
turnRequest = turnRequest,
)
}

@Throws(IOException::class)
private inline fun <reified T : Any> parseAndUnwrap(getResponse: () -> String): T {
val response = getResponse()
return try {
when (val result = objectMapper.readValue<XirsysResponse<T>>(response)) {
is XirsysResponse.Error -> throw XirsysSpecifiedApiException(
errorCode = result.code,
message = "Listing sessions failed: ${result.code}",
)

is XirsysResponse.Success -> result.data
}
} catch (e: IOException) {
throw XirsysUnspecifiedApiException(errorResponse = response, cause = e)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,30 +14,30 @@ import org.eclipse.microprofile.rest.client.inject.RegisterRestClient
@ApplicationScoped
@RegisterRestClient
@Consumes(MediaType.APPLICATION_JSON)
interface XirsysClient {
interface XirsysApiClient {
@GET
@Path("/_ns/{namespace}/{environment}")
@ClientQueryParam(name = "depth", value = ["10"])
fun listChannel(
@PathParam("namespace") namespace: String,
@PathParam("environment") environment: String,
): XirsysResponse<List<String>>
): String

@PUT
@Path("/_ns/{namespace}/{environment}/{channelName}")
fun createChannel(
@PathParam("namespace") namespace: String,
@PathParam("environment") environment: String,
@PathParam("channelName") channelName: String,
): XirsysResponse<Map<String, String>>
): String

@DELETE
@Path("/_ns/{namespace}/{environment}/{channelName}")
fun deleteChannel(
@PathParam("namespace") namespace: String,
@PathParam("environment") environment: String,
@PathParam("channelName") channelName: String,
): XirsysResponse<Int>
): String

@PUT
@Path("/_turn/{namespace}/{environment}/{channelName}")
Expand All @@ -46,5 +46,5 @@ interface XirsysClient {
@PathParam("environment") environment: String,
@PathParam("channelName") channelName: String,
turnRequest: TurnRequest,
): XirsysResponse<TurnResponse>
): String
}
Original file line number Diff line number Diff line change
@@ -1,36 +1,23 @@
package com.faforever.icebreaker.service.xirsys

import com.faforever.icebreaker.config.FafProperties
import com.faforever.icebreaker.service.Server
import com.faforever.icebreaker.service.Session
import com.faforever.icebreaker.service.SessionHandler
import jakarta.inject.Singleton
import org.eclipse.microprofile.rest.client.RestClientBuilder
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import java.net.URI

private val LOG: Logger = LoggerFactory.getLogger(XirsysSessionHandler::class.java)

@Singleton
class XirsysSessionHandler(
private val fafProperties: FafProperties,
private val xirsysProperties: XirsysProperties,
xirsysProperties: XirsysProperties,
private val xirsysApiAdapter: XirsysApiAdapter,
) : SessionHandler {
companion object {
const val SERVER_NAME = "xirsys.com"
}

private val xirsysClient: XirsysClient = RestClientBuilder.newBuilder()
.baseUri(URI.create(xirsysProperties.baseUrl()))
.register(
BasicAuthenticationRequestFilter(
username = xirsysProperties.ident(),
password = xirsysProperties.secret(),
),
)
.build(XirsysClient::class.java)

override val active = xirsysProperties.enabled()

override fun createSession(id: String) {
Expand All @@ -41,72 +28,34 @@ class XirsysSessionHandler(

LOG.debug("Creating session id $id")

val result = xirsysClient.createChannel(
namespace = xirsysProperties.channelNamespace(),
environment = fafProperties.environment(),
channelName = id,
)

if (result is XirsysResponse.Error) {
LOG.error("Creating session failed: ${result.code}")
}
xirsysApiAdapter.createChannel(id)
}

override fun deleteSession(id: String) {
val result = xirsysClient.deleteChannel(
namespace = xirsysProperties.channelNamespace(),
environment = fafProperties.environment(),
channelName = id,
)

if (result is XirsysResponse.Error) {
LOG.error("Deleting session failed: ${result.code}")
}
xirsysApiAdapter.deleteChannel(channelName = id)
}

private fun listSessions(): List<String> =
when (
val result = xirsysClient.listChannel(
namespace = xirsysProperties.channelNamespace(),
environment = fafProperties.environment(),
)
) {
is XirsysResponse.Error -> emptyList<String>().also {
LOG.error("Listing sessions failed: ${result.code}")
}

is XirsysResponse.Success -> result.data
}
private fun listSessions(): List<String> = xirsysApiAdapter.listChannel()

override fun getIceServers() = listOf(Server(id = SERVER_NAME, region = "Global"))

override fun getIceServersSession(sessionId: String): List<Session.Server> =
when (
val result = xirsysClient.requestIceServers(
namespace = xirsysProperties.channelNamespace(),
environment = fafProperties.environment(),
channelName = sessionId,
turnRequest = TurnRequest(),
xirsysApiAdapter.requestIceServers(
channelName = sessionId,
turnRequest = TurnRequest(),
).iceServers.let {
listOf(
Session.Server(
id = SERVER_NAME,
username = it.username,
credential = it.credential,
urls = it.urls.map { url ->
// A sample response looks like "stun:fr-turn1.xirsys.com"
// The java URI class fails to read host and port due to the missing // after the :
// Thus we "normalize" the uri, even though it is technically valid
url.replaceFirst(":", "://")
},
),
)
) {
is XirsysResponse.Error -> emptyList<Session.Server>().also {
LOG.error("Requesting ICE servers failed: ${result.code}")
}

is XirsysResponse.Success -> result.data.iceServers.let {
listOf(
Session.Server(
id = SERVER_NAME,
username = it.username,
credential = it.credential,
urls = it.urls.map { url ->
// A sample response looks like "stun:fr-turn1.xirsys.com"
// The java URI class fails to read host and port due to the missing // after the :
// Thus we "normalize" the uri, even though it is technically valid
url.replaceFirst(":", "://")
},
),
)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.faforever.icebreaker.service.xirsys

import java.io.IOException

interface XirsysApiException

class XirsysSpecifiedApiException(val errorCode: String, override val message: String) :
IOException("Xirsys API responded with error code: $errorCode"), XirsysApiException
class XirsysUnspecifiedApiException(val errorResponse: String, cause: Exception? = null) :
IOException("Xirsys API failed with unparseable message: $errorResponse", cause), XirsysApiException

0 comments on commit 8b7bc70

Please sign in to comment.