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

com.evolutiongaming.kafka.journal.KafkaHealthCheck.scala Maven / Gradle / Ivy

The newest version!
package com.evolutiongaming.kafka.journal

import cats.effect._
import cats.effect.concurrent.Ref
import cats.effect.implicits._
import cats.syntax.all._
import cats.{Applicative, Functor, Monad}
import com.evolutiongaming.catshelper.CatsHelper._
import com.evolutiongaming.catshelper.{FromTry, Log, LogOf}
import com.evolutiongaming.kafka.journal.util.CatsHelper._
import com.evolutiongaming.skafka.Topic
import com.evolutiongaming.skafka.consumer.{AutoOffsetReset, ConsumerConfig}
import com.evolutiongaming.skafka.producer.{ProducerConfig, ProducerRecord}

import scala.concurrent.duration._

trait KafkaHealthCheck[F[_]] {

  def error: F[Option[Throwable]]

  def done: F[Unit]
}

object KafkaHealthCheck {

  def empty[F[_] : Applicative]: KafkaHealthCheck[F] = new KafkaHealthCheck[F] {

    def error = none[Throwable].pure[F]

    def done = ().pure[F]
  }
  

  def of[F[_] : Concurrent : Timer : LogOf : KafkaConsumerOf : KafkaProducerOf : RandomIdOf : FromTry](
    config: Config,
    kafkaConfig: KafkaConfig
  ): Resource[F, KafkaHealthCheck[F]] = {

    val result = for {
      log      <- LogOf[F].apply(KafkaHealthCheck.getClass)
      randomId <- RandomIdOf[F].apply
    } yield {
      val key = randomId.value

      val consumer = Consumer.of[F](key, kafkaConfig.consumer)

      val producer = Producer.of[F](config.topic, kafkaConfig.producer)

      of(
        key = key,
        config = config,
        stop = false.pure[F],
        producer = producer,
        consumer = consumer,
        log = log)
    }

    result
      .toResource
      .flatten
  }

  def of[F[_] : Concurrent : Timer](
    key: String,
    config: Config,
    stop: F[Boolean],
    producer: Resource[F, Producer[F]],
    consumer: Resource[F, Consumer[F]],
    log: Log[F]
  ): Resource[F, KafkaHealthCheck[F]] = {

    val result = for {
      ref   <- Ref.of[F, Option[Throwable]](None)
      fiber <- (producer, consumer)
        .tupled
        .use { case (producer, consumer) => run(key, config, stop, producer, consumer, ref.set, log) }
        .start
    } yield {
      val result = new KafkaHealthCheck[F] {
        def error = ref.get
        def done = fiber.join
      }
      (result, fiber.cancel)
    }

    Resource(result)
  }

  def run[F[_] : Concurrent : Timer](
    key: String,
    config: Config,
    stop: F[Boolean],
    producer: Producer[F],
    consumer: Consumer[F],
    set: Option[Throwable] => F[Unit],
    log: Log[F]
  ): F[Unit] = {

    val sleep = Timer[F].sleep(config.interval)

    def produce(value: String) = {
      val record = Record(key = key.some, value = value.some)
      for {
        _ <- log.debug(s"$key send $value")
        _ <- producer.send(record)
      } yield {}
    }

    def produceConsume(n: Long) = {
      val value = n.toString

      def consume(retry: Long) = {
        for {
          records <- consumer.poll(config.pollTimeout)
          found    = records.find { record => record.key.contains_(key) && record.value.contains_(value) }
          result  <- found.fold {
            for {
              _ <- sleep
              _ <- produce(s"$n:$retry")
            } yield {
              (retry + 1).asLeft[Unit]
            }
          } { _ =>
            ().asRight[Long].pure[F]
          }
        } yield result
      }

      val produceConsume = for {
        _ <- produce(value)
        _ <- 0L.tailRecM(consume)
      } yield {}

      produceConsume
        .timeout(config.timeout)
        .error
    }

    def check(n: Long) = {
      for {
        error  <- produceConsume(n)
        _      <- error.fold(().pure[F]) { error => log.error(s"$n failed with $error") }
        _      <- set(error)
        _      <- sleep
        stop   <- stop
      } yield {
        if (stop) ().asRight[Long]
        else (n + 1).asLeft[Unit]
      }
    }

    for {
      _ <- Timer[F].sleep(config.initial)
      _ <- consumer.subscribe(config.topic)
      _ <- consumer.poll(config.interval)
      _ <- produceConsume(0L) // warmup
      _ <- 1L.tailRecM(check)
    } yield {}
  }


  trait Producer[F[_]] {
    def send(record: Record): F[Unit]
  }

  object Producer {

    def apply[F[_]](implicit F: Producer[F]): Producer[F] = F

    def apply[F[_] : Monad : FromTry](topic: Topic, producer: KafkaProducer[F]): Producer[F] = {
      new Producer[F] {
        def send(record: Record) = {
          val record1 = ProducerRecord[String, String](topic = topic, key = record.key, value = record.value)
          producer.send(record1).void
        }
      }
    }

    def of[F[_] : Monad : KafkaProducerOf : FromTry](topic: Topic, config: ProducerConfig): Resource[F, Producer[F]] = {
      for {
        producer <- KafkaProducerOf[F].apply(config)
      } yield {
        Producer[F](topic = topic, producer = producer)
      }
    }
  }


  trait Consumer[F[_]] {

    def subscribe(topic: Topic): F[Unit]
    
    def poll(timeout: FiniteDuration): F[Iterable[Record]]
  }

  object Consumer {

    def apply[F[_]](implicit F: Consumer[F]): Consumer[F] = F

    def apply[F[_] : Functor](consumer: KafkaConsumer[F, String, String]): Consumer[F] = {

      new Consumer[F] {

        def subscribe(topic: Topic) = {
          consumer.subscribe(topic, none)
        }

        def poll(timeout: FiniteDuration) = {
          for {
            records <- consumer.poll(timeout)
          } yield for {
            record <- records.values.values.flatMap(_.toList)
          } yield {
            Record(key = record.key.map(_.value), value = record.value.map(_.value))
          }
        }
      }
    }

    def of[F[_] : Monad : KafkaConsumerOf : FromTry](key: String, config: ConsumerConfig): Resource[F, Consumer[F]] = {
      val config1 = {
        val groupId = config.common.clientId.fold(key) { clientId => s"$clientId-$key" }
        config.copy(
          groupId = groupId.some,
          autoOffsetReset = AutoOffsetReset.Latest)
      }

      for {
        consumer <- KafkaConsumerOf[F].apply[String, String](config1)
      } yield {
        Consumer[F](consumer)
      }
    }
  }


  final case class Record(key: Option[String], value: Option[String])


  final case class Config(
    topic: Topic = "healthcheck",
    initial: FiniteDuration = 10.seconds,
    interval: FiniteDuration = 1.second,
    timeout: FiniteDuration = 2.minutes,
    pollTimeout: FiniteDuration = 10.millis)

  object Config {
    val default: Config = Config()
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy