Skip to content

Commit

Permalink
Streamline TqRest Performance
Browse files Browse the repository at this point in the history
✨ Add support for streaming response data and optimizing for arrays
🔥 Remove RestLog support as it wasn't used in the wild much and
   hindered the ability to stream json array responses efficiently.

Overall not holding everything into memory first before transforming the
data improves the speed and lowers the memory requirements for existing
use cases by a tiny amount. But for cases where the response was an
array list, it creates a massive improvement in performance and reduces
the memory requirements when receiving large payloads.
  • Loading branch information
sepatel committed Apr 27, 2023
1 parent 5d218b5 commit 871e974
Show file tree
Hide file tree
Showing 2 changed files with 38 additions and 37 deletions.
61 changes: 25 additions & 36 deletions tekniq-rest/src/main/kotlin/io/tekniq/rest/TqRestClient.kt
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package io.tekniq.rest

import com.fasterxml.jackson.core.JsonToken
import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.KotlinFeature
import com.fasterxml.jackson.module.kotlin.KotlinModule
import java.io.IOException
import java.io.InputStream
import java.net.URI
import java.net.http.HttpClient
import java.net.http.HttpRequest
Expand All @@ -13,15 +15,13 @@ import java.net.http.HttpResponse
import java.security.SecureRandom
import java.security.cert.X509Certificate
import java.time.Duration
import java.util.*
import javax.net.ssl.SSLContext
import javax.net.ssl.X509TrustManager
import kotlin.reflect.KClass
import kotlin.system.measureTimeMillis

@Suppress("unused")
open class TqRestClient(
val logHandler: RestLogHandler = NoOpRestLogHandler,
val mapper: ObjectMapper = ObjectMapper().registerModule(
KotlinModule.Builder()
.withReflectionCacheSize(512)
Expand Down Expand Up @@ -129,24 +129,13 @@ open class TqRestClient(
.build()

response = try {
val resp = client.send(request, HttpResponse.BodyHandlers.ofString())
val resp = client.send(request, HttpResponse.BodyHandlers.ofInputStream())
TqResponse(resp.statusCode(), resp.body(), resp.headers().map(), mapper)
} catch (e: IOException) {
TqResponse(-1, e.message ?: "", request.headers().map(), mapper)
TqResponse(-1, (e.message ?: "").byteInputStream(), request.headers().map(), mapper)
} catch (e: InterruptedException) {
TqResponse(-1, e.message ?: "", request.headers().map(), mapper)
TqResponse(-1, (e.message ?: "").byteInputStream(), request.headers().map(), mapper)
}
}.also {
logHandler.onRestLog(
RestLog(
method,
url,
duration = it,
request = payload,
status = response.status,
response = response.body
)
)
}
return response
}
Expand All @@ -160,7 +149,7 @@ open class TqRestClient(

data class TqResponse(
val status: Int,
val body: String,
val body: InputStream,
private val headers: Map<String, Any>,
private val mapper: ObjectMapper
) {
Expand All @@ -184,25 +173,25 @@ data class TqResponse(

inline fun <reified T : Any> jsonAs(): T = jsonAsNullable(T::class)!!
inline fun <reified T : Any> jsonAsNullable(): T? = jsonAsNullable(T::class)
fun <T : Any> jsonAsNullable(type: KClass<T>): T? = mapper.readValue(body, type.java)
}

data class RestLog(
val method: String,
val url: String,
val ts: Date = Date(),
val duration: Long = 0,
val request: String? = null,
val status: Int = 0,
val response: String? = null
)

fun interface RestLogHandler {
fun onRestLog(log: RestLog)
}
fun <T : Any> jsonAsNullable(type: KClass<T>): T? = mapper.readValue(body.readAllBytes(), type.java)

inline fun <reified T : Any> jsonArrayOf(): Iterator<T> = jsonArrayOf(T::class)
fun <T : Any> jsonArrayOf(type: KClass<T>): Iterator<T> {
val parser = mapper.factory.createParser(body)
if (parser.nextToken() != JsonToken.START_ARRAY) error("Invalid start of array")
val it = object : Iterator<T> {
var checked: Boolean? = null
override fun hasNext(): Boolean {
val next = checked
if (next != null) return next
return (parser.nextToken() != JsonToken.END_ARRAY).also { checked = it }
}

private object NoOpRestLogHandler : RestLogHandler {
override fun onRestLog(log: RestLog) {
override fun next(): T {
if (hasNext()) return mapper.readValue(parser, type.java).also { checked = null }
throw NoSuchElementException()
}
}
return it
}
}

14 changes: 13 additions & 1 deletion tekniq-rest/src/test/kotlin/io/tekniq/rest/TqRestClientSpec.kt
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,22 @@ object TqRestClientSpec : DescribeSpec({
val url = "https://httpstat.us/200?sleep=2000" // 2 second delay with status 200 on success
val rest = TqRestClient()
val resp = rest.get(url, timeoutInSec = 1)
println(resp)
assertEquals(-1, resp.status)
}
}
//
// describe("Streaming functionality") {
// it("Should stream data back as well") {
// data class SimpleData(val name: String, val meta: Map<String, Any>)
// val url = "https://localhost:2223/api/test"
// val rest = TqRestClient(allowSelfSigned = true)
// val resp = rest.post(url, "true")
// assertEquals(200, resp.status)
// resp.jsonArrayOf<SimpleData>().forEach {
// println(it)
// }
// }
// }
}) {
data class PostmanEcho(
val args: Map<String, Any> = emptyMap(),
Expand Down

0 comments on commit 871e974

Please sign in to comment.