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

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

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

import cats.data.{NonEmptyMap => Nem, NonEmptySet => Nes}
import cats.effect._
import cats.syntax.all._
import cats.{Applicative, Monad, ~>}
import com.evolutiongaming.kafka.journal.util.Named
import com.evolutiongaming.skafka._
import com.evolutiongaming.skafka.consumer.{Consumer, ConsumerRecords, RebalanceListener}

import scala.concurrent.duration.FiniteDuration
import scala.util.control.NoStackTrace

trait KafkaConsumer[F[_], K, V] {

  def assign(partitions: Nes[TopicPartition]): F[Unit]

  def seek(partition: TopicPartition, offset: Offset): F[Unit]

  def subscribe(topic: Topic, listener: Option[RebalanceListener[F]]): F[Unit]

  def poll(timeout: FiniteDuration): F[ConsumerRecords[K, V]]

  def commit(offsets: Nem[TopicPartition, OffsetAndMetadata]): F[Unit]

  def topics: F[Set[Topic]]

  def partitions(topic: Topic): F[Set[Partition]]

  def assignment: F[Set[TopicPartition]]
}

object KafkaConsumer {

  def apply[F[_], K, V](implicit F: KafkaConsumer[F, K, V]): KafkaConsumer[F, K, V] = F


  def of[F[_] : Sync, K, V](
    consumer: Resource[F, Consumer[F, K, V]]
  ): Resource[F, KafkaConsumer[F, K, V]] = {

    val result = for {
      result <- consumer.allocated
    } yield {
      val (consumer, close0) = result

      val toError = new Named[F] {
        def apply[A](fa: F[A], method: String) = {
          fa.adaptError { case e => KafkaConsumerError(s"consumer.$method", e) }
        }
      }

      val close = toError(close0, "close")

      val kafkaConsumer = apply[F, K, V](consumer).mapMethod(toError)

      (kafkaConsumer, close)
    }
    Resource(result)
  }


  def apply[F[_] : Applicative, K, V](consumer: Consumer[F, K, V]): KafkaConsumer[F, K, V] = {
    new KafkaConsumer[F, K, V] {

      def assign(partitions: Nes[TopicPartition]) = {
        consumer.assign(partitions)
      }

      def seek(partition: TopicPartition, offset: Offset) = {
        consumer.seek(partition, offset)
      }

      def subscribe(topic: Topic, listener: Option[RebalanceListener[F]]) = {
        consumer.subscribe(Nes.of(topic), listener)
      }

      def poll(timeout: FiniteDuration) = {
        consumer.poll(timeout)
      }

      def commit(offsets: Nem[TopicPartition, OffsetAndMetadata]) = {
        consumer.commit(offsets)
      }

      def topics: F[Set[Topic]] = {
        for {
          infos <- consumer.topics
        } yield {
          infos.keySet
        }
      }

      def partitions(topic: Topic) = {
        consumer
          .partitions(topic)
          .map { infos =>
            infos
              .map { _.partition }
              .toSet
          }
      }

      def assignment = {
        consumer.assignment
      }
    }
  }


  implicit class KafkaConsumerOps[F[_], K, V](val self: KafkaConsumer[F, K, V]) extends AnyVal {

    def mapK[G[_]](fg: F ~> G, gf: G ~> F): KafkaConsumer[G, K, V] = new KafkaConsumer[G, K, V] {

      def assign(partitions: Nes[TopicPartition]) = fg(self.assign(partitions))

      def seek(partition: TopicPartition, offset: Offset) = fg(self.seek(partition, offset))

      def subscribe(topic: Topic, listener: Option[RebalanceListener[G]]) = {
        val listener1 = listener.map(_.mapK(gf))
        fg(self.subscribe(topic, listener1))
      }

      def poll(timeout: FiniteDuration) = fg(self.poll(timeout))

      def commit(offsets: Nem[TopicPartition, OffsetAndMetadata]) = fg(self.commit(offsets))

      def topics = fg(self.topics)

      def partitions(topic: Topic) = fg(self.partitions(topic))

      def assignment = fg(self.assignment)
    }


    def mapMethod(f: Named[F]): KafkaConsumer[F, K, V] = new KafkaConsumer[F, K, V] {

      def assign(partitions: Nes[TopicPartition]) = f(self.assign(partitions), "assign")

      def seek(partition: TopicPartition, offset: Offset) = f(self.seek(partition, offset), "seek")

      def subscribe(topic: Topic, listener: Option[RebalanceListener[F]]) = {
        f(self.subscribe(topic, listener), "subscribe")
      }

      def poll(timeout: FiniteDuration) = f(self.poll(timeout), "poll")

      def commit(offsets: Nem[TopicPartition, OffsetAndMetadata]) = f(self.commit(offsets), "commit")

      def topics = f(self.topics, "topics")

      def partitions(topic: Topic) = f(self.partitions(topic), "partitions")

      def assignment = f(self.assignment, "assignment")
    }


    implicit def withShiftPoll(implicit F: Monad[F], contextShift: ContextShift[F]): KafkaConsumer[F, K, V] = {

      new KafkaConsumer[F, K, V] {

        def assign(partitions: Nes[TopicPartition]) = self.assign(partitions)

        def seek(partition: TopicPartition, offset: Offset) = self.seek(partition, offset)

        def subscribe(topic: Topic, listener: Option[RebalanceListener[F]]) = {
          self.subscribe(topic, listener)
        }

        def poll(timeout: FiniteDuration) = {
          for {
            a <- self.poll(timeout)
            _ <- if (a.values.isEmpty) contextShift.shift else ().pure[F]
          } yield a
        }

        def commit(offsets: Nem[TopicPartition, OffsetAndMetadata]) = self.commit(offsets)

        def topics = self.topics

        def partitions(topic: Topic) = self.partitions(topic)

        def assignment = self.assignment
      }
    }
  }
}


final case class KafkaConsumerError(
  message: String,
  cause: Throwable) extends RuntimeException(message, cause) with NoStackTrace




© 2015 - 2024 Weber Informatics LLC | Privacy Policy