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

com.permutive.pubsub.producer.http.internal.BatchingHttpPublisher.scala Maven / Gradle / Ivy

package com.permutive.pubsub.producer.http.internal

import cats.{Foldable, Traverse}
import cats.effect.concurrent.Deferred
import cats.effect.syntax.all._
import cats.effect.{Concurrent, Resource, Timer}
import cats.syntax.all._
import com.permutive.pubsub.producer.Model.MessageId
import com.permutive.pubsub.producer.encoder.MessageEncoder
import com.permutive.pubsub.producer.http.BatchingHttpProducerConfig
import com.permutive.pubsub.producer.{AsyncPubsubProducer, Model, PubsubProducer}
import fs2.Chunk._
import fs2.concurrent.{Dequeue, Enqueue, Queue}
import fs2.{Chunk, Stream}

private[http] class BatchingHttpPublisher[F[_]: Timer, A: MessageEncoder] private (
  queue: Enqueue[F, Model.AsyncRecord[F, A]]
)(implicit F: Concurrent[F])
    extends AsyncPubsubProducer[F, A] {

  override def produceAsync(
    record: A,
    callback: Either[Throwable, Unit] => F[Unit],
    metadata: Map[String, String],
    uniqueId: String
  ): F[Unit] =
    queue.enqueue1(Model.AsyncRecord(record, callback, metadata, uniqueId))

  override def produceManyAsync[G[_]: Foldable](records: G[Model.AsyncRecord[F, A]]): F[Unit] =
    records.traverse_(queue.enqueue1)

  override def produce(
    record: A,
    metadata: Map[String, String],
    uniqueId: String
  ): F[F[Unit]] =
    produceAsync(Model.SimpleRecord(record, metadata, uniqueId))

  override def produceMany[G[_]: Traverse](records: G[Model.SimpleRecord[A]]): F[G[F[Unit]]] =
    records.traverse(produceAsync)

  private def produceAsync(record: Model.SimpleRecord[A]): F[F[Unit]] =
    for {
      d <- Deferred[F, Either[Throwable, Unit]]
      _ <- queue.enqueue1(Model.AsyncRecord(record.value, d.complete, record.metadata, record.uniqueId))
    } yield d.get.rethrow

}

private[http] object BatchingHttpPublisher {
  def resource[F[_]: Concurrent: Timer, A: MessageEncoder](
    publisher: PubsubProducer[F, A],
    config: BatchingHttpProducerConfig
  ): Resource[F, AsyncPubsubProducer[F, A]] =
    for {
      queue <- Resource.liftF(Queue.unbounded[F, Model.AsyncRecord[F, A]])
      _     <- Resource.make(consume(publisher, config, queue).start)(_.cancel)
    } yield new BatchingHttpPublisher(queue)

  private def consume[F[_]: Concurrent: Timer, A: MessageEncoder](
    underlying: PubsubProducer[F, A],
    config: BatchingHttpProducerConfig,
    queue: Dequeue[F, Model.AsyncRecord[F, A]]
  ): F[Unit] = {
    val handler: Chunk[Model.AsyncRecord[F, A]] => F[List[MessageId]] =
      if (config.retryTimes == 0) { records =>
        underlying.produceMany[Chunk](records)
      } else { records =>
        Stream
          .retry(
            underlying.produceMany[Chunk](records),
            delay = config.retryInitialDelay,
            nextDelay = config.retryNextDelay,
            maxAttempts = config.retryTimes
          )
          .compile
          .lastOrError
      }

    queue.dequeue
      .groupWithin(config.batchSize, config.maxLatency)
      .evalMap { asyncRecords =>
        handler(asyncRecords).void.attempt
          .flatMap(etu => asyncRecords.traverse_(_.callback(etu)))
      }
      .compile
      .drain
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy