how implement a gRPC/Proto endpoint with repeated in message (i.e nested types in request and response) - kotlin

Goal: I want to code a microservice exposing an endpoint receiving and responding a message with repeated. I tried apply what I learned from Proto official guide and I coded this proto:
syntax = "proto3";
option java_multiple_files = true;
option java_package = "com.mybank.endpoint";
option java_outer_classname = "TransactionsProto";
option objc_class_prefix = "HLW";
package com.mybank.endpoint;
import "google/protobuf/wrappers.proto";
service TransactionsService {
rpc PostTransactions(TransactionsRequest) returns (TransactionsReply);
}
message TransactionsRequest {
string transactionDesc = 1;
repeated Transaction transactions = 2;
}
message Transaction {
string id = 1;
string name = 2;
string description = 3;
}
message TransactionsReply {
string message = 1;
}
I could gradle build and I got this TransactionsServiceGrpcKt autogenerated
package com.mybank.endpoint
import com.mybank.endpoint.TransactionsServiceGrpc.getServiceDescriptor
import io.grpc.CallOptions
import io.grpc.CallOptions.DEFAULT
import io.grpc.Channel
import io.grpc.Metadata
import io.grpc.MethodDescriptor
import io.grpc.ServerServiceDefinition
import io.grpc.ServerServiceDefinition.builder
import io.grpc.ServiceDescriptor
import io.grpc.Status.UNIMPLEMENTED
import io.grpc.StatusException
import io.grpc.kotlin.AbstractCoroutineServerImpl
import io.grpc.kotlin.AbstractCoroutineStub
import io.grpc.kotlin.ClientCalls.unaryRpc
import io.grpc.kotlin.ServerCalls.unaryServerMethodDefinition
import io.grpc.kotlin.StubFor
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
import kotlin.jvm.JvmOverloads
import kotlin.jvm.JvmStatic
/**
* Holder for Kotlin coroutine-based client and server APIs for
* com.mybank.endpoint.TransactionsService.
*/
object TransactionsServiceGrpcKt {
#JvmStatic
val serviceDescriptor: ServiceDescriptor
get() = TransactionsServiceGrpc.getServiceDescriptor()
val postTransactionsMethod: MethodDescriptor<TransactionsRequest, TransactionsReply>
#JvmStatic
get() = TransactionsServiceGrpc.getPostTransactionsMethod()
/**
* A stub for issuing RPCs to a(n) com.mybank.endpoint.TransactionsService service as suspending
* coroutines.
*/
#StubFor(TransactionsServiceGrpc::class)
class TransactionsServiceCoroutineStub #JvmOverloads constructor(
channel: Channel,
callOptions: CallOptions = DEFAULT
) : AbstractCoroutineStub<TransactionsServiceCoroutineStub>(channel, callOptions) {
override fun build(channel: Channel, callOptions: CallOptions): TransactionsServiceCoroutineStub
= TransactionsServiceCoroutineStub(channel, callOptions)
/**
* Executes this RPC and returns the response message, suspending until the RPC completes
* with [`Status.OK`][io.grpc.Status]. If the RPC completes with another status, a
* corresponding
* [StatusException] is thrown. If this coroutine is cancelled, the RPC is also cancelled
* with the corresponding exception as a cause.
*
* #param request The request message to send to the server.
*
* #return The single response from the server.
*/
suspend fun postTransactions(request: TransactionsRequest): TransactionsReply = unaryRpc(
channel,
TransactionsServiceGrpc.getPostTransactionsMethod(),
request,
callOptions,
Metadata()
)}
/**
* Skeletal implementation of the com.mybank.endpoint.TransactionsService service based on Kotlin
* coroutines.
*/
abstract class TransactionsServiceCoroutineImplBase(
coroutineContext: CoroutineContext = EmptyCoroutineContext
) : AbstractCoroutineServerImpl(coroutineContext) {
/**
* Returns the response to an RPC for com.mybank.endpoint.TransactionsService.PostTransactions.
*
* If this method fails with a [StatusException], the RPC will fail with the corresponding
* [io.grpc.Status]. If this method fails with a [java.util.concurrent.CancellationException],
* the RPC will fail
* with status `Status.CANCELLED`. If this method fails for any other reason, the RPC will
* fail with `Status.UNKNOWN` with the exception as a cause.
*
* #param request The request from the client.
*/
open suspend fun postTransactions(request: TransactionsRequest): TransactionsReply = throw
StatusException(UNIMPLEMENTED.withDescription("Method com.mybank.endpoint.TransactionsService.PostTransactions is unimplemented"))
final override fun bindService(): ServerServiceDefinition = builder(getServiceDescriptor())
.addMethod(unaryServerMethodDefinition(
context = this.context,
descriptor = TransactionsServiceGrpc.getPostTransactionsMethod(),
implementation = ::postTransactions
)).build()
}
}
So far so good. Now I want to implement it and I am completed stuck.
Here is all three tentatives and its errors
package com.mybank.endpoint
import io.grpc.stub.StreamObserver
import javax.inject.Singleton
#Singleton
class TransactionsEndpoint : TransactionsServiceGrpc.TransactionsServiceImplBase(){
//First tentative
//This complains "'postTransactions' overrides nothing" and IntelliJ suggest second next approach
//override fun postTransactions(request: TransactionsRequest?) : TransactionsReply {
//Second Tentative
// override fun postTransactions(request: TransactionsRequest?, responseObserver: StreamObserver<TransactionsReply>?) {
// //it complains Type mismatch... Found: TransactionsReply
// return TransactionsReply.newBuilder().setMessage("teste").build()
// }
//Third Tentative
//This causes:
//Return type is 'TransactionsReply', which is not a subtype of overridden public open
// fun postTransactions(request: TransactionsRequest!, responseObserver: StreamObserver<TransactionsReply!>!):
// Unit defined in com.mybank.endpoint.TransactionsServiceGrpc.TransactionsServiceImplBase
//override fun postTransactions(request: TransactionsRequest?, responseObserver: StreamObserver<TransactionsReply>?) : TransactionsReply {
// return TransactionsReply.newBuilder().setMessage("teste").build()
// }
}
gradle.build
plugins {
id "org.jetbrains.kotlin.jvm" version "1.3.72"
id "org.jetbrains.kotlin.kapt" version "1.3.72"
id "org.jetbrains.kotlin.plugin.allopen" version "1.3.72"
id "application"
id 'com.google.protobuf' version '0.8.13'
}
version "0.2"
group "account-control"
repositories {
mavenLocal()
jcenter()
}
configurations {
// for dependencies that are needed for development only
developmentOnly
}
dependencies {
kapt(enforcedPlatform("io.micronaut:micronaut-bom:$micronautVersion"))
kapt("io.micronaut:micronaut-inject-java")
kapt("io.micronaut:micronaut-validation")
implementation(enforcedPlatform("io.micronaut:micronaut-bom:$micronautVersion"))
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:${kotlinVersion}")
implementation("org.jetbrains.kotlin:kotlin-reflect:${kotlinVersion}")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinxCoroutinesVersion")
implementation("io.micronaut:micronaut-runtime")
// implementation("io.micronaut.grpc:micronaut-grpc-runtime")
implementation("io.micronaut.grpc:micronaut-grpc-server-runtime:$micronautGrpcVersion")
implementation("io.micronaut.grpc:micronaut-grpc-client-runtime:$micronautGrpcVersion")
implementation("io.grpc:grpc-kotlin-stub:${grpcKotlinVersion}")
//Kafka
implementation("io.micronaut.kafka:micronaut-kafka")
runtimeOnly("ch.qos.logback:logback-classic:1.2.3")
runtimeOnly("com.fasterxml.jackson.module:jackson-module-kotlin:2.9.8")
kaptTest("io.micronaut:micronaut-inject-java")
testImplementation enforcedPlatform("io.micronaut:micronaut-bom:$micronautVersion")
testImplementation("org.junit.jupiter:junit-jupiter-api:5.3.0")
testImplementation("io.micronaut.test:micronaut-test-junit5")
testImplementation("org.mockito:mockito-junit-jupiter:2.22.0")
testRuntime("org.junit.jupiter:junit-jupiter-engine:5.3.0")
testRuntime("org.jetbrains.spek:spek-junit-platform-engine:1.1.5")
}
test.classpath += configurations.developmentOnly
mainClassName = "account-control.Application"
test {
useJUnitPlatform()
}
allOpen {
annotation("io.micronaut.aop.Around")
}
compileKotlin {
kotlinOptions {
jvmTarget = '11'
//Will retain parameter names for Java reflection
javaParameters = true
}
}
//compileKotlin.dependsOn(generateProto)
compileTestKotlin {
kotlinOptions {
jvmTarget = '11'
javaParameters = true
}
}
tasks.withType(JavaExec) {
classpath += configurations.developmentOnly
jvmArgs('-XX:TieredStopAtLevel=1', '-Dcom.sun.management.jmxremote')
}
sourceSets {
main {
java {
srcDirs 'build/generated/source/proto/main/grpc'
srcDirs 'build/generated/source/proto/main/grpckt'
srcDirs 'build/generated/source/proto/main/java'
}
}
}
protobuf {
protoc { artifact = "com.google.protobuf:protoc:${protocVersion}" }
plugins {
grpc { artifact = "io.grpc:protoc-gen-grpc-java:${grpcVersion}" }
grpckt { artifact = "io.grpc:protoc-gen-grpc-kotlin:${grpcKotlinVersion}" }
}
generateProtoTasks {
all()*.plugins {
grpc {}
grpckt {}
}
}
}
I have successfully created my first grpc endpoint with very basic request/reply based on String and now I want to move forward by creating a list of a message. As an analogy, let's say I want a DTO/Pojo which contains a list of an entity.
Honestly, I am totally stuck. So, my main question is: how implement a proto service with repeated message?
An useful comment that can give a north is, why I see in autogenerated stub a method to be implemented with "..., responseObserver: StreamObserver?" instead of a simple "TransactionsReply" as I clearly specify in my proto? What relationship between StreamObserver and repeated message?
Here is the whole project in my GitHub develop branch
You will find two protos: one well successful implement with simple request/reply and other failing as explained above.
*** edited after first answer from Louis
I am quite confused.
With a simple proto as
...
service Account {
rpc SendDebit (DebitRequest) returns (DebitReply) {}
}
message DebitRequest {
string name = 1;
}
message DebitReply {
string message = 1;
}
I can implemented with
override suspend fun sendDebit(request: DebitRequest): DebitReply {
return DebitReply.newBuilder().setMessage("teste").build()
}
Nevertheless, with
...
service TransactionsService {
rpc PostTransactions(TransactionsRequest) returns (TransactionsReply);
}
message TransactionsRequest {
string transactionDesc = 1;
repeated Transaction transactions = 2;
}
message Transaction {
string id = 1;
string name = 2;
string description = 3;
}
message TransactionsReply {
string message = 1;
}
I can't override with same type of response (note that the reply is exactly the same)
neither this implementation proposed from IntelliJ
override fun postTransactions(request: TransactionsRequest?, responseObserver: StreamObserver<TransactionsReply>?) {
super.postTransactions(request, responseObserver)
return TransactionsReply.newBuilder().setMessage("testReply").build()
}
nor this with similar response approach
override fun postTransactions(request: TransactionsRequest?, responseObserver: StreamObserver<TransactionsReply>?) :TransactionsReply {
super.postTransactions(request, responseObserver)
return TransactionsReply.newBuilder().setMessage("testReply").build()
}
On top of that, why if the reply is exactly the same, in my first approach I didn't get StreamObserver proposed from IntelliJ?
*** Final solution thanks to Louis' help. I extended wrong abstract class
override suspend fun postTransactions(request: TransactionsRequest): TransactionsReply {
return TransactionsReply.newBuilder().setMessage("testReply").build()
}

You're looking for
class TransactionsEndpoint : TransactionsServiceGrpcKt.TransactionsServiceCoroutineImplBase(){
override suspend fun postTransactions(request: TransactionsRequest) : TransactionsReply {
...
}
}
...with the suspend modifier and no ? on the request, extending TransactionsServiceCoroutineImplBase with the Coroutine in there.
Note that this has nothing to do with a repeated message. Your RPC has a single input proto, which gets sent only once -- which may well be what you want, unless you want a stream of requests, in which case your proto file should look like
service TransactionsService {
rpc PostTransactions(stream TransactionsRequest) returns (TransactionsReply);
}
...and the Kotlin generated code will look different in that case.

Related

Spring Cloud Gateway Filter with external configuration service call

I am working on a Spring Cloud Gateway app that has a filter controlling access to certain paths or features based on a configuration held by a different service. So if a path is associated with feature x then only allow access if the configuration service returns that feature x is enabled.
The configuration is returned as a Mono and then flatMapped to check the enabled features. This all appears to work correctly. If the feature is enabled then the request is allowed to proceed through the chain. If the feature is disabled, then the response status is set to forbidden and the request marked as complete. However, this does not appear to stop the filter chain, and the request continues to be processed and eventually returns a 200 response.
If the feature configuration is not returned from an external source and is immediately available then this logic works correctly, but this involves a blocking call and does not seem desirable. I cannot see what is wrong with the first approach. It seems to be similar to examples available elsewhere.
I believe my question is similar to this one:
https://stackoverflow.com/questions/73496938/spring-cloud-api-gateway-custom-filters-with-external-api-for-authorization/75095356#75095356
Filter 1
This is the way I would like to do this:
override fun filter(exchange: ServerWebExchange, chain: GatewayFilterChain): Mono<Void> {
logger.info("Feature Security Filter")
// getFeatures returns Mono<Map<String, Boolean>>
return featureConfigService.getFeatures().flatMap { features ->
val path = exchange.request.path.toString()
val method = exchange.request.method.toString()
if (featureMappings.keys.any { it.matcher(path).matches() }) {
val pathIsRestricted = featureMappings
.filter { it.key.matcher(path).matches() }
.filter { features[it.value.requiresFeature] != true || !it.value.methodsAllowed.contains(method) }
.isNotEmpty()
if (pathIsRestricted) {
logger.warn("Access to path [$method|$path] restricted. ")
exchange.response.statusCode = HttpStatus.FORBIDDEN
exchange.response.setComplete()
// processing should stop here but continues through other filters
}
}
chain.filter(exchange);
}
}
Filter 2
This way works but involves a blocking call in featureService.
override fun filter(exchange: ServerWebExchange, chain: GatewayFilterChain): Mono<Void> {
logger.info("Feature Security Filter")
// this call returns a Map<String, Boolean> instead of a Mono
val features = featureService.getFeatureConfig()
val path = exchange.request.path.toString()
val method = exchange.request.method.toString()
if (featureMappings.keys.any { it.matcher(path).matches() }) {
val pathIsRestricted = featureMappings
.filter { it.key.matcher(path).matches() }
.filter { features[it.value.requiresFeature] != true || !it.value.methodsAllowed.contains(method) }
.isNotEmpty()
if (pathIsRestricted) {
logger.warn("Access to path [$method|$path] restricted. ")
val response: ServerHttpResponse = exchange.response
response.statusCode = HttpStatus.FORBIDDEN;
return response.setComplete()
// this works as this request will complete here
}
}
return chain.filter(exchange)
}
When the tests run I can see that a path is correctly logged as restricted, and the response status is set to HttpStatus.FORBIDDEN as expected, but the request continues to be processed by filters later in the chain, and eventually returns a 200 response.
I've tried returning variations on Mono.error and onErrorComplete but I get the same behaviour. I am new to Spring Cloud Gateway and cannot see what I am doing wrong
After doing a few tests, I figured out that Filters are executed after route filters even if you set high order. If you need to filter requests before routing, you can use WebFilter. Here is a working Java example based on your requirements.
package com.test.test.filters;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;
import java.util.Map;
#Configuration
#Slf4j
public class TestGlobalFilter implements WebFilter, Ordered {
private Mono<Map<String, Boolean>> test() {
return Mono.just(Map.of("test", Boolean.TRUE));
}
#Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE;
}
#Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
log.info("Feature Security Filter");
// getFeatures returns Mono<Map<String, Boolean>>
return test().flatMap(features -> {
final var isRestricted = features.get("test");
if (Boolean.TRUE.equals(isRestricted)) {
log.info("Feature Security stop");
exchange.getResponse().setStatusCode(HttpStatus. FORBIDDEN);
return exchange.getResponse().setComplete();
}
return chain.filter(exchange);
});
}
}

If and how can I install a ktor plugin locally within a route for GET but not for POST?

For a ktor (2.0.3) application (kotlin 1.7.10) I want to have two endpoints on the same route (/feedback) but with different http methods, one GET and one POST.
So far no problem.
Now I would like to install an AuthorizationPlugin on only one of them.
I know how to install a plugin for a specific route only, but is it also possible to separately install it for different http methods on the same route?
So far I could not figure out a solution that does not require me to either introduce different routes (e.g. /feedback/read, /feedback/new) or handle the authorization check within the GET and POST callbacks directly.
The following is a reduced code containing two tests demonstrating the problem.
package some.example.package
import io.ktor.client.request.*
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.auth.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import io.ktor.server.testing.*
import org.apache.http.auth.AuthenticationException
import kotlin.test.Test
import kotlin.test.assertEquals
internal enum class AuthRole {
Admin, User
}
#kotlinx.serialization.Serializable
internal data class AuthUserSession(val username: String, val roles: Set<AuthRole> = setOf()) : Principal
const val authName = "form-auth"
const val usernameFormField = "username"
const val passwordFormField = "password"
/**
* Plugin Implementation
*/
internal val AuthorizationPlugin = createRouteScopedPlugin(
name = "AuthorizationPlugin",
createConfiguration = ::RoleBaseConfiguration
) {
pluginConfig.apply {
on(AuthenticationChecked) { call ->
val principal =
call.authentication.principal<AuthUserSession>() ?: throw Exception("Missing principal")
val userRoles = principal.roles
val denyReasons = mutableListOf<String>()
roles?.let {
if (roles!!.none { it in userRoles }) {
denyReasons += "Principal $principal has none of the sufficient role(s) ${
roles!!.joinToString(
" or "
)
}"
}
}
if (denyReasons.isNotEmpty()) {
val message = denyReasons.joinToString(". ")
throw Exception(message)
}
}
}
}
internal class RoleBaseConfiguration (
var roles: Set<AuthRole>? = null,
)
/**
* Server setup
*/
internal fun Application.setupConfig() {
install(Authentication) {
form(authName) {
userParamName = usernameFormField
passwordParamName = passwordFormField
challenge {
throw AuthenticationException()
}
validate { cred: UserPasswordCredential ->
if (cred.name == AuthRole.Admin.name) {
AuthUserSession(username = "admin", roles = setOf(AuthRole.Admin))
} else {
AuthUserSession(username = "user", roles = setOf(AuthRole.User))
}
}
}
}
routing {
route("feedback") {
authenticate(authName) {
post {
call.respond(HttpStatusCode.Created, "Submitting feedback")
}
install(AuthorizationPlugin) {
roles = setOf(AuthRole.Admin)
}
get {
call.respond(HttpStatusCode.OK, "Getting feedback")
}
}
}
}
}
/**
* Tests
*/
internal class PluginIssueTest {
/**
* For a valid solution this test should succeed.
*/
#Test
fun testGiveFeedback() = testApplication {
application {
setupConfig()
}
client.post("/feedback") {
header(HttpHeaders.ContentType, ContentType.Application.FormUrlEncoded.toString())
setBody(
listOf(
usernameFormField to AuthRole.User.name,
passwordFormField to "mypassword"
).formUrlEncode()
)
}.apply {
assertEquals(HttpStatusCode.Created, status)
}
}
/**
* For this test the plugin is successfully called and required role is checked.
*/
#Test
fun testReadFeedback() = testApplication {
application {
setupConfig()
}
client.get("/feedback") {
header(HttpHeaders.ContentType, ContentType.Application.FormUrlEncoded.toString())
setBody(
listOf(
usernameFormField to AuthRole.Admin.name,
passwordFormField to "mypassword"
).formUrlEncode()
)
}.apply {
assertEquals(HttpStatusCode.OK, status)
}
}
}
I made most of the things internal so they would not interfere with the implementations for my actual application. It should not have any influence on the tests.
Suggestions are highly appreciated.
If I forgot some important information please let me know.
I took a closer look at the implementation of authenticate and found a solution there.
It uses the createChild of the Route class with a custom RouteSelector that always evaluates to a "transparent" quality, to define a specific route for the authentication.
Since I currently only need a single RouteSelector instance I simplified it to be an object instead of a class.
By adding the following implementation to my code...
fun Route.authorize(
roles: Set<AuthRole>,
build: Route.() -> Unit
): Route {
val authenticatedRoute = createChild(AuthorizationRouteSelector)
authenticatedRoute.install(AuthorizationPlugin) {
this.roles = roles
}
authenticatedRoute.build()
return authenticatedRoute
}
object AuthorizationRouteSelector : RouteSelector() {
override fun evaluate(context: RoutingResolveContext, segmentIndex: Int): RouteSelectorEvaluation {
return RouteSelectorEvaluation.Transparent
}
override fun toString(): String = "(authorize \"default\" )"
}
...I was able to use my authorization plugin as follows:
routing {
route("feedback") {
authenticate(authName) {
post {
call.respond(HttpStatusCode.Created, "Submitting feedback")
}
authorize(setOf(AuthRole.Admin)) {
get {
call.respond(HttpStatusCode.OK, "Getting feedback")
}
}
}
}
}
making both tests succeed.

Ktor Server/Application request/response body logging

Is there any way to log the request and response body from the ktor server communication?
The buildin CallLogging feature only logs the metadata of a call. I tried writing my own logging feature like in this example: https://github.com/Koriit/ktor-logging/blob/master/src/main/kotlin/korrit/kotlin/ktor/features/logging/Logging.kt
class Logging(private val logger: Logger) {
class Configuration {
var logger: Logger = LoggerFactory.getLogger(Logging::class.java)
}
private suspend fun logRequest(call: ApplicationCall) {
logger.info(StringBuilder().apply {
appendLine("Received request:")
val requestURI = call.request.path()
appendLine(call.request.origin.run { "${method.value} $scheme://$host:$port$requestURI $version" })
call.request.headers.forEach { header, values ->
appendLine("$header: ${values.firstOrNull()}")
}
try {
appendLine()
appendLine(String(call.receive<ByteArray>()))
} catch (e: RequestAlreadyConsumedException) {
logger.error("Logging payloads requires DoubleReceive feature to be installed with receiveEntireContent=true", e)
}
}.toString())
}
private suspend fun logResponse(call: ApplicationCall, subject: Any) {
logger.info(StringBuilder().apply {
appendLine("Sent response:")
appendLine("${call.request.httpVersion} ${call.response.status()}")
call.response.headers.allValues().forEach { header, values ->
appendLine("$header: ${values.firstOrNull()}")
}
when (subject) {
is TextContent -> appendLine(subject.text)
is OutputStreamContent -> appendLine() // ToDo: How to get response body??
else -> appendLine("unknown body type")
}
}.toString())
}
/**
* Feature installation.
*/
fun install(pipeline: Application) {
pipeline.intercept(ApplicationCallPipeline.Monitoring) {
logRequest(call)
proceedWith(subject)
}
pipeline.sendPipeline.addPhase(responseLoggingPhase)
pipeline.sendPipeline.intercept(responseLoggingPhase) {
logResponse(call, subject)
}
}
companion object Feature : ApplicationFeature<Application, Configuration, Logging> {
override val key = AttributeKey<Logging>("Logging Feature")
val responseLoggingPhase = PipelinePhase("ResponseLogging")
override fun install(pipeline: Application, configure: Configuration.() -> Unit): Logging {
val configuration = Configuration().apply(configure)
return Logging(configuration.logger).apply { install(pipeline) }
}
}
}
It works fine for logging the request body using the DoubleReceive plugin. And if the response is plain text i can log the response as the subject in the sendPipeline interception will be of type TextContent or like in the example ByteArrayContent.
But in my case i am responding a data class instance with Jackson ContentNegotiation. In this case the subject is of type OutputStreamContent and i see no options to geht the serialized body from it.
Any idea how to log the serialized response json in my logging feature? Or maybe there is another option using the ktor server? I mean i could serialize my object manually and respond plain text, but thats an ugly way to do it.
I'm not shure about if this is the best way to do it, but here it is:
public fun ApplicationResponse.toLogString(subject: Any): String = when(subject) {
is TextContent -> subject.text
is OutputStreamContent -> {
val channel = ByteChannel(true)
runBlocking {
(subject as OutputStreamContent).writeTo(channel)
val buffer = StringBuilder()
while (!channel.isClosedForRead) {
channel.readUTF8LineTo(buffer)
}
buffer.toString()
}
}
else -> String()
}

Access ApplicationCall in object without propagation

Is there a thread-safe method in Ktor where it is possible to statically access the current ApplicationCall? I am trying to get the following simple example to work;
object Main {
fun start() {
val server = embeddedServer(Jetty, 8081) {
intercept(ApplicationCallPipeline.Call) {
// START: this will be more dynamic in the future, we don't want to pass ApplicationCall
Addon.processRequest()
// END: this will be more dynamic in the future, we don't want to pass ApplicationCall
call.respondText(output, ContentType.Text.Html, HttpStatusCode.OK)
return#intercept finish()
}
}
server.start(wait = true)
}
}
fun main(args: Array<String>) {
Main.start();
}
object Addon {
fun processRequest() {
val call = RequestUtils.getCurrentApplicationCall()
// processing of call.request.queryParameters
// ...
}
}
object RequestUtils {
fun getCurrentApplicationCall(): ApplicationCall {
// Here is where I am getting lost..
return null
}
}
I would like to be able to get the ApplicationCall for the current context to be available statically from the RequestUtils so that I can access information about the request anywhere. This of course needs to scale to be able to handle multiple requests at the same time.
I have done some experiments with dependency inject and ThreadLocal, but to no success.
Well, the application call is passed to a coroutine, so it's really dangerous to try and get it "statically", because all requests are treated in a concurrent context.
Kotlin official documentation talks about Thread-local in the context of coroutine executions. It uses the concept of CoroutineContext to restore Thread-Local values in specific/custom coroutine context.
However, if you are able to design a fully asynchronous API, you will be able to bypass thread-locals by directly creating a custom CoroutineContext, embedding the request call.
EDIT: I've updated my example code to test 2 flavors:
async endpoint: Solution fully based on Coroutine contexts and suspend functions
blocking endpoint: Uses a thread-local to store application call, as referred in kotlin doc.
import io.ktor.server.engine.embeddedServer
import io.ktor.server.jetty.Jetty
import io.ktor.application.*
import io.ktor.http.ContentType
import io.ktor.http.HttpStatusCode
import io.ktor.response.respondText
import io.ktor.routing.get
import io.ktor.routing.routing
import kotlinx.coroutines.asContextElement
import kotlinx.coroutines.launch
import kotlin.coroutines.AbstractCoroutineContextElement
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.coroutineContext
/**
* Thread local in which you'll inject application call.
*/
private val localCall : ThreadLocal<ApplicationCall> = ThreadLocal();
object Main {
fun start() {
val server = embeddedServer(Jetty, 8081) {
routing {
// Solution requiring full coroutine/ supendable execution.
get("/async") {
// Ktor will launch this block of code in a coroutine, so you can create a subroutine with
// an overloaded context providing needed information.
launch(coroutineContext + ApplicationCallContext(call)) {
PrintQuery.processAsync()
}
}
// Solution based on Thread-Local, not requiring suspending functions
get("/blocking") {
launch (coroutineContext + localCall.asContextElement(value = call)) {
PrintQuery.processBlocking()
}
}
}
intercept(ApplicationCallPipeline.ApplicationPhase.Call) {
call.respondText("Hé ho", ContentType.Text.Plain, HttpStatusCode.OK)
}
}
server.start(wait = true)
}
}
fun main() {
Main.start();
}
interface AsyncAddon {
/**
* Asynchronicity propagates in order to properly access coroutine execution information
*/
suspend fun processAsync();
}
interface BlockingAddon {
fun processBlocking();
}
object PrintQuery : AsyncAddon, BlockingAddon {
override suspend fun processAsync() = processRequest("async", fetchCurrentCallFromCoroutineContext())
override fun processBlocking() = processRequest("blocking", fetchCurrentCallFromThreadLocal())
private fun processRequest(prefix : String, call : ApplicationCall?) {
println("$prefix -> Query parameter: ${call?.parameters?.get("q") ?: "NONE"}")
}
}
/**
* Custom coroutine context allow to provide information about request execution.
*/
private class ApplicationCallContext(val call : ApplicationCall) : AbstractCoroutineContextElement(Key) {
companion object Key : CoroutineContext.Key<ApplicationCallContext>
}
/**
* This is your RequestUtils rewritten as a first-order function. It defines as asynchronous.
* If not, you won't be able to access coroutineContext.
*/
suspend fun fetchCurrentCallFromCoroutineContext(): ApplicationCall? {
// Here is where I am getting lost..
return coroutineContext.get(ApplicationCallContext.Key)?.call
}
fun fetchCurrentCallFromThreadLocal() : ApplicationCall? {
return localCall.get()
}
You can test it in your navigator:
http://localhost:8081/blocking?q=test1
http://localhost:8081/blocking?q=test2
http://localhost:8081/async?q=test3
server log output:
blocking -> Query parameter: test1
blocking -> Query parameter: test2
async -> Query parameter: test3
The key mechanism you want to use for this is the CoroutineContext. This is the place that you can set key value pairs to be used in any child coroutine or suspending function call.
I will try to lay out an example.
First, let us define a CoroutineContextElement that will let us add an ApplicationCall to the CoroutineContext.
class ApplicationCallElement(var call: ApplicationCall?) : AbstractCoroutineContextElement(ApplicationCallElement) {
companion object Key : CoroutineContext.Key<ApplicationCallElement>
}
Now we can define some helpers that will add the ApplicationCall on one of our routes. (This could be done as some sort of Ktor plugin that listens to the pipeline, but I don't want to add to much noise here).
suspend fun PipelineContext<Unit, ApplicationCall>.withCall(
bodyOfCall: suspend PipelineContext<Unit, ApplicationCall>.() -> Unit
) {
val pipeline = this
val appCallContext = buildAppCallContext(this.call)
withContext(appCallContext) {
pipeline.bodyOfCall()
}
}
internal suspend fun buildAppCallContext(call: ApplicationCall): CoroutineContext {
var context = coroutineContext
val callElement = ApplicationCallElement(call)
context = context.plus(callElement)
return context
}
And then we can use it all together like in this test case below where we are able to get the call from a nested suspending function:
suspend fun getSomethingFromCall(): String {
val call = coroutineContext[ApplicationCallElement.Key]?.call ?: throw Exception("Element not set")
return call.parameters["key"] ?: throw Exception("Parameter not set")
}
fun Application.myApp() {
routing {
route("/foo") {
get {
withCall {
call.respondText(getSomethingFromCall())
}
}
}
}
}
class ApplicationCallTest {
#Test
fun `we can get the application call in a nested function`() {
withTestApplication({ myApp() }) {
with(handleRequest(HttpMethod.Get, "/foo?key=bar")) {
assertEquals(HttpStatusCode.OK, response.status())
assertEquals("bar", response.content)
}
}
}
}

How to be notified when all futures in Future.compose chain succeeded?

My application (typical REST server that calls other REST services internally) has two main classes to perform the bootstrapping procedure.
There is the Application.kt class that is supposed to configure the vertx instance itself and to register certain modules (jackson kotlin integration for example):
class Application(
private val profileSetting: String? = System.getenv("ACTIVE_PROFILES"),
private val logger: Logger = LoggerFactory.getLogger(Application::class.java)!!
) {
fun bootstrap() {
val profiles = activeProfiles()
val meterRegistry = configureMeters()
val vertx = bootstrapVertx(meterRegistry)
vertx.deployVerticle(ApplicationBootstrapVerticle(profiles)) { startup ->
if (startup.succeeded()) {
logger.info("Application startup finished")
} else {
logger.error("Application startup failed", startup.cause())
vertx.close()
}
}
}
}
In addition there is a ApplicationBootstrapVerticle.kt class that is supposed to deploy the different verticles in a defined order. Some of them in sequence, some of them in parallel:
class ApplicationBootstrapVerticle(
private val profiles: List<String>,
private val logger: Logger = LoggerFactory.getLogger(ApplicationBootstrapVerticle::class.java)
) : AbstractVerticle() {
override fun start(startFuture: Future<Void>) {
initializeApplicationConfig().compose {
logger.info("Application configuration initialized")
initializeScheduledJobs()
}.compose {
logger.info("Scheduled jobs initialized")
initializeRestEndpoints()
}.compose {
logger.info("Http server started")
startFuture
}.setHandler { ar ->
if (ar.succeeded()) {
startFuture.complete()
} else {
startFuture.fail(ar.cause())
}
}
}
private fun initializeApplicationConfig(): Future<String> {
return Future.future<String>().also {
vertx.deployVerticle(
ApplicationConfigVerticle(profiles),
it.completer()
)
}
}
private fun initializeScheduledJobs(): CompositeFuture {
val stationsJob = Future.future<String>()
val capabilitiesJob = Future.future<String>()
return CompositeFuture.all(stationsJob, capabilitiesJob).also {
vertx.deployVerticle(
StationQualitiesVerticle(),
stationsJob.completer()
)
vertx.deployVerticle(
VideoCapabilitiesVerticle(),
capabilitiesJob.completer()
)
}
}
private fun initializeRestEndpoints(): Future<String> {
return Future.future<String>().also {
vertx.deployVerticle(
RestEndpointVerticle(dispatcherFactory = RouteDispatcherFactory(vertx)),
it.completer()
)
}
}
}
I am not sure if this is the supposed way to bootstrap an application, if there is any. More important though, I am not sure if I understand the Future.compose mechanics correctly.
The application starts up successfully and I see all desired log messages except the
Application startup finished
message. Also the following code is never called in case of successs:
}.setHandler { ar ->
if (ar.succeeded()) {
startFuture.complete()
} else {
startFuture.fail(ar.cause())
}
}
In case of an failure though, for example when my application configuration files (yaml) cannot be parsed because there is an unknown field in the destination entity, the log message
Application startup failed
appears in the logs and also the code above is invoked.
I am curious what is wrong with my composed futures chain. I thought that the handler would be called after the previous futures succeeded or one of them failed but I think it's only called in case of success.
Update
I suppose that an invocation of startFuture.complete() was missing. By adapting the start method, it finally worked:
override fun start(startFuture: Future<Void>) {
initializeApplicationConfig().compose {
logger.info("Application configuration initialized")
initializeScheduledJobs()
}.compose {
logger.info("Scheduled jobs initialized")
initializeRestEndpoints()
}.compose {
logger.info("Http server started")
startFuture.complete()
startFuture
}.setHandler(
startFuture.completer()
)
}
I am not sure though, if this is the supposed way to handle this future chain.
The solution that worked for me looks like this:
override fun start(startFuture: Future<Void>) {
initializeApplicationConfig().compose {
logger.info("Application configuration initialized")
initializeScheduledJobs()
}.compose {
logger.info("Scheduled jobs initialized")
initializeRestEndpoints()
}.setHandler { ar ->
if(ar.succeeded()) {
logger.info("Http server started")
startFuture.complete()
} else {
startFuture.fail(ar.cause())
}
}
}