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

backbone.scaladsl.Backbone.scala Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (c) 2025 Backbone contributors
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
 * this software and associated documentation files (the "Software"), to deal in
 * the Software without restriction, including without limitation the rights to
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
 * the Software, and to permit persons to whom the Software is furnished to do so,
 * subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
 * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
 * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

package backbone.scaladsl

import akka.Done
import akka.actor.{ActorRef, ActorSystem}
import akka.stream.OverflowStrategy
import akka.stream.scaladsl.Sink
import backbone.aws.{AmazonSnsOps, AmazonSqsOps, CreateQueueParams}
import backbone.consumer.ConsumerSettings
import backbone.consumer.scaladsl.Consumer
import backbone.publisher.PublisherSettings
import backbone.publisher.scaladsl.Publisher
import backbone.scaladsl.Backbone.QueueInformation
import backbone.{MessageReader, MessageWriter, ProcessingResult, consumer, publisher}
import org.slf4j.LoggerFactory
import software.amazon.awssdk.services.sns.SnsAsyncClient
import software.amazon.awssdk.services.sqs.SqsAsyncClient

import scala.concurrent.{ExecutionContext, Future}
import scala.util.{Failure, Success}

object Backbone {

  final case class QueueInformation(url: String, arn: String)

  def apply()(implicit sqs: SqsAsyncClient, sns: SnsAsyncClient, system: ActorSystem): Backbone = new Backbone()

}

/**
 * Subscribing to certain kinds of messages from various SNS topics and consume them via a Amazon SQS queue, and publish
 * messages to an Amazon SNS topic.
 *
 * @param sqs
 *   implicit aws sqs async client
 * @param sns
 *   implicit aws sns async client
 * @param system
 *   implicit actor system
 */
class Backbone(implicit val sqs: SqsAsyncClient, val sns: SnsAsyncClient, system: ActorSystem)
    extends AmazonSqsOps
    with AmazonSnsOps {

  private[this] val logger = LoggerFactory.getLogger(getClass)

  /**
   * Consume messages of type T until an optional condition in ConsumerSettings is met.
   *
   * Creates a queue with the name provided in settings if it does not already exist. Subscribes the queue to all
   * provided topics and modifies the AWS Policy to allow sending messages to the queue from the topics.
   *
   * @param settings
   *   ConsumerSettings configuring Backbone
   * @param f
   *   function which processes messages of type T and returns a ProcessingResult
   * @param fo
   *   Format[T] typeclass instance describing how to decode SQS Message to T
   * @tparam T
   *   type of message to consume
   * @return
   *   a future completing when the stream quits
   */
  def consume[T](settings: ConsumerSettings)(f: T => ProcessingResult)(implicit fo: MessageReader[T]): Future[Done] = {
    consumeAsync[T](settings)(f.andThen(Future.successful))
  }

  /**
   * Consume messages of type T including MessageHeaders until an optional condition in ConsumerSettings is met.
   *
   * Creates a queue with the name provided in settings if it does not already exist. Subscribes the queue to all
   * provided topics and modifies the AWS Policy to allow sending messages to the queue from the topics.
   *
   * @param settings
   *   ConsumerSettings configuring Backbone
   * @param f
   *   function which processes messages of type T and returns a ProcessingResult
   * @param fo
   *   Format[T] typeclass instance describing how to decode SQS Message to T
   * @tparam T
   *   type of message to consume
   * @return
   *   a future completing when the stream quits
   */
  def consumeWithHeaders[T](settings: ConsumerSettings)(f: (T, consumer.MessageHeaders) => ProcessingResult)(implicit
      fo: MessageReader[T]): Future[Done] = {
    consumeWithHeadersAsync[T](settings) { case (t, h) => Future.successful(f(t, h)) }
  }

  /**
   * Consume messages of type T until an optional condition in ConsumerSettings is met.
   *
   * Creates a queue with the name provided in settings if it does not already exist. Subscribes the queue to all
   * provided topics and modifies the AWS Policy to allow sending messages to the queue from the topics.
   *
   * @param settings
   *   ConsumerSettings configuring Backbone
   * @param f
   *   function which processes messages of type T and returns a Future[ProcessingResult]
   * @param fo
   *   Format[T] typeclass instance describing how to decode SQS Message to T
   * @tparam T
   *   type of message to consume
   * @return
   *   a future completing when the stream quits
   */
  def consumeAsync[T](
      settings: ConsumerSettings
  )(f: T => Future[ProcessingResult])(implicit fo: MessageReader[T]): Future[Done] = {
    consumeWithHeadersAsync(settings)((t: T, _) => f(t))
  }

  /**
   * Consume messages of type T until an optional condition in ConsumerSettings is met.
   *
   * Creates a queue with the name provided in settings if it does not already exist. Subscribes the queue to all
   * provided topics and modifies the AWS Policy to allow sending messages to the queue from the topics.
   *
   * @param settings
   *   ConsumerSettings configuring Backbone
   * @param f
   *   function which processes messages of type T and returns a Future[ProcessingResult]
   * @param fo
   *   Format[T] typeclass instance describing how to decode SQS Message to T
   * @tparam T
   *   type of message to consume
   * @return
   *   a future completing when the stream quits
   */
  def consumeWithHeadersAsync[T](
      settings: ConsumerSettings
  )(f: (T, consumer.MessageHeaders) => Future[ProcessingResult])(implicit fo: MessageReader[T]): Future[Done] = {
    implicit val ec = system.dispatcher

    logger.debug(s"Preparing to consume messages. config=$settings")

    val subscription = for {
      queue <- createQueue(CreateQueueParams(settings.queue, settings.kmsKeyAlias))
      _     <- subscribe(queue, settings.topics)
    } yield queue

    subscription.onComplete {
      case Success(q) => logger.debug(s"Successfully created and subscribed queue. $q")
      case Failure(t) => logger.error(s"Subscribing to the topics failed.", t)
    }

    val result = for {
      queue <- subscription
      set = consumer.Settings(queue.url, settings.parallelism, settings.consumeWithin, settings.receiveSettings)
      r <- Consumer().consumeWithHeadersAsync[T](set)(f)
    } yield r

    result.onComplete {
      case Success(_) => logger.info("Backbone consumer stream finished successfully.")
      case Failure(t) => logger.error("Backbone consumer stream finished with an error.", t)
    }

    result
  }

  /**
   * Publish a single message of type T to an AWS SNS topic.
   *
   * @param message
   *   the message to publish
   * @param settings
   *   PublisherSettings configuring Backbone
   * @param mw
   *   typeclass instance describing how to write the message to a String
   * @tparam T
   *   type of message to publish
   * @return
   *   a future completing when the stream quits
   */
  def publishAsync[T](message: T, settings: PublisherSettings)(implicit mw: MessageWriter[T]): Future[Done] = {
    val publisherSettings = publisher.Settings(settings.topicArn)
    Publisher().publishAsync[T](publisherSettings)(message)
  }

  /**
   * Publish a single message of type T including MessageHeaders to an AWS SNS topic.
   *
   * @param message
   *   the message to publish
   * @param settings
   *   PublisherSettings configuring Backbone
   * @param mw
   *   typeclass instance describing how to write the message to a String
   * @tparam T
   *   type of message to publish
   * @return
   *   a future completing when the stream quits
   */
  def publishWithHeadersAsync[T](message: T, headers: publisher.MessageHeaders, settings: PublisherSettings)(implicit
      mw: MessageWriter[T]): Future[Done] = {
    val publisherSettings = publisher.Settings(settings.topicArn)
    Publisher().publishWithHeadersAsync[T](publisherSettings)(message -> headers)
  }

  /**
   * Publish a list of messages of type T to an AWS SNS topic.
   *
   * @param messages
   *   the messages to publish
   * @param settings
   *   PublisherSettings configuring Backbone
   * @param mw
   *   typeclass instance describing how to write a single message to a String
   * @tparam T
   *   type of messages to publish
   * @return
   *   a future completing when the stream quits
   */
  def publishAsync[T](messages: List[T], settings: PublisherSettings)(implicit mw: MessageWriter[T]): Future[Done] = {
    val publisherSettings = publisher.Settings(settings.topicArn)
    Publisher().publishAsync[T](publisherSettings)(messages: _*)
  }

  /**
   * An actor reference that publishes received messages of type T to an AWS SNS topic.
   *
   * @param settings
   *   PublisherSettings configuring Backbone
   * @param bufferSize
   *   size of the buffer
   * @param overflowStrategy
   *   strategy to use if the buffer is full
   * @param mw
   *   typeclass instance describing how to write a single message to a String
   * @tparam T
   *   type of messages to publish
   * @return
   *   an ActorRef that publishes received messages
   */
  def actorPublisher[T](
      settings: PublisherSettings,
      bufferSize: Int = Int.MaxValue,
      overflowStrategy: OverflowStrategy = OverflowStrategy.dropHead
  )(implicit mw: MessageWriter[T]): ActorRef = {
    val publisherSettings = publisher.Settings(settings.topicArn)
    Publisher().actor[T](publisherSettings)(bufferSize, overflowStrategy)
  }

  /**
   * Returns a sink that publishes received messages of type T to an AWS SNS topic.
   *
   * @param settings
   *   PublisherSettings configuring Backbone
   * @param mw
   *   typeclass instance describing how to write a single message to a String
   * @tparam T
   *   type of messages to publish
   * @return
   *   a Sink that publishes received messages
   */
  def publisherSink[T](settings: PublisherSettings)(implicit mw: MessageWriter[T]): Sink[T, Future[Done]] = {
    val publisherSettings = publisher.Settings(settings.topicArn)
    Publisher().sink[T](publisherSettings)
  }

  /**
   * Returns a sink that publishes received messages of type T including message headers to an AWS SNS topic.
   *
   * @param settings
   *   PublisherSettings configuring Backbone
   * @param mw
   *   typeclass instance describing how to write a single message to a String
   * @tparam T
   *   type of messages to publish
   * @return
   *   a Sink that publishes received messages
   */
  def publisherSinkWithHeaders[T](settings: PublisherSettings)(implicit
      mw: MessageWriter[T]): Sink[(T, publisher.MessageHeaders), Future[Done]] = {
    val publisherSettings = publisher.Settings(settings.topicArn)
    Publisher().sinkWithHeaders[T](publisherSettings)
  }

  private[this] def subscribe(queue: QueueInformation, topics: List[String])(implicit
      ec: ExecutionContext
  ): Future[Unit] = {

    logger.info(s"Subscribing queue to topics. queueArn=${queue.arn}, topicArns=$topics")
    for {
      _ <- updatePolicy(queue, topics)
      _ <- Future.sequence(topics.map(t => subscribe(queue, t)))
    } yield ()
  }

  private[this] def updatePolicy(queue: QueueInformation, topics: List[String])(implicit
      ec: ExecutionContext
  ): Future[Unit] = {
    topics match {
      case Nil => Future.successful(())
      case ts =>
        val policy = createPolicy(queue.arn, ts)
        logger.debug(s"Saving new policy for queue. queueArn=${queue.arn}, policy=$policy")
        savePolicy(queue.url, policy)
    }
  }

  private[this] def createPolicy(queueArn: String, topicsArns: Seq[String]): String = {
    val statements = topicsArns.map { topicArn =>
      s"""
         |{
         |  "Sid": "topic-subscription-arn:aws:$topicArn",
         |  "Effect": "Allow",
         |  "Principal": {
         |    "AWS": "*"
         |  },
         |  "Action": "sqs:SendMessage",
         |  "Resource": "$queueArn",
         |  "Condition": {
         |    "ArnLike": {
         |      "aws:SourceArn": "$topicArn"
         |    }
         |  }
         |}
         |""".stripMargin
    }

    val statementsJsonString = statements.mkString(",")
    s"""{ "Version": "2012-10-17", "Statement": [$statementsJsonString] }"""
  }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy