Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
com.trendyol.stove.testing.e2e.kafka.TestSystemInterceptor.kt Maven / Gradle / Ivy
package com.trendyol.stove.testing.e2e.kafka
import arrow.core.*
import com.fasterxml.jackson.databind.ObjectMapper
import kotlinx.coroutines.*
import org.apache.kafka.clients.consumer.*
import org.apache.kafka.clients.producer.*
import org.slf4j.*
import org.springframework.kafka.listener.*
import org.springframework.kafka.support.ProducerListener
import java.util.*
import java.util.concurrent.*
import kotlin.reflect.KClass
import kotlin.time.Duration
class TestSystemKafkaInterceptor(private val objectMapper: ObjectMapper) :
CompositeRecordInterceptor(),
ProducerListener {
private val logger: Logger = LoggerFactory.getLogger(javaClass)
private val consumedRecords: ConcurrentMap> = ConcurrentHashMap()
private val producedRecords: ConcurrentMap> = ConcurrentHashMap()
private val exceptions: ConcurrentMap> = ConcurrentHashMap()
override fun success(
record: ConsumerRecord,
consumer: Consumer
): Unit = runBlocking {
consumedRecords.putIfAbsent(UUID.randomUUID(), record)
logger.info(
"""
SUCCESSFULLY CONSUMED:
Consumer: ${consumer.groupMetadata().memberId()}
Topic: ${record.topic()}
Record: ${record.value()}
Key: ${record.key()}
Headers: ${record.headers().map { Pair(it.key(), String(it.value())) }}
TestCase: ${record.headers().firstOrNone { it.key() == "testCase" }.map { String(it.value()) }.getOrElse { "" }}
""".trimIndent()
)
}
override fun onSuccess(
record: ProducerRecord,
recordMetadata: RecordMetadata
): Unit = runBlocking {
producedRecords.putIfAbsent(UUID.randomUUID(), record)
logger.info(
"""
SUCCESSFULLY PUBLISHED:
Topic: ${record.topic()}
Record: ${record.value()}
Key: ${record.key()}
Headers: ${record.headers().map { Pair(it.key(), String(it.value())) }}
TestCase: ${record.headers().firstOrNone { it.key() == "testCase" }.map { String(it.value()) }.getOrElse { "" }}
""".trimIndent()
)
}
override fun onError(
record: ProducerRecord,
recordMetadata: RecordMetadata?,
exception: Exception
): Unit = runBlocking {
exceptions.putIfAbsent(
UUID.randomUUID(),
Failure(
ObservedMessage(record.value().toString(), record.toMetadata()),
extractCause(exception)
)
)
logger.error(
"""
PRODUCER GOT AN ERROR:
Topic: ${record.topic()}
Record: ${record.value()}
Key: ${record.key()}
Headers: ${record.headers().map { Pair(it.key(), String(it.value())) }}
TestCase: ${record.headers().firstOrNone { it.key() == "testCase" }.map { String(it.value()) }.getOrElse { "" }}
Exception: $exception
""".trimIndent()
)
}
override fun failure(
record: ConsumerRecord,
exception: Exception,
consumer: Consumer
): Unit = runBlocking {
exceptions.putIfAbsent(
UUID.randomUUID(),
Failure(
ObservedMessage(record.value().toString(), record.toMetadata()),
extractCause(exception)
)
)
logger.error(
"""
CONSUMER GOT AN ERROR:
Topic: ${record.topic()}
Record: ${record.value()}
Key: ${record.key()}
Headers: ${record.headers().map { Pair(it.key(), String(it.value())) }}
TestCase: ${record.headers().firstOrNone { it.key() == "testCase" }.map { String(it.value()) }.getOrElse { "" }}
Exception: $exception
""".trimIndent()
)
}
internal suspend fun waitUntilConsumed(
atLeastIn: Duration,
clazz: KClass,
condition: (metadata: ParsedMessage) -> Boolean
) {
val getRecords = { consumedRecords.map { it.value } }
getRecords.waitUntilConditionMet(atLeastIn, "While CONSUMING ${clazz.java.simpleName}") {
val outcome = readCatching(it.value(), clazz)
outcome.isSuccess && condition(ParsedMessage(outcome.getOrNull().toOption(), it.toMetadata()))
}
throwIfFailed(clazz, condition)
}
internal suspend fun waitUntilFailed(
atLeastIn: Duration,
clazz: KClass,
condition: (metadata: FailedParsedMessage) -> Boolean
) {
val getRecords = { exceptions.map { it.value } }
getRecords.waitUntilConditionMet(atLeastIn, "While WAITING FOR FAILURE ${clazz.java.simpleName}") {
val outcome = readCatching(it.message.actual.toString(), clazz)
outcome.isSuccess &&
condition(
FailedParsedMessage(
ParsedMessage(outcome.getOrNull().toOption(), it.message.metadata),
it.reason
)
)
}
throwIfSucceeded(clazz, condition)
}
internal suspend fun waitUntilPublished(
atLeastIn: Duration,
clazz: KClass,
condition: (message: ParsedMessage) -> Boolean
) {
val getRecords = { producedRecords.map { it.value } }
getRecords.waitUntilConditionMet(atLeastIn, "While PUBLISHING ${clazz.java.simpleName}") {
val outcome = readCatching(it.value().toString(), clazz)
outcome.isSuccess && condition(ParsedMessage(outcome.getOrNull().toOption(), it.toMetadata()))
}
throwIfFailed(clazz, condition)
}
private fun extractCause(listenerException: Throwable): Throwable =
when (listenerException) {
is ListenerExecutionFailedException ->
listenerException.cause
?: AssertionError("No cause found: Listener was not able to capture the cause")
else -> listenerException
}
private fun readCatching(
json: String,
clazz: KClass
): Result = runCatching { objectMapper.readValue(json, clazz.java) }
private fun throwIfFailed(
clazz: KClass,
selector: (message: ParsedMessage) -> Boolean
) = exceptions
.filter {
selector(
ParsedMessage(
readCatching(it.value.message.actual.toString(), clazz).getOrNull().toOption(),
it.value.message.metadata
)
)
}
.forEach { throw it.value.reason }
private fun throwIfSucceeded(
clazz: KClass,
selector: (FailedParsedMessage) -> Boolean
): Unit = consumedRecords
.filter { record ->
selector(
FailedParsedMessage(
ParsedMessage(readCatching(record.value.value(), clazz).getOrNull().toOption(), record.value.toMetadata()),
getExceptionFor(clazz, selector)
)
)
}
.forEach { throw AssertionError("Expected to fail but succeeded: $it") }
private fun getExceptionFor(
clazz: KClass,
selector: (message: FailedParsedMessage) -> Boolean
): Throwable = exceptions
.map { it.value }
.first {
selector(
FailedParsedMessage(
ParsedMessage(
readCatching(it.message.actual.toString(), clazz).getOrNull().toOption(),
it.message.metadata
),
it.reason
)
)
}
.reason
private suspend fun (() -> Collection).waitUntilConditionMet(
duration: Duration,
subject: String,
condition: (T) -> Boolean
): Collection = runCatching {
val collectionFunc = this
withTimeout(duration) { while (!collectionFunc().any { condition(it) }) delay(50) }
return collectionFunc().filter { condition(it) }
}.recoverCatching {
when (it) {
is TimeoutCancellationException -> throw AssertionError("GOT A TIMEOUT: $subject. ${dumpMessages()}")
is ConcurrentModificationException ->
Result.success(waitUntilConditionMet(duration, subject, condition))
else -> throw it
}.getOrThrow()
}.getOrThrow()
private fun dumpMessages(): String =
"""
Messages in the KafkaSystem so far:
PUBLISHED MESSAGES:
${producedRecords.map { it.value.value() }.joinToString("\n")}
------------------------
CONSUMED MESSAGES:
${consumedRecords.map { it.value.value() }.joinToString("\n")}
""".trimIndent()
}
internal fun ProducerRecord.toMetadata(): MessageMetadata = MessageMetadata(
this.topic(),
this.key().toString(),
this.headers().associate { h -> Pair(h.key(), String(h.value())) }
)
internal fun ConsumerRecord.toMetadata(): MessageMetadata = MessageMetadata(
this.topic(),
this.key().toString(),
this.headers().associate { h -> Pair(h.key(), String(h.value())) }
)