All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.trendyol.stove.testing.e2e.kafka.TestSystemInterceptor.kt Maven / Gradle / Ivy

There is a newer version: 1f1ca59
Show newest version
package com.trendyol.stove.testing.e2e.kafka

import arrow.core.Option
import arrow.core.firstOrNone
import arrow.core.getOrElse
import arrow.core.toOption
import com.fasterxml.jackson.databind.ObjectMapper
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeout
import org.apache.kafka.clients.consumer.Consumer
import org.apache.kafka.clients.consumer.ConsumerRecord
import org.apache.kafka.clients.producer.ProducerRecord
import org.apache.kafka.clients.producer.RecordMetadata
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.kafka.listener.CompositeRecordInterceptor
import org.springframework.kafka.listener.ListenerExecutionFailedException
import org.springframework.kafka.support.ProducerListener
import org.springframework.stereotype.Component
import java.util.*
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ConcurrentMap
import kotlin.reflect.KClass
import kotlin.time.Duration

@Component
class TestSystemKafkaInterceptor(private val objectMapper: ObjectMapper) :
    CompositeRecordInterceptor(),
    ProducerListener {
    data class Failure(
        val message: Any,
        val reason: Throwable
    )

    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(record.value(), 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(record.value(), 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()
        )
    }

    suspend fun  waitUntilConsumed(
        atLeastIn: Duration,
        clazz: KClass,
        condition: (Option) -> Boolean
    ) {
        val getRecords = { consumedRecords.map { it.value.value() } }
        getRecords.waitUntilConditionMet(atLeastIn, "While CONSUMING ${clazz.java.simpleName}") {
            val outcome = readCatching(it, clazz)
            outcome.isSuccess && condition(outcome.getOrNull().toOption())
        }

        throwIfFailed(clazz, condition)
    }

    suspend fun  waitUntilFailed(
        atLeastIn: Duration,
        clazz: KClass,
        condition: (Option, Throwable) -> Boolean
    ) {
        val getRecords = { exceptions.map { Pair(it.value.message.toString(), it.value.reason) } }
        getRecords.waitUntilConditionMet(atLeastIn, "While WAITING FOR FAILURE ${clazz.java.simpleName}") {
            val outcome = readCatching(it.first, clazz)
            outcome.isSuccess && condition(outcome.getOrNull().toOption(), it.second)
        }

        throwIfSucceeded(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
    }

    suspend fun  waitUntilPublished(
        atLeastIn: Duration,
        clazz: KClass,
        condition: (Option) -> Boolean
    ) {
        val getRecords = { producedRecords.map { it.value.value() } }
        getRecords.waitUntilConditionMet(atLeastIn, "While PUBLISHING ${clazz.java.simpleName}") {
            val outcome = readCatching(it.toString(), clazz)
            outcome.isSuccess && condition(outcome.getOrNull().toOption())
        }

        throwIfFailed(clazz, condition)
    }

    private fun  readCatching(
        json: String,
        clazz: KClass
    ) = runCatching { objectMapper.readValue(json, clazz.java) }

    fun reset() {
        exceptions.clear()
        producedRecords.clear()
        consumedRecords.clear()
    }

    private fun  throwIfFailed(
        clazz: KClass,
        selector: (Option) -> Boolean
    ) = exceptions
        .filter { selector(readCatching(it.value.message.toString(), clazz).getOrNull().toOption()) }
        .forEach { throw it.value.reason }

    private fun  throwIfSucceeded(
        clazz: KClass,
        selector: (Option, Throwable) -> Boolean
    ): Unit = consumedRecords
        .filter { selector(readCatching(it.value.value(), clazz).getOrNull().toOption(), getExceptionFor(selector, clazz)) }
        .forEach { throw AssertionError("Expected to fail but succeeded: $it") }

    private fun  getExceptionFor(
        selector: (Option, Throwable) -> Boolean,
        clazz: KClass
    ): Throwable = exceptions
        .map { Pair(it.value.message.toString(), it.value.reason) }
        .first { selector(readCatching(it.first, clazz).getOrNull().toOption(), it.second) }
        .second

    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()
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy