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

fs2.kafka.KafkaConsumer.scala Maven / Gradle / Ivy

There is a newer version: 3.6.0-RC1
Show newest version
/*
 * Copyright 2018-2024 OVO Energy Limited
 *
 * SPDX-License-Identifier: Apache-2.0
 */

package fs2.kafka

import java.util

import scala.annotation.nowarn
import scala.collection.immutable.SortedSet
import scala.concurrent.duration.FiniteDuration
import scala.util.matching.Regex

import cats.{Foldable, Functor, Reducible}
import cats.data.{NonEmptySet, OptionT}
import cats.effect.*
import cats.effect.implicits.*
import cats.effect.std.*
import cats.syntax.all.*
import fs2.{Chunk, Stream}
import fs2.kafka.consumer.*
import fs2.kafka.consumer.KafkaConsumeChunk.CommitNow
import fs2.kafka.instances.*
import fs2.kafka.internal.*
import fs2.kafka.internal.converters.collection.*
import fs2.kafka.internal.syntax.*
import fs2.kafka.internal.KafkaConsumerActor.*
import fs2.kafka.internal.LogEntry.{RevokedPreviousFetch, StoredFetch}

import org.apache.kafka.clients.consumer.{OffsetAndMetadata, OffsetAndTimestamp}
import org.apache.kafka.common.{Metric, MetricName, PartitionInfo, TopicPartition}

/**
  * [[KafkaConsumer]] represents a consumer of Kafka records, with the ability to `subscribe` to
  * topics, start a single top-level stream, and optionally control it via the provided [[fiber]]
  * instance.

* * The following top-level streams are provided.

* - [[stream]] provides a single stream of records, where the order of records is guaranteed per * topic-partition.
* - [[partitionedStream]] provides a stream with elements as streams that continually request * records for a single partition. Order is guaranteed per topic-partition, but all assigned * partitions will have to be processed in parallel.
* - [[partitionsMapStream]] provides a stream where each element contains a current assignment. * The current assignment is the `Map`, where keys is a `TopicPartition`, and values are * streams with records for a particular `TopicPartition`.
For the streams, records are * wrapped in [[CommittableConsumerRecord]]s which provide [[CommittableOffset]]s with the * ability to commit record offsets to Kafka. For performance reasons, offsets are usually * committed in batches using [[CommittableOffsetBatch]]. Provided `Pipe`s, like * [[commitBatchWithin]] are available for batch committing offsets. If you are not committing * offsets to Kafka, you can simply discard the [[CommittableOffset]], and only make use of the * record.

* * While it's technically possible to start more than one stream from a single [[KafkaConsumer]], * it is generally not recommended as there is no guarantee which stream will receive which * records, and there might be an overlap, in terms of duplicate records, between the two streams. * If a first stream completes, possibly with error, there's no guarantee the stream has processed * all of the records it received, and a second stream from the same [[KafkaConsumer]] might not be * able to pick up where the first one left off. Therefore, only create a single top-level stream * per [[KafkaConsumer]], and if you want to start a new stream if the first one finishes, let the * [[KafkaConsumer]] shutdown and create a new one. */ sealed abstract class KafkaConsumer[F[_], K, V] extends KafkaConsume[F, K, V] with KafkaConsumeChunk[F, K, V] with KafkaAssignment[F] with KafkaOffsetsV2[F] with KafkaSubscription[F] with KafkaTopicsV2[F] with KafkaCommit[F] with KafkaMetrics[F] with KafkaConsumerLifecycle[F] object KafkaConsumer { /** * Processes requests from the queue, if there are pending requests, otherwise waits for the next * poll.

* * In particular, any newly queued requests may wait for up to pollInterval, and for the next * poll to complete.

* * The resulting effect runs forever, until canceled. */ private def runConsumerActor[F[_], K, V]( requests: QueueSource[F, Request[F, K, V]], polls: QueueSource[F, Request.Poll[F]], actor: KafkaConsumerActor[F, K, V] )(implicit F: Async[F] ): F[Unit] = OptionT(requests.tryTake).getOrElseF(polls.take.widen).flatMap(actor.handle).foreverM[Unit] /** * Schedules polls every pollInterval to be handled by runConsumerActor. * * The polls queue is assumed bounded to provide backpressure. * * The resulting effect runs forever, until canceled. */ private def runPollScheduler[F[_], K, V]( polls: QueueSink[F, Request.Poll[F]], pollInterval: FiniteDuration )(implicit F: Temporal[F] ): F[Unit] = polls.offer(Request.poll).andWait(pollInterval).foreverM[Unit] private def startBackgroundConsumer[F[_], K, V]( requests: QueueSource[F, Request[F, K, V]], polls: Queue[F, Request.Poll[F]], actor: KafkaConsumerActor[F, K, V], pollInterval: FiniteDuration )(implicit F: Async[F] ): Resource[F, Fiber[F, Throwable, Unit]] = Resource.make { F.race( runConsumerActor(requests, polls, actor), runPollScheduler(polls, pollInterval) ) .void .start }(_.cancel.start.void) private def createKafkaConsumer[F[_], K, V]( requests: QueueSink[F, Request[F, K, V]], settings: ConsumerSettings[F, K, V], actor: KafkaConsumerActor[F, K, V], fiber: Fiber[F, Throwable, Unit], streamIdRef: Ref[F, StreamId], id: Int, withConsumer: WithConsumer[F], stopConsumingDeferred: Deferred[F, Unit] )(implicit F: Async[F], logging: Logging[F]): KafkaConsumer[F, K, V] = new KafkaConsumer[F, K, V] { override def partitionsMapStream : Stream[F, Map[TopicPartition, Stream[F, CommittableConsumerRecord[F, K, V]]]] = { val chunkQueue: F[Queue[F, Option[Chunk[CommittableConsumerRecord[F, K, V]]]]] = Queue.bounded(settings.maxPrefetchBatches - 1) type PartitionResult = (Chunk[CommittableConsumerRecord[F, K, V]], FetchCompletedReason) type PartitionsMap = Map[TopicPartition, Stream[F, CommittableConsumerRecord[F, K, V]]] type PartitionsMapQueue = Queue[F, Option[PartitionsMap]] def partitionStream( streamId: StreamId, partition: TopicPartition, assignmentRevoked: F[Unit] ): Stream[F, CommittableConsumerRecord[F, K, V]] = Stream.force { for { chunks <- chunkQueue dequeueDone <- Deferred[F, Unit] shutdown = F.race( F.race( awaitTermination.attempt, dequeueDone.get ), F.race( stopConsumingDeferred.get, assignmentRevoked ) ) .void stopReqs <- Deferred[F, Unit] } yield Stream .eval { val fetchPartition: F[Unit] = F .deferred[PartitionResult] .flatMap { deferred => val callback: PartitionResult => F[Unit] = deferred.complete(_).void val fetch: F[PartitionResult] = withPermit { val assigned = withConsumer.blocking { _.assignment.contains(partition) } def storeFetch: F[Unit] = actor .ref .modify { state => val (newState, oldFetches) = state.withFetch(partition, streamId, callback) newState -> (logging.log(StoredFetch(partition, callback, newState)) >> oldFetches.traverse_ { fetch => fetch.completeRevoked(Chunk.empty) >> logging.log(RevokedPreviousFetch(partition, streamId)) }) } .flatten def completeRevoked: F[Unit] = callback((Chunk.empty, FetchCompletedReason.TopicPartitionRevoked)) assigned.ifM(storeFetch, completeRevoked) } >> deferred.get fetch.flatMap { case (chunk, reason) => val enqueueChunk = chunks.offer(Some(chunk)).unlessA(chunk.isEmpty) val completeRevoked = stopReqs.complete(()).void.whenA(reason.topicPartitionRevoked) enqueueChunk >> completeRevoked } } Stream .repeatEval { stopReqs .tryGet .flatMap { case None => fetchPartition case Some(()) => // Prevent issuing additional requests after partition is // revoked or shutdown happens, in case the stream isn't // interrupted fast enough F.unit } } .interruptWhen(F.race(shutdown, stopReqs.get).void.attempt) .compile .drain .guarantee(F.race(dequeueDone.get, chunks.offer(None)).void) .start .as { Stream .fromQueueNoneTerminated(chunks) .flatMap(Stream.chunk) .covary[F] .onFinalize(dequeueDone.complete(()).void) } } .flatten } def enqueueAssignment( streamId: StreamId, assigned: Map[TopicPartition, Deferred[F, Unit]], partitionsMapQueue: PartitionsMapQueue ): F[Unit] = stopConsumingDeferred .tryGet .flatMap { case None => val assignment: PartitionsMap = assigned.map { case (partition, finisher) => partition -> partitionStream(streamId, partition, finisher.get) } partitionsMapQueue.offer(Some(assignment)) case Some(()) => F.unit } def onRebalance( streamId: StreamId, assignmentRef: Ref[F, Map[TopicPartition, Deferred[F, Unit]]], partitionsMapQueue: PartitionsMapQueue ): OnRebalance[F] = OnRebalance( onRevoked = revoked => { for { finishers <- assignmentRef.modify(_.partition(entry => !revoked.contains(entry._1))) _ <- finishers.toVector.traverse { case (_, finisher) => finisher.complete(()) } } yield () }, onAssigned = assignedPartitions => { for { assignment <- assignedPartitions .toVector .traverse { partition => Deferred[F, Unit].map(partition -> _) } .map(_.toMap) _ <- assignmentRef.update(_ ++ assignment) _ <- enqueueAssignment( streamId = streamId, assigned = assignment, partitionsMapQueue = partitionsMapQueue ) } yield () } ) def requestAssignment( streamId: StreamId, assignmentRef: Ref[F, Map[TopicPartition, Deferred[F, Unit]]], partitionsMapQueue: PartitionsMapQueue ): F[Map[TopicPartition, Deferred[F, Unit]]] = { val assignment = this.assignment( Some( onRebalance( streamId, assignmentRef, partitionsMapQueue ) ) ) F.race(awaitTermination.attempt, assignment) .flatMap { case Left(_) => F.pure(Map.empty) case Right(assigned) => assigned .toVector .traverse { partition => Deferred[F, Unit].map(partition -> _) } .map(_.toMap) } } def initialEnqueue( streamId: StreamId, assignmentRef: Ref[F, Map[TopicPartition, Deferred[F, Unit]]], partitionsMapQueue: PartitionsMapQueue ): F[Unit] = for { assigned <- requestAssignment( streamId, assignmentRef, partitionsMapQueue ) _ <- enqueueAssignment(streamId, assigned, partitionsMapQueue) } yield () Stream .eval(stopConsumingDeferred.tryGet) .flatMap { case None => for { partitionsMapQueue <- Stream.eval(Queue.unbounded[F, Option[PartitionsMap]]) streamId <- Stream.eval(streamIdRef.modify(n => (n + 1, n))) assignmentRef <- Stream .eval(Ref[F].of(Map.empty[TopicPartition, Deferred[F, Unit]])) _ <- Stream.eval( initialEnqueue( streamId, assignmentRef, partitionsMapQueue ) ) out <- Stream .fromQueueNoneTerminated(partitionsMapQueue) .interruptWhen(awaitTermination.attempt) .concurrently( Stream.eval(stopConsumingDeferred.get >> partitionsMapQueue.offer(None)) ) } yield out case Some(()) => Stream.empty.covaryAll[F, PartitionsMap] } } override def partitionedStream: Stream[F, Stream[F, CommittableConsumerRecord[F, K, V]]] = partitionsMapStream.flatMap(partitionsMap => Stream.iterable(partitionsMap.values)) override def stream: Stream[F, CommittableConsumerRecord[F, K, V]] = partitionedStream.parJoinUnbounded override def commitAsync(offsets: Map[TopicPartition, OffsetAndMetadata]): F[Unit] = request { callback => Request.ManualCommitAsync( callback = callback, offsets = offsets ) } override def commitSync(offsets: Map[TopicPartition, OffsetAndMetadata]): F[Unit] = request { callback => Request.ManualCommitSync( callback = callback, offsets = offsets ) } private[this] def request[A]( request: (Either[Throwable, A] => F[Unit]) => Request[F, K, V] ): F[A] = Deferred[F, Either[Throwable, A]] .flatMap { deferred => requests.offer(request(deferred.complete(_).void)) >> F.race(awaitTermination.as(ConsumerShutdownException()), deferred.get.rethrow) } .rethrow override def assignment: F[SortedSet[TopicPartition]] = assignment(Option.empty) private def assignment( onRebalance: Option[OnRebalance[F]] ): F[SortedSet[TopicPartition]] = withPermit { onRebalance .fold(actor.ref.updateAndGet(_.asStreaming)) { on => actor .ref .updateAndGet(_.withOnRebalance(on).asStreaming) .flatTap { newState => logging.log(LogEntry.StoredOnRebalance(on, newState)) } } .ensure(NotSubscribedException())(_.subscribed) >> withConsumer.blocking(_.assignment.toSortedSet) } override def assignmentStream: Stream[F, SortedSet[TopicPartition]] = { // NOTE: `initialAssignmentDone` is needed here to guard against the // race condition when a rebalance triggers after the listeners are // registered but before `assignmentRef` can be updated with initial // assignments. def onRebalanceWith( updateQueue: Queue[F, SortedSet[TopicPartition]], assignmentRef: Ref[F, SortedSet[TopicPartition]], initialAssignmentDone: F[Unit] ): OnRebalance[F] = OnRebalance( onAssigned = assigned => initialAssignmentDone >> assignmentRef.updateAndGet(_ ++ assigned).flatMap(updateQueue.offer), onRevoked = revoked => initialAssignmentDone >> assignmentRef.updateAndGet(_ -- revoked).flatMap(updateQueue.offer) ) Stream .eval { ( Queue.unbounded[F, SortedSet[TopicPartition]], Ref[F].of(SortedSet.empty[TopicPartition]), Deferred[F, Unit] ).tupled .flatMap[Stream[F, SortedSet[TopicPartition]]] { case (updateQueue, assignmentRef, initialAssignmentDeferred) => val onRebalance = onRebalanceWith( updateQueue = updateQueue, assignmentRef = assignmentRef, initialAssignmentDone = initialAssignmentDeferred.get ) assignment(Some(onRebalance)) .flatMap { initialAssignment => assignmentRef.set(initialAssignment) >> updateQueue.offer(initialAssignment) >> initialAssignmentDeferred.complete(()) } .as(Stream.fromQueueUnterminated(updateQueue).changes) } } .flatten } override def seek(partition: TopicPartition, offset: Long): F[Unit] = withConsumer.blocking(_.seek(partition, offset)) override def seekToBeginning[G[_]](partitions: G[TopicPartition])(implicit G: Foldable[G] ): F[Unit] = withConsumer.blocking(_.seekToBeginning(partitions.asJava)) override def seekToEnd[G[_]]( partitions: G[TopicPartition] )(implicit G: Foldable[G]): F[Unit] = withConsumer.blocking(_.seekToEnd(partitions.asJava)) override def partitionsFor( topic: String ): F[List[PartitionInfo]] = withConsumer.blocking(_.partitionsFor(topic).asScala.toList) override def partitionsFor( topic: String, timeout: FiniteDuration ): F[List[PartitionInfo]] = withConsumer.blocking(_.partitionsFor(topic, timeout.toJava).asScala.toList) override def position(partition: TopicPartition): F[Long] = withConsumer.blocking(_.position(partition)) override def position(partition: TopicPartition, timeout: FiniteDuration): F[Long] = withConsumer.blocking(_.position(partition, timeout.toJava)) override def committed( partitions: Set[TopicPartition] ): F[Map[TopicPartition, OffsetAndMetadata]] = withConsumer.blocking { _.committed(partitions.asJava) .asInstanceOf[util.Map[TopicPartition, OffsetAndMetadata]] .toMap } override def committed( partitions: Set[TopicPartition], timeout: FiniteDuration ): F[Map[TopicPartition, OffsetAndMetadata]] = withConsumer.blocking { _.committed(partitions.asJava, timeout.toJava) .asInstanceOf[util.Map[TopicPartition, OffsetAndMetadata]] .toMap } override def subscribe[G[_]](topics: G[String])(implicit G: Reducible[G]): F[Unit] = withPermit { withConsumer.blocking { _.subscribe( topics.toList.asJava, actor.consumerRebalanceListener ) } >> actor .ref .updateAndGet(_.asSubscribed) .log(LogEntry.SubscribedTopics(topics.toNonEmptyList, _)) } private def withPermit[A](fa: F[A]): F[A] = F.deferred[Either[Throwable, A]] .flatMap { deferred => requests.offer( Request.WithPermit(fa, deferred.complete(_: Either[Throwable, A]).void) ) >> deferred.get.rethrow } override def subscribe(regex: Regex): F[Unit] = withPermit { withConsumer.blocking { _.subscribe( regex.pattern, actor.consumerRebalanceListener ) } >> actor.ref.updateAndGet(_.asSubscribed).log(LogEntry.SubscribedPattern(regex.pattern, _)) } override def unsubscribe: F[Unit] = withPermit { withConsumer.blocking(_.unsubscribe()) >> actor .ref .updateAndGet(_.asUnsubscribed) .log(LogEntry.Unsubscribed(_)) } override def stopConsuming: F[Unit] = stopConsumingDeferred.complete(()).attempt.void override def assign(partitions: NonEmptySet[TopicPartition]): F[Unit] = withPermit { withConsumer.blocking { _.assign( partitions.toList.asJava ) } >> actor .ref .updateAndGet(_.asSubscribed) .log(LogEntry.ManuallyAssignedPartitions(partitions, _)) } override def assign(topic: String): F[Unit] = for { partitions <- partitionsFor(topic).map { partitionInfo => NonEmptySet.fromSet { SortedSet(partitionInfo.map(_.partition)*) } } _ <- partitions.fold(F.unit)(assign(topic, _)) } yield () override def beginningOffsets( partitions: Set[TopicPartition] ): F[Map[TopicPartition, Long]] = withConsumer.blocking { _.beginningOffsets(partitions.asJava).asInstanceOf[util.Map[TopicPartition, Long]].toMap } override def beginningOffsets( partitions: Set[TopicPartition], timeout: FiniteDuration ): F[Map[TopicPartition, Long]] = withConsumer.blocking { _.beginningOffsets(partitions.asJava, timeout.toJava) .asInstanceOf[util.Map[TopicPartition, Long]] .toMap } override def endOffsets( partitions: Set[TopicPartition] ): F[Map[TopicPartition, Long]] = withConsumer.blocking { _.endOffsets(partitions.asJava).asInstanceOf[util.Map[TopicPartition, Long]].toMap } override def endOffsets( partitions: Set[TopicPartition], timeout: FiniteDuration ): F[Map[TopicPartition, Long]] = withConsumer.blocking { _.endOffsets(partitions.asJava, timeout.toJava) .asInstanceOf[util.Map[TopicPartition, Long]] .toMap } override def offsetsForTimes( timestampsToSearch: Map[TopicPartition, Long] ): F[Map[TopicPartition, Option[OffsetAndTimestamp]]] = withConsumer.blocking { _.offsetsForTimes( timestampsToSearch.asJava.asInstanceOf[util.Map[TopicPartition, java.lang.Long]] ) // Convert empty/missing partition null values to None for more idiomatic scala .toMapOptionValues } override def offsetsForTimes( timestampsToSearch: Map[TopicPartition, Long], timeout: FiniteDuration ): F[Map[TopicPartition, Option[OffsetAndTimestamp]]] = withConsumer.blocking { _.offsetsForTimes( timestampsToSearch.asJava.asInstanceOf[util.Map[TopicPartition, java.lang.Long]], timeout.toJava ) // Convert empty/missing partition null values to None for more idiomatic scala .toMapOptionValues } override def listTopics: F[Map[String, List[PartitionInfo]]] = withConsumer.blocking { _.listTopics().toMap.map { case (k, v) => (k, v.toList) } } override def listTopics(timeout: FiniteDuration): F[Map[String, List[PartitionInfo]]] = withConsumer.blocking { _.listTopics(timeout.toJava).toMap.map { case (k, v) => (k, v.toList) } } override def metrics: F[Map[MetricName, Metric]] = withConsumer.blocking(_.metrics().asScala.toMap) override def toString: String = "KafkaConsumer$" + id override def terminate: F[Unit] = fiber.cancel.start.void override def awaitTermination: F[Unit] = fiber.joinWithUnit } /** * Creates a new [[KafkaConsumer]] in the `Resource` context, using the specified * [[ConsumerSettings]]. Note that there is another version where `F[_]` is specified explicitly * and the key and value type can be inferred, which allows you to use the following syntax. * * {{{ * KafkaConsumer.resource[F].using(settings) * }}} */ def resource[F[_], K, V]( settings: ConsumerSettings[F, K, V] )(implicit F: Async[F], mk: MkConsumer[F] ): Resource[F, KafkaConsumer[F, K, V]] = for { keyDeserializer <- settings.keyDeserializer valueDeserializer <- settings.valueDeserializer id <- Resource.eval(F.delay(new Object().hashCode)) jitter <- Resource.eval(Jitter.default[F]) logging <- Resource.eval(Logging.default[F](id)) requests <- Resource.eval(Queue.unbounded[F, Request[F, K, V]]) polls <- Resource.eval(Queue.bounded[F, Request.Poll[F]](1)) ref <- Resource.eval(Ref.of[F, State[F, K, V]](State.empty)) streamId <- Resource.eval(Ref.of[F, StreamId](0)) dispatcher <- Dispatcher.sequential[F] stopConsumingDeferred <- Resource.eval(Deferred[F, Unit]) withConsumer <- WithConsumer(mk, settings) actor = { implicit val jitter0: Jitter[F] = jitter implicit val logging0: Logging[F] = logging implicit val dispatcher0: Dispatcher[F] = dispatcher new KafkaConsumerActor( settings = settings, keyDeserializer = keyDeserializer, valueDeserializer = valueDeserializer, ref = ref, requests = requests, withConsumer = withConsumer ) } fiber <- startBackgroundConsumer(requests, polls, actor, settings.pollInterval) } yield createKafkaConsumer( requests, settings, actor, fiber, streamId, id, withConsumer, stopConsumingDeferred )(F, logging) /** * Creates a new [[KafkaConsumer]] in the `Stream` context, using the specified * [[ConsumerSettings]]. Note that there is another version where `F[_]` is specified explicitly * and the key and value type can be inferred, which allows you to use the following syntax. * * {{{ * KafkaConsumer.stream[F].using(settings) * }}} */ def stream[F[_], K, V]( settings: ConsumerSettings[F, K, V] )(implicit F: Async[F], mk: MkConsumer[F]): Stream[F, KafkaConsumer[F, K, V]] = Stream.resource(resource(settings)(F, mk)) def apply[F[_]]: ConsumerPartiallyApplied[F] = new ConsumerPartiallyApplied() final private[kafka] class ConsumerPartiallyApplied[F[_]](val dummy: Boolean = true) extends AnyVal { /** * Alternative version of `resource` where the `F[_]` is specified explicitly, and where the * key and value type can be inferred from the [[ConsumerSettings]]. This allows you to use the * following syntax. * * {{{ * KafkaConsumer[F].resource(settings) * }}} */ def resource[K, V](settings: ConsumerSettings[F, K, V])(implicit F: Async[F], mk: MkConsumer[F] ): Resource[F, KafkaConsumer[F, K, V]] = KafkaConsumer.resource(settings)(F, mk) /** * Alternative version of `stream` where the `F[_]` is specified explicitly, and where the key * and value type can be inferred from the [[ConsumerSettings]]. This allows you to use the * following syntax. * * {{{ * KafkaConsumer[F].stream(settings) * }}} */ def stream[K, V](settings: ConsumerSettings[F, K, V])(implicit F: Async[F], mk: MkConsumer[F] ): Stream[F, KafkaConsumer[F, K, V]] = KafkaConsumer.stream(settings)(F, mk) override def toString: String = "ConsumerPartiallyApplied$" + System.identityHashCode(this) } /* * Extension methods for operating on a `KafkaConsumer` in a `Stream` context without needing * to explicitly use operations such as `flatMap` and `evalTap` */ implicit final class StreamOps[F[_]: Functor, K, V](self: Stream[F, KafkaConsumer[F, K, V]]) { /** * Subscribes a consumer to the specified topics within the [[Stream]] context. See * [[KafkaSubscription#subscribe]]. */ def subscribe[G[_]: Reducible](topics: G[String]): Stream[F, KafkaConsumer[F, K, V]] = self.evalTap(_.subscribe(topics)) def subscribe(regex: Regex): Stream[F, KafkaConsumer[F, K, V]] = self.evalTap(_.subscribe(regex)) /** * Subscribes a consumer to the specified topics within the [[Stream]] context. See * [[KafkaSubscription#subscribe]]. */ def subscribeTo( firstTopic: String, remainingTopics: String* ): Stream[F, KafkaConsumer[F, K, V]] = self.evalTap(_.subscribeTo(firstTopic, remainingTopics*)) /** * A [[Stream]] of records from the allocated [[KafkaConsumer]]. Alias for [[stream]]. See * [[KafkaConsume#stream]] */ def records: Stream[F, CommittableConsumerRecord[F, K, V]] = stream /** * A [[Stream]] of records from the allocated [[KafkaConsumer]]. See [[KafkaConsume#stream]] */ def stream: Stream[F, CommittableConsumerRecord[F, K, V]] = self.flatMap(_.records) /** * Alias for [[partitionedStream]]. See [[KafkaConsume#partitionedStream]] */ def partitionedRecords: Stream[F, Stream[F, CommittableConsumerRecord[F, K, V]]] = partitionedStream /** * See [[KafkaConsume#partitionedStream]] */ def partitionedStream: Stream[F, Stream[F, CommittableConsumerRecord[F, K, V]]] = self.flatMap(_.partitionedRecords) /** * Consume from all assigned partitions concurrently, processing the messages in `Chunk`s. See * [[KafkaConsumeChunk#consumeChunk]] */ def consumeChunk(processor: Chunk[ConsumerRecord[K, V]] => F[CommitNow])(implicit F: Concurrent[F] ): F[Nothing] = self.evalMap(_.consumeChunk(processor)).compile.onlyOrError } /* * Prevents the default `MkConsumer` instance from being implicitly available * to code defined in this object, ensuring factory methods require an instance * to be provided at the call site. */ @nowarn("msg=never used") implicit private def mkAmbig1[F[_]]: MkConsumer[F] = throw new AssertionError("should not be used") @nowarn("msg=never used") implicit private def mkAmbig2[F[_]]: MkConsumer[F] = throw new AssertionError("should not be used") }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy