├── project ├── build.properties ├── plugins.sbt ├── Settings.scala └── Dependencies.scala ├── docker-compose.yml ├── docs ├── package.json ├── sidebars.js └── index.md ├── renovate.json ├── .github ├── workflows │ ├── release-drafter.yml │ ├── auto-approve.yml │ └── ci.yml └── release-drafter.yml ├── zio-amqp └── src │ ├── test │ ├── resources │ │ └── logback.xml │ └── scala │ │ └── zio │ │ └── amqp │ │ └── AmqpClientSpec.scala │ └── main │ └── scala │ └── zio │ └── amqp │ ├── model │ ├── package.scala │ ├── ExchangeType.scala │ └── AMQPConfig.scala │ └── Client.scala ├── .scalafmt.conf ├── .scalafix.conf ├── .mergify.yml ├── .gitignore ├── README.md └── LICENSE /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.10.0 2 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | rabbitmq: 5 | image: rabbitmq:latest 6 | ports: 7 | - "5672:5672" 8 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@zio.dev/zio-amqp", 3 | "description": "ZIO AMQP Documentation", 4 | "license": "Apache-2.0" 5 | } 6 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["config:base"], 3 | "branchPrefix": "renovate/", 4 | "automergeType": "pr", 5 | "automerge": true, 6 | "platformAutomerge": true, 7 | } 8 | -------------------------------------------------------------------------------- /docs/sidebars.js: -------------------------------------------------------------------------------- 1 | const sidebars = { 2 | sidebar: [ 3 | { 4 | type: "category", 5 | label: "ZIO AMQP", 6 | collapsed: false, 7 | link: { type: "doc", id: "index" }, 8 | items: [ 9 | ] 10 | } 11 | ] 12 | }; 13 | 14 | module.exports = sidebars; 15 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "2.2.0") 2 | addSbtPlugin("dev.zio" % "zio-sbt-ecosystem" % "0.4.0-alpha.22") 3 | addSbtPlugin("dev.zio" % "zio-sbt-ci" % "0.4.0-alpha.22") 4 | addSbtPlugin("dev.zio" % "zio-sbt-website" % "0.4.0-alpha.22") 5 | -------------------------------------------------------------------------------- /.github/workflows/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name: Release Drafter 2 | 3 | on: 4 | push: 5 | branches: ['master'] 6 | 7 | jobs: 8 | update_release_draft: 9 | runs-on: ubuntu-22.04 10 | steps: 11 | - uses: release-drafter/release-drafter@v5 12 | env: 13 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /zio-amqp/src/test/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /.scalafmt.conf: -------------------------------------------------------------------------------- 1 | version=3.8.2 2 | runner.dialect = scala213 3 | maxColumn = 120 4 | align.preset = most 5 | continuationIndent.defnSite = 2 6 | assumeStandardLibraryStripMargin = true 7 | docstrings.style = asterisk 8 | lineEndings = preserve 9 | includeCurlyBraceInSelectChains = false 10 | danglingParentheses.preset = true 11 | spaces { 12 | inImportCurlyBraces = true 13 | } 14 | optIn.annotationNewlines = true 15 | 16 | rewrite.rules = [RedundantBraces] -------------------------------------------------------------------------------- /.scalafix.conf: -------------------------------------------------------------------------------- 1 | rules = [ 2 | DisableSyntax 3 | ExplicitResultTypes 4 | LeakingImplicitClassVal 5 | NoAutoTupling 6 | NoValInForComprehension 7 | OrganizeImports 8 | ProcedureSyntax 9 | RemoveUnused 10 | ] 11 | 12 | Disable { 13 | ifSynthetic = [ 14 | "scala/Option.option2Iterable" 15 | "scala/Predef.any2stringadd" 16 | ] 17 | } 18 | 19 | OrganizeImports { 20 | expandRelative = true 21 | groupedImports = Merge 22 | } 23 | 24 | RemoveUnused { 25 | imports = false // handled by OrganizeImports 26 | } -------------------------------------------------------------------------------- /.github/workflows/auto-approve.yml: -------------------------------------------------------------------------------- 1 | # name: Auto-approve Renovate's PRs 2 | 3 | # on: pull_request_target 4 | 5 | # jobs: 6 | # auto-approve-renovate-prs: 7 | # runs-on: ubuntu-latest 8 | # steps: 9 | # - name: Git Checkout 10 | # uses: actions/checkout@v4.1.1 11 | # with: 12 | # fetch-depth: '0' 13 | # - name: Approve PR 14 | # if: github.actor == 'renovate[bot]' 15 | # run: gh pr review --approve ${{ github.event.number }} 16 | # env: 17 | # GH_TOKEN: "${{ secrets.GITHUB_TOKEN }}" -------------------------------------------------------------------------------- /zio-amqp/src/main/scala/zio/amqp/model/package.scala: -------------------------------------------------------------------------------- 1 | package zio.amqp 2 | 3 | import zio.prelude.{ Newtype, Subtype } 4 | 5 | import java.net.URI 6 | 7 | package object model { 8 | object QueueName extends Newtype[String] 9 | type QueueName = QueueName.Type 10 | 11 | object ExchangeName extends Newtype[String] 12 | type ExchangeName = ExchangeName.Type 13 | 14 | object RoutingKey extends Newtype[String] 15 | type RoutingKey = RoutingKey.Type 16 | 17 | object ConsumerTag extends Newtype[String] 18 | type ConsumerTag = ConsumerTag.Type 19 | 20 | // DeliveryTag is subtype of Long 21 | // because we need ordering 22 | object DeliveryTag extends Subtype[Long] 23 | type DeliveryTag = DeliveryTag.Type 24 | 25 | object AmqpUri extends Subtype[URI] 26 | type AmqpUri = AmqpUri.Type 27 | } 28 | -------------------------------------------------------------------------------- /.mergify.yml: -------------------------------------------------------------------------------- 1 | pull_request_rules: 2 | - name: merge Scala Steward's PRs 3 | conditions: 4 | - base=master 5 | - author=scala-steward 6 | - "body~=(labels: library-update, semver-minor)|(labels: library-update, semver-patch)|(labels: sbt-plugin-update, semver-minor)|(labels: sbt-plugin-update, semver-patch)|(labels: test-library-update, semver-minor)|(labels: test-library-update, semver-patch)" 7 | - "status-success=ci/circleci: test213_jdk11" 8 | - "status-success=ci/circleci: test213_jdk8" 9 | - "status-success=ci/circleci: test212_jdk11" 10 | - "status-success=ci/circleci: test212_jdk8" 11 | actions: 12 | merge: 13 | method: squash 14 | - name: Automatic merge on approval 15 | conditions: 16 | - "#approved-reviews-by>=1" 17 | actions: 18 | merge: 19 | method: squash -------------------------------------------------------------------------------- /project/Settings.scala: -------------------------------------------------------------------------------- 1 | import sbt.Keys.{ developers, homepage, licenses, organization, scmInfo } 2 | import sbt.{ url, Developer, ScmInfo } 3 | 4 | object Settings { 5 | lazy val devs = List( 6 | Developer( 7 | "Adriani277", 8 | "Adriani Furtado", 9 | "adrini.furtado@gmail.com", 10 | url("https://github.com/Adriani277") 11 | ) 12 | ) 13 | 14 | lazy val org = Seq( 15 | organization := "dev.zio", 16 | homepage := Some(url("https://zio.dev/ecosystem/community/zio-amqp/")), 17 | licenses := List( 18 | "Apache-2.0" -> url("http://www.apache.org/licenses/LICENSE-2.0") 19 | ), 20 | developers := devs, 21 | scmInfo := Some( 22 | ScmInfo( 23 | url("https://github.com/zio/zio-amqp"), 24 | "scm:git:git@github.com:zio/zio-amqp.git" 25 | ) 26 | ) 27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name-template: 'v$RESOLVED_VERSION' 2 | tag-template: 'v$RESOLVED_VERSION' 3 | template: | 4 | # What's Changed 5 | $CHANGES 6 | categories: 7 | - title: 'Breaking' 8 | label: 'type: breaking' 9 | - title: 'New' 10 | label: 'type: feature' 11 | - title: 'Bug Fixes' 12 | label: 'type: bug' 13 | - title: 'Maintenance' 14 | label: 'type: maintenance' 15 | - title: 'Documentation' 16 | label: 'type: docs' 17 | - title: 'Dependency Updates' 18 | label: 'type: dependencies' 19 | 20 | version-resolver: 21 | major: 22 | labels: 23 | - 'type: breaking' 24 | minor: 25 | labels: 26 | - 'type: feature' 27 | patch: 28 | labels: 29 | - 'type: bug' 30 | - 'type: maintenance' 31 | - 'type: docs' 32 | - 'type: dependencies' 33 | - 'type: security' 34 | 35 | exclude-labels: 36 | - 'skip-changelog' -------------------------------------------------------------------------------- /zio-amqp/src/main/scala/zio/amqp/model/ExchangeType.scala: -------------------------------------------------------------------------------- 1 | package zio.amqp.model 2 | 3 | import scala.language.implicitConversions 4 | 5 | sealed trait ExchangeType extends Product with Serializable { 6 | def name: String 7 | } 8 | 9 | object ExchangeType { 10 | case object Direct extends ExchangeType { 11 | override val name: String = "direct" 12 | } 13 | case object Fanout extends ExchangeType { 14 | override val name: String = "fanout" 15 | } 16 | case object Topic extends ExchangeType { 17 | override val name: String = "topic" 18 | } 19 | case object Headers extends ExchangeType { 20 | override val name: String = "headers" 21 | } 22 | case class Custom(`type`: String) extends ExchangeType { 23 | override val name: String = `type` 24 | } 25 | 26 | implicit def represent(`type`: ExchangeType): String = `type`.name 27 | } 28 | -------------------------------------------------------------------------------- /project/Dependencies.scala: -------------------------------------------------------------------------------- 1 | import sbt._ 2 | 3 | object Dependencies { 4 | 5 | val zioVersion = "2.1.5" 6 | lazy val deps = Seq( 7 | "dev.zio" %% "zio-streams" % zioVersion, 8 | "dev.zio" %% "zio-interop-reactivestreams" % "2.0.2", 9 | "com.rabbitmq" % "amqp-client" % "5.21.0", 10 | "org.scala-lang.modules" %% "scala-collection-compat" % "2.11.0", 11 | "dev.zio" %% "zio-prelude" % "1.0.0-RC23", 12 | "com.dimafeng" %% "testcontainers-scala-rabbitmq" % "0.41.4" % Test, 13 | "dev.zio" %% "zio-test" % zioVersion % Test, 14 | "dev.zio" %% "zio-test-sbt" % zioVersion % Test, 15 | "ch.qos.logback" % "logback-classic" % "1.5.6" % Test, 16 | "org.scalameta" % "semanticdb-scalac_2.13.12" % "4.8.14" 17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /zio-amqp/src/main/scala/zio/amqp/model/AMQPConfig.scala: -------------------------------------------------------------------------------- 1 | package zio.amqp.model 2 | import zio.{ Duration, durationInt } 3 | 4 | import java.net.{ URI, URLEncoder } 5 | 6 | case class AMQPConfig( 7 | user: String, 8 | password: String, 9 | vhost: String, 10 | heartbeatInterval: Duration, 11 | ssl: Boolean, 12 | host: String, 13 | port: Short, 14 | connectionTimeout: Duration 15 | ) { 16 | val encodedVhost: String = URLEncoder.encode(vhost, "UTF-8") 17 | def toUri: AmqpUri = 18 | AmqpUri( 19 | new URI( 20 | s"amqp${if (ssl) "s" else ""}://$user:$password@$host:$port/$encodedVhost?heartbeat=${heartbeatInterval.getSeconds}&connection_timeout=${connectionTimeout.toMillis}" 21 | ) 22 | ) 23 | } 24 | object AMQPConfig { 25 | lazy val default = AMQPConfig( 26 | user = "guest", 27 | password = "guest", 28 | vhost = "/", 29 | heartbeatInterval = 10.seconds, 30 | ssl = false, 31 | host = "localhost", 32 | port = 5672, 33 | connectionTimeout = 10.seconds 34 | ) 35 | 36 | } 37 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: index 3 | title: "ZIO AMQP" 4 | --- 5 | 6 | ZIO AMQP is a ZIO-based wrapper around the RabbitMQ client. It provides a streaming interface to AMQP queues and helps to prevent you from shooting yourself in the foot with thread-safety issues. 7 | 8 | @PROJECT_BADGES@ 9 | 10 | ## Installation 11 | 12 | Add the following lines to your `build.sbt` file: 13 | 14 | ```scala 15 | libraryDependencies += "dev.zio" %% "zio-amqp" % "@VERSION@" 16 | ``` 17 | 18 | ## Consuming 19 | 20 | The example below creates a connection to an AMQP server and then creates a channel. Both are created as Managed resources, which means they are closed automatically after using even in the face of errors. 21 | 22 | The example then creates a stream of the messages consumed from a queue named `"queueName"`. Each received message is acknowledged back to the AMQP server. 23 | 24 | ## Producing 25 | 26 | Also in the example bellow is a producer which publishes to a given queue 27 | 28 | ```scala 29 | import zio.amqp._ 30 | import zio.amqp.model._ 31 | import java.net.URI 32 | import zio._ 33 | import zio.Console._ 34 | 35 | val channel: ZIO[Scope, Throwable, Channel] = for { 36 | connection <- Amqp.connect(URI.create("amqp://my_amqp_server_uri")) 37 | channel <- Amqp.createChannel(connection) 38 | } yield channel 39 | 40 | val effect: ZIO[Any, Throwable, Unit] = 41 | ZIO.scoped { 42 | channel.flatMap { channel => 43 | channel 44 | .consume(queue = QueueName("queueName"), consumerTag = ConsumerTag("test")) 45 | .mapZIO { record => 46 | val deliveryTag = record.getEnvelope.getDeliveryTag 47 | printLine(s"Received ${deliveryTag}: ${new String(record.getBody)}") *> 48 | channel.ack(DeliveryTag(deliveryTag)) 49 | } 50 | .take(5) 51 | .runDrain 52 | } 53 | } 54 | 55 | val producer = ZIO.scoped { 56 | channel.flatMap { channel => 57 | channel.publish( 58 | exchange = ExchangeName(""), 59 | routingKey = RoutingKey("queueName"), 60 | body = "Hello world".getBytes 61 | ) 62 | } 63 | } 64 | ``` 65 | 66 | See the [ZIO documentation](https://zio.dev/docs/overview/overview_running_effects#defaultruntime) for more information on how to run this effect or integrate with an existing application. 67 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io/api/sbt,scala,intellij+all 2 | # Edit at https://www.gitignore.io/?templates=sbt,scala,intellij+all 3 | 4 | ### Intellij+all ### 5 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 6 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 7 | 8 | # User-specific stuff 9 | .idea/**/workspace.xml 10 | .idea/**/tasks.xml 11 | .idea/**/usage.statistics.xml 12 | .idea/**/dictionaries 13 | .idea/**/shelf 14 | 15 | # Generated files 16 | .idea/**/contentModel.xml 17 | 18 | # Sensitive or high-churn files 19 | .idea/**/dataSources/ 20 | .idea/**/dataSources.ids 21 | .idea/**/dataSources.local.xml 22 | .idea/**/sqlDataSources.xml 23 | .idea/**/dynamic.xml 24 | .idea/**/uiDesigner.xml 25 | .idea/**/dbnavigator.xml 26 | 27 | # Gradle 28 | .idea/**/gradle.xml 29 | .idea/**/libraries 30 | 31 | # Gradle and Maven with auto-import 32 | # When using Gradle or Maven with auto-import, you should exclude module files, 33 | # since they will be recreated, and may cause churn. Uncomment if using 34 | # auto-import. 35 | # .idea/modules.xml 36 | # .idea/*.iml 37 | # .idea/modules 38 | 39 | # CMake 40 | cmake-build-*/ 41 | 42 | # Mongo Explorer plugin 43 | .idea/**/mongoSettings.xml 44 | 45 | # File-based project format 46 | *.iws 47 | 48 | # IntelliJ 49 | out/ 50 | 51 | # mpeltonen/sbt-idea plugin 52 | .idea_modules/ 53 | 54 | # JIRA plugin 55 | atlassian-ide-plugin.xml 56 | 57 | # Cursive Clojure plugin 58 | .idea/replstate.xml 59 | 60 | # Crashlytics plugin (for Android Studio and IntelliJ) 61 | com_crashlytics_export_strings.xml 62 | crashlytics.properties 63 | crashlytics-build.properties 64 | fabric.properties 65 | 66 | # Editor-based Rest Client 67 | .idea/httpRequests 68 | 69 | # Android studio 3.1+ serialized cache file 70 | .idea/caches/build_file_checksums.ser 71 | 72 | # JetBrains templates 73 | **___jb_tmp___ 74 | 75 | ### Intellij+all Patch ### 76 | # Ignores the whole .idea folder and all .iml files 77 | # See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360 78 | 79 | .idea/ 80 | 81 | # Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023 82 | 83 | *.iml 84 | modules.xml 85 | .idea/misc.xml 86 | *.ipr 87 | 88 | # Sonarlint plugin 89 | .idea/sonarlint 90 | 91 | ### SBT ### 92 | # Simple Build Tool 93 | # http://www.scala-sbt.org/release/docs/Getting-Started/Directories.html#configuring-version-control 94 | 95 | dist/* 96 | target/ 97 | lib_managed/ 98 | src_managed/ 99 | project/boot/ 100 | project/plugins/project/ 101 | .history 102 | .cache 103 | .lib/ 104 | 105 | ### Scala ### 106 | *.class 107 | *.log 108 | 109 | ### Metals / Bloop ### 110 | .bloop/ 111 | .metals/ 112 | metals.sbt 113 | # End of https://www.gitignore.io/api/sbt,scala,intellij+all 114 | .bsp/ 115 | .vscode/ 116 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [//]: # (This file was autogenerated using `zio-sbt-website` plugin via `sbt generateReadme` command.) 2 | [//]: # (So please do not edit it manually. Instead, change "docs/index.md" file or sbt setting keys) 3 | [//]: # (e.g. "readmeDocumentation" and "readmeSupport".) 4 | 5 | # ZIO AMQP 6 | 7 | ZIO AMQP is a ZIO-based wrapper around the RabbitMQ client. It provides a streaming interface to AMQP queues and helps to prevent you from shooting yourself in the foot with thread-safety issues. 8 | 9 | [![Development](https://img.shields.io/badge/Project%20Stage-Development-green.svg)](https://github.com/zio/zio/wiki/Project-Stages) ![CI Badge](https://github.com/zio/zio-amqp/workflows/CI/badge.svg) [![Sonatype Releases](https://img.shields.io/nexus/r/https/oss.sonatype.org/dev.zio/zio-amqp_2.13.svg?label=Sonatype%20Release)](https://oss.sonatype.org/content/repositories/releases/dev/zio/zio-amqp_2.13/) [![Sonatype Snapshots](https://img.shields.io/nexus/s/https/oss.sonatype.org/dev.zio/zio-amqp_2.13.svg?label=Sonatype%20Snapshot)](https://oss.sonatype.org/content/repositories/snapshots/dev/zio/zio-amqp_2.13/) [![javadoc](https://javadoc.io/badge2/dev.zio/zio-amqp-docs_2.13/javadoc.svg)](https://javadoc.io/doc/dev.zio/zio-amqp-docs_2.13) [![ZIO AMQP](https://img.shields.io/github/stars/zio/zio-amqp?style=social)](https://github.com/zio/zio-amqp) 10 | 11 | ## Installation 12 | 13 | Add the following lines to your `build.sbt` file: 14 | 15 | ```scala 16 | libraryDependencies += "dev.zio" %% "zio-amqp" % "1.0.0-alpha.3" 17 | ``` 18 | 19 | ## Consuming 20 | 21 | The example below creates a connection to an AMQP server and then creates a channel. Both are created as Managed resources, which means they are closed automatically after using even in the face of errors. 22 | 23 | The example then creates a stream of the messages consumed from a queue named `"queueName"`. Each received message is acknowledged back to the AMQP server. 24 | 25 | ## Producing 26 | 27 | Also in the example bellow is a producer which publishes to a given queue 28 | 29 | ```scala 30 | import zio.amqp._ 31 | import zio.amqp.model._ 32 | import java.net.URI 33 | import zio._ 34 | import zio.Console._ 35 | 36 | val channel: ZIO[Scope, Throwable, Channel] = for { 37 | connection <- Amqp.connect(URI.create("amqp://my_amqp_server_uri")) 38 | channel <- Amqp.createChannel(connection) 39 | } yield channel 40 | 41 | val effect: ZIO[Any, Throwable, Unit] = 42 | ZIO.scoped { 43 | channel.flatMap { channel => 44 | channel 45 | .consume(queue = QueueName("queueName"), consumerTag = ConsumerTag("test")) 46 | .mapZIO { record => 47 | val deliveryTag = record.getEnvelope.getDeliveryTag 48 | printLine(s"Received ${deliveryTag}: ${new String(record.getBody)}") *> 49 | channel.ack(DeliveryTag(deliveryTag)) 50 | } 51 | .take(5) 52 | .runDrain 53 | } 54 | } 55 | 56 | val producer = ZIO.scoped { 57 | channel.flatMap { channel => 58 | channel.publish( 59 | exchange = ExchangeName(""), 60 | routingKey = RoutingKey("queueName"), 61 | body = "Hello world".getBytes 62 | ) 63 | } 64 | } 65 | ``` 66 | 67 | See the [ZIO documentation](https://zio.dev/docs/overview/overview_running_effects#defaultruntime) for more information on how to run this effect or integrate with an existing application. 68 | 69 | ## Documentation 70 | 71 | Learn more on the [ZIO AMQP homepage](https://zio.dev/zio-amqp)! 72 | 73 | ## Contributing 74 | 75 | For the general guidelines, see ZIO [contributor's guide](https://zio.dev/contributor-guidelines). 76 | 77 | ## Code of Conduct 78 | 79 | See the [Code of Conduct](https://zio.dev/code-of-conduct) 80 | 81 | ## Support 82 | 83 | Come chat with us on [![Badge-Discord]][Link-Discord]. 84 | 85 | [Badge-Discord]: https://img.shields.io/discord/629491597070827530?logo=discord "chat on discord" 86 | [Link-Discord]: https://discord.gg/2ccFBr4 "Discord" 87 | 88 | ## License 89 | 90 | [License](LICENSE) 91 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # This file was autogenerated using `zio-sbt-ci` plugin via `sbt ciGenerateGithubWorkflow` 2 | # task and should be included in the git repository. Please do not edit it manually. 3 | 4 | name: CI 5 | env: 6 | JDK_JAVA_OPTIONS: -XX:+PrintCommandLineFlags 7 | 'on': 8 | workflow_dispatch: {} 9 | release: 10 | types: 11 | - published 12 | push: 13 | branches: 14 | - master 15 | pull_request: 16 | branches-ignore: 17 | - gh-pages 18 | jobs: 19 | build: 20 | name: Build 21 | runs-on: ubuntu-latest 22 | continue-on-error: true 23 | steps: 24 | - name: Git Checkout 25 | uses: actions/checkout@v4.1.0 26 | with: 27 | fetch-depth: '0' 28 | - name: Install libuv 29 | run: sudo apt-get update && sudo apt-get install -y libuv1-dev 30 | - name: Setup Scala 31 | uses: actions/setup-java@v3.13.0 32 | with: 33 | distribution: corretto 34 | java-version: '17' 35 | check-latest: true 36 | - name: Cache Dependencies 37 | uses: coursier/cache-action@v6 38 | - name: Check all code compiles 39 | run: sbt +Test/compile 40 | - name: Check artifacts build process 41 | run: sbt +publishLocal 42 | - name: Check website build process 43 | run: sbt docs/clean; sbt docs/buildWebsite 44 | lint: 45 | name: Lint 46 | runs-on: ubuntu-latest 47 | continue-on-error: false 48 | steps: 49 | - name: Git Checkout 50 | uses: actions/checkout@v4.1.0 51 | with: 52 | fetch-depth: '0' 53 | - name: Install libuv 54 | run: sudo apt-get update && sudo apt-get install -y libuv1-dev 55 | - name: Setup Scala 56 | uses: actions/setup-java@v3.13.0 57 | with: 58 | distribution: corretto 59 | java-version: '17' 60 | check-latest: true 61 | - name: Cache Dependencies 62 | uses: coursier/cache-action@v6 63 | - name: Check if the site workflow is up to date 64 | run: sbt ciCheckGithubWorkflow 65 | - name: Lint 66 | run: sbt lint 67 | test: 68 | name: Test 69 | runs-on: ubuntu-latest 70 | continue-on-error: false 71 | strategy: 72 | fail-fast: false 73 | matrix: 74 | java: 75 | - '11' 76 | - '17' 77 | - '21' 78 | steps: 79 | - name: Install libuv 80 | run: sudo apt-get update && sudo apt-get install -y libuv1-dev 81 | - name: Setup Scala 82 | uses: actions/setup-java@v3.13.0 83 | with: 84 | distribution: corretto 85 | java-version: ${{ matrix.java }} 86 | check-latest: true 87 | - name: Cache Dependencies 88 | uses: coursier/cache-action@v6 89 | - name: Git Checkout 90 | uses: actions/checkout@v4.1.0 91 | with: 92 | fetch-depth: '0' 93 | - name: Start containers 94 | run: docker-compose -f docker-compose.yml up -d --build 95 | - name: Test 96 | run: sbt +test 97 | update-readme: 98 | name: Update README 99 | runs-on: ubuntu-latest 100 | continue-on-error: false 101 | if: ${{ github.event_name == 'push' }} 102 | steps: 103 | - name: Git Checkout 104 | uses: actions/checkout@v4.1.0 105 | with: 106 | fetch-depth: '0' 107 | - name: Install libuv 108 | run: sudo apt-get update && sudo apt-get install -y libuv1-dev 109 | - name: Setup Scala 110 | uses: actions/setup-java@v3.13.0 111 | with: 112 | distribution: corretto 113 | java-version: '17' 114 | check-latest: true 115 | - name: Cache Dependencies 116 | uses: coursier/cache-action@v6 117 | - name: Generate Readme 118 | run: sbt docs/generateReadme 119 | - name: Commit Changes 120 | run: | 121 | git config --local user.email "zio-assistant[bot]@users.noreply.github.com" 122 | git config --local user.name "ZIO Assistant" 123 | git add README.md 124 | git commit -m "Update README.md" || echo "No changes to commit" 125 | - name: Generate Token 126 | id: generate-token 127 | uses: zio/generate-github-app-token@v1.0.0 128 | with: 129 | app_id: ${{ secrets.APP_ID }} 130 | app_private_key: ${{ secrets.APP_PRIVATE_KEY }} 131 | - name: Create Pull Request 132 | id: cpr 133 | uses: peter-evans/create-pull-request@v5.0.2 134 | with: 135 | body: |- 136 | Autogenerated changes after running the `sbt docs/generateReadme` command of the [zio-sbt-website](https://zio.dev/zio-sbt) plugin. 137 | 138 | I will automatically update the README.md file whenever there is new change for README.md, e.g. 139 | - After each release, I will update the version in the installation section. 140 | - After any changes to the "docs/index.md" file, I will update the README.md file accordingly. 141 | branch: zio-sbt-website/update-readme 142 | commit-message: Update README.md 143 | token: ${{ steps.generate-token.outputs.token }} 144 | delete-branch: true 145 | title: Update README.md 146 | - name: Approve PR 147 | if: ${{ steps.cpr.outputs.pull-request-number }} 148 | run: gh pr review "$PR_URL" --approve 149 | env: 150 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 151 | PR_URL: ${{ steps.cpr.outputs.pull-request-url }} 152 | - name: Enable Auto-Merge 153 | if: ${{ steps.cpr.outputs.pull-request-number }} 154 | run: gh pr merge --auto --squash "$PR_URL" || gh pr merge --squash "$PR_URL" 155 | env: 156 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 157 | PR_URL: ${{ steps.cpr.outputs.pull-request-url }} 158 | ci: 159 | name: ci 160 | runs-on: ubuntu-latest 161 | continue-on-error: false 162 | needs: 163 | - lint 164 | - test 165 | - build 166 | steps: 167 | - name: Report Successful CI 168 | run: echo "ci passed" 169 | release: 170 | name: Release 171 | runs-on: ubuntu-latest 172 | continue-on-error: false 173 | needs: 174 | - ci 175 | if: ${{ github.event_name != 'pull_request' }} 176 | steps: 177 | - name: Git Checkout 178 | uses: actions/checkout@v4.1.0 179 | with: 180 | fetch-depth: '0' 181 | - name: Install libuv 182 | run: sudo apt-get update && sudo apt-get install -y libuv1-dev 183 | - name: Setup Scala 184 | uses: actions/setup-java@v3.13.0 185 | with: 186 | distribution: corretto 187 | java-version: '17' 188 | check-latest: true 189 | - name: Cache Dependencies 190 | uses: coursier/cache-action@v6 191 | - name: Release 192 | run: sbt ci-release 193 | env: 194 | PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }} 195 | PGP_SECRET: ${{ secrets.PGP_SECRET }} 196 | SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} 197 | SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} 198 | release-docs: 199 | name: Release Docs 200 | runs-on: ubuntu-latest 201 | continue-on-error: false 202 | needs: 203 | - release 204 | if: ${{ ((github.event_name == 'release') && (github.event.action == 'published')) || (github.event_name == 'workflow_dispatch') }} 205 | steps: 206 | - name: Git Checkout 207 | uses: actions/checkout@v4.1.0 208 | with: 209 | fetch-depth: '0' 210 | - name: Install libuv 211 | run: sudo apt-get update && sudo apt-get install -y libuv1-dev 212 | - name: Setup Scala 213 | uses: actions/setup-java@v3.13.0 214 | with: 215 | distribution: corretto 216 | java-version: '17' 217 | check-latest: true 218 | - name: Cache Dependencies 219 | uses: coursier/cache-action@v6 220 | - name: Setup NodeJs 221 | uses: actions/setup-node@v3 222 | with: 223 | node-version: 16.x 224 | registry-url: https://registry.npmjs.org 225 | - name: Publish Docs to NPM Registry 226 | run: sbt docs/publishToNpm 227 | env: 228 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 229 | notify-docs-release: 230 | name: Notify Docs Release 231 | runs-on: ubuntu-latest 232 | continue-on-error: false 233 | needs: 234 | - release-docs 235 | if: ${{ (github.event_name == 'release') && (github.event.action == 'published') }} 236 | steps: 237 | - name: Git Checkout 238 | uses: actions/checkout@v4.1.0 239 | with: 240 | fetch-depth: '0' 241 | - name: notify the main repo about the new release of docs package 242 | run: | 243 | PACKAGE_NAME=$(cat docs/package.json | grep '"name"' | awk -F'"' '{print $4}') 244 | PACKAGE_VERSION=$(npm view $PACKAGE_NAME version) 245 | curl -L \ 246 | -X POST \ 247 | -H "Accept: application/vnd.github+json" \ 248 | -H "Authorization: token ${{ secrets.PAT_TOKEN }}"\ 249 | https://api.github.com/repos/zio/zio/dispatches \ 250 | -d '{ 251 | "event_type":"update-docs", 252 | "client_payload":{ 253 | "package_name":"'"${PACKAGE_NAME}"'", 254 | "package_version": "'"${PACKAGE_VERSION}"'" 255 | } 256 | }' 257 | -------------------------------------------------------------------------------- /zio-amqp/src/test/scala/zio/amqp/AmqpClientSpec.scala: -------------------------------------------------------------------------------- 1 | package zio.amqp 2 | 3 | import com.dimafeng.testcontainers.RabbitMQContainer 4 | import com.rabbitmq.client.ConnectionFactory 5 | import zio._ 6 | import zio.amqp.model._ 7 | import zio.test.Assertion.equalTo 8 | import zio.test.TestAspect.{ timeout, withLiveClock } 9 | import zio.test._ 10 | 11 | import java.nio.charset.StandardCharsets 12 | import java.util.UUID 13 | import java.util.concurrent.TimeUnit 14 | 15 | final case class ContainerDetails(host: String, amqpPort: Int) 16 | object AmqpClientSpec extends ZIOSpecDefault { 17 | val rabbitContainerDetails: ZLayer[Scope, Throwable, ContainerDetails] = 18 | ZLayer.fromZIO( 19 | zio.System.env("CI").flatMap { 20 | case Some(_) => 21 | ZIO.succeed(ContainerDetails("localhost", 5672)).debug("Running on CI, using dedicated RabbitMQ") 22 | case None => 23 | ZIO 24 | .acquireRelease(ZIO.attempt(RabbitMQContainer.Def().start()))(c => ZIO.attempt(c.stop()).orDie) 25 | .map(c => ContainerDetails(c.host, c.amqpPort)) 26 | .debug("Running locally, using testcontainers RabbitMQ") 27 | 28 | } 29 | ) 30 | 31 | override def spec = 32 | suite("AmqpClientSpec")( 33 | test("Amqp.consume delivers messages") { 34 | val testAmqpSuffix = s"AmqpClientSpec-${UUID.randomUUID().toString}" 35 | val exchangeName = ExchangeName(s"exchange-$testAmqpSuffix") 36 | val queueName = QueueName(s"queue-$testAmqpSuffix") 37 | val message1 = UUID.randomUUID().toString 38 | val message2 = UUID.randomUUID().toString 39 | val messages = Set(message1, message2) 40 | val factory = new ConnectionFactory() 41 | 42 | ZIO.service[ContainerDetails].flatMap { container => 43 | factory.setHost(container.host) 44 | factory.setPort(container.amqpPort) 45 | Amqp 46 | .connect(factory) 47 | .tap(_ => ZIO.log("Connected!")) 48 | .flatMap(Amqp.createChannel) 49 | .tap(_ => ZIO.log("Created channel!")) 50 | .flatMap { channel => 51 | for { 52 | _ <- channel.queueDeclare(queueName) 53 | _ <- channel.exchangeDeclare(exchangeName, ExchangeType.Fanout) 54 | _ <- channel.queueBind(queueName, exchangeName, RoutingKey("myroutingkey")) 55 | _ <- channel.publish(exchangeName, message1.getBytes) 56 | _ <- channel.publish(exchangeName, message2.getBytes) 57 | bodies <- 58 | channel 59 | .consume(queue = queueName, consumerTag = ConsumerTag("test")) 60 | .tap(record => ZIO.log(s"${record.getEnvelope.getDeliveryTag}: ${new String(record.getBody)}")) 61 | .take(2) 62 | .runCollect 63 | .tap { records => 64 | val tag = records.last.getEnvelope.getDeliveryTag 65 | ZIO.log(s"At tag: $tag") *> 66 | channel.ack(DeliveryTag(tag)) 67 | } 68 | .map(_.map(r => new String(r.getBody))) 69 | _ <- channel.queueDelete(queueName) 70 | _ <- channel.exchangeDelete(exchangeName) 71 | } yield assert(messages)(equalTo(bodies.toSet)) 72 | } 73 | 74 | } 75 | } @@ timeout(Duration(60, TimeUnit.SECONDS)), 76 | test("Amqp.publish delivers messages with high concurrency") { 77 | 78 | val testAmqpSuffix = s"AmqpClientSpec-${UUID.randomUUID().toString}" 79 | val exchangeName = ExchangeName(s"exchange-$testAmqpSuffix") 80 | val queueName = QueueName(s"queue-$testAmqpSuffix") 81 | val numMessages = 20000 82 | val messages = (1 to numMessages).map(i => s"$i " + UUID.randomUUID.toString) 83 | val factory = new ConnectionFactory() 84 | 85 | ZIO.service[ContainerDetails].flatMap { container => 86 | factory.setHost(container.host) 87 | factory.setPort(container.amqpPort) 88 | 89 | Amqp 90 | .connect(factory) 91 | .tap(_ => ZIO.log("Connected!")) 92 | .flatMap(Amqp.createChannel) 93 | .tap(_ => ZIO.log("Created channel!")) 94 | .flatMap { channel => 95 | for { 96 | _ <- channel.queueDeclare(queueName) 97 | _ <- 98 | channel.exchangeDeclare( 99 | exchangeName, 100 | ExchangeType.Custom("fanout") 101 | ) // doesn't actually need to be a custom exchange type, just testing to make sure custom exchange types "work" 102 | _ <- channel.queueBind(queueName, exchangeName, RoutingKey("myroutingkey")) 103 | _ <- 104 | ZIO.foreachParDiscard(0 until numMessages)(i => channel.publish(exchangeName, messages(i).getBytes)) 105 | bodies <- channel 106 | .consume(queue = queueName, consumerTag = ConsumerTag("test")) 107 | .take(numMessages.toLong) 108 | .runCollect 109 | .tap { records => 110 | val tag = records.last.getEnvelope.getDeliveryTag 111 | channel.ack(DeliveryTag(tag)) 112 | } 113 | .map(_.map(r => new String(r.getBody))) 114 | _ <- channel.queueDelete(queueName) 115 | _ <- channel.exchangeDelete(exchangeName) 116 | } yield assert(messages.toSet)(equalTo(bodies.toSet)) 117 | } 118 | } 119 | } @@ timeout(2.minutes) @@ TestAspect.flaky, 120 | test("Amqp.declareQueuePassive checks if a queue exists") { 121 | val testAmqpSuffix = s"AmqpClientSpec-${UUID.randomUUID().toString}" 122 | val queueName = QueueName(s"queue-$testAmqpSuffix") 123 | val factory = new ConnectionFactory() 124 | 125 | ZIO.service[ContainerDetails].flatMap { container => 126 | factory.setHost(container.host) 127 | factory.setPort(container.amqpPort) 128 | Amqp 129 | .connect(factory) 130 | .flatMap(Amqp.createChannel) 131 | .flatMap { channel => 132 | for { 133 | _ <- channel.queueDeclare(queueName) 134 | q <- channel.queueDeclarePassive(queueName) 135 | _ <- channel.queueDelete(queueName) 136 | } yield assert(q.getQueue)(equalTo(QueueName.unwrap(queueName))) 137 | } 138 | } 139 | } @@ timeout(Duration(60, TimeUnit.SECONDS)), 140 | test("Amqp.messageCounts returns the number of messages in a queue ready to be delivered to consumers") { 141 | val testAmqpSuffix = s"AmqpClientSpec-${UUID.randomUUID().toString}" 142 | val queueName = QueueName(s"queue-$testAmqpSuffix") 143 | val factory = new ConnectionFactory() 144 | 145 | ZIO.service[ContainerDetails].flatMap { container => 146 | factory.setHost(container.host) 147 | factory.setPort(container.amqpPort) 148 | Amqp 149 | .connect(factory) 150 | .flatMap(Amqp.createChannel) 151 | .flatMap { channel => 152 | for { 153 | _ <- channel.queueDeclare(queueName) 154 | before <- channel.messageCount(queueName) 155 | _ <- channel.publish( 156 | ExchangeName(""), 157 | "ping".getBytes(StandardCharsets.UTF_8), 158 | RoutingKey(QueueName.unwrap(queueName)) 159 | ) 160 | after <- channel.messageCount(queueName).delay(1.second) 161 | _ <- channel.queueDelete(queueName) 162 | } yield assert(before -> after)(equalTo(0L -> 1L)) 163 | } 164 | } 165 | } @@ withLiveClock @@ timeout(Duration(60, TimeUnit.SECONDS)), 166 | test("Amqp.consumerCounts the number of consumers on a queue") { 167 | val testAmqpSuffix = s"AmqpClientSpec-${UUID.randomUUID().toString}" 168 | val queueName = QueueName(s"queue-$testAmqpSuffix") 169 | val factory = new ConnectionFactory() 170 | 171 | ZIO.service[ContainerDetails].flatMap { container => 172 | factory.setHost(container.host) 173 | factory.setPort(container.amqpPort) 174 | Amqp 175 | .connect(factory) 176 | .flatMap(Amqp.createChannel) 177 | .flatMap { channel => 178 | for { 179 | _ <- channel.queueDeclare(queueName) 180 | before <- channel.consumerCount(queueName) 181 | fiber <- channel.consume(queueName, ConsumerTag("tag")).runDrain.fork 182 | after <- channel.consumerCount(queueName).delay(1.second) 183 | _ <- fiber.interrupt 184 | _ <- channel.queueDelete(queueName) 185 | } yield assert(before -> after)(equalTo(0L -> 1L)) 186 | } 187 | } 188 | } @@ withLiveClock @@ timeout(Duration(60, TimeUnit.SECONDS)) 189 | ).provideSomeShared[Scope](rabbitContainerDetails) @@ TestAspect.timed @@ TestAspect.withLiveSystem 190 | } 191 | -------------------------------------------------------------------------------- /zio-amqp/src/main/scala/zio/amqp/Client.scala: -------------------------------------------------------------------------------- 1 | package zio.amqp 2 | 3 | import com.rabbitmq.client.AMQP.Queue.{ DeclareOk, PurgeOk } 4 | import com.rabbitmq.client.{ Channel => RChannel, _ } 5 | import zio.ZIO.attemptBlocking 6 | import zio._ 7 | import zio.amqp.model._ 8 | import zio.stream.ZStream 9 | 10 | import java.net.URI 11 | import scala.jdk.CollectionConverters._ 12 | 13 | /** 14 | * Thread-safe access to a RabbitMQ Channel 15 | */ 16 | class Channel private[amqp] (channel: RChannel, access: Semaphore) { 17 | 18 | /** 19 | * Declare a queue 20 | * @param queue 21 | * Name of the queue. If left empty, a random queue name is used 22 | * @param durable 23 | * True if we are declaring a durable queue (the queue will survive a server restart) 24 | * @param exclusive 25 | * Exclusive to this connection 26 | * @param autoDelete 27 | * True if we are declaring an autodelete queue (server will delete it when no longer in use) 28 | * @param arguments 29 | * @return 30 | * The name of the created queue 31 | */ 32 | def queueDeclare( 33 | queue: QueueName, 34 | durable: Boolean = false, 35 | exclusive: Boolean = false, 36 | autoDelete: Boolean = false, 37 | arguments: Map[String, AnyRef] = Map.empty 38 | ): ZIO[Any, Throwable, String] = withChannelBlocking( 39 | _.queueDeclare( 40 | QueueName.unwrap(queue), 41 | durable, 42 | exclusive, 43 | autoDelete, 44 | arguments.asJava 45 | ) 46 | ).map(_.getQueue) 47 | 48 | /** 49 | * Check if a queue exists 50 | * @param queue 51 | * Name of the queue. 52 | * @return 53 | * a declaration-confirm method to indicate the queue exists 54 | */ 55 | def queueDeclarePassive( 56 | queue: QueueName 57 | ): ZIO[Any, Throwable, DeclareOk] = withChannelBlocking( 58 | _.queueDeclarePassive( 59 | QueueName.unwrap(queue) 60 | ) 61 | ) 62 | 63 | /** 64 | * Delete a queue 65 | * 66 | * @param queue 67 | * Name of the queue 68 | * @param ifUnused 69 | * True if the queue should be deleted only if not in use 70 | * @param ifEmpty 71 | * True if the queue should be deleted only if empty 72 | */ 73 | def queueDelete( 74 | queue: QueueName, 75 | ifUnused: Boolean = false, 76 | ifEmpty: Boolean = false 77 | ): ZIO[Any, Throwable, Unit] = withChannelBlocking( 78 | _.queueDelete( 79 | QueueName.unwrap(queue), 80 | ifUnused, 81 | ifEmpty 82 | ) 83 | ).unit 84 | 85 | def exchangeDeclare( 86 | exchange: ExchangeName, 87 | `type`: ExchangeType, 88 | durable: Boolean = false, 89 | autoDelete: Boolean = false, 90 | internal: Boolean = false, 91 | arguments: Map[String, AnyRef] = Map.empty 92 | ): ZIO[Any, Throwable, Unit] = withChannelBlocking( 93 | _.exchangeDeclare( 94 | ExchangeName.unwrap(exchange), 95 | `type`, 96 | durable, 97 | autoDelete, 98 | internal, 99 | arguments.asJava 100 | ) 101 | ).unit 102 | 103 | def exchangeDelete( 104 | exchange: ExchangeName, 105 | ifUnused: Boolean = false 106 | ): ZIO[Any, Throwable, Unit] = withChannelBlocking( 107 | _.exchangeDelete( 108 | ExchangeName.unwrap(exchange), 109 | ifUnused 110 | ) 111 | ).unit 112 | 113 | def queueBind( 114 | queue: QueueName, 115 | exchange: ExchangeName, 116 | routingKey: RoutingKey, 117 | arguments: Map[String, AnyRef] = Map.empty 118 | ): ZIO[Any, Throwable, Unit] = withChannelBlocking( 119 | _.queueBind( 120 | QueueName.unwrap(queue), 121 | ExchangeName.unwrap(exchange), 122 | RoutingKey.unwrap(routingKey), 123 | arguments.asJava 124 | ) 125 | ).unit 126 | 127 | def basicQos( 128 | count: Int, 129 | global: Boolean = false 130 | ): ZIO[Any, Throwable, Unit] = 131 | withChannelBlocking(_.basicQos(count, global)).unit 132 | 133 | /** 134 | * Consume a stream of messages from a queue 135 | * 136 | * When the stream is completed, the AMQP consumption is cancelled 137 | * 138 | * @param queue 139 | * @param consumerTag 140 | * @param autoAck 141 | * @return 142 | */ 143 | def consume( 144 | queue: QueueName, 145 | consumerTag: ConsumerTag, 146 | autoAck: Boolean = false 147 | ): ZStream[Any, Throwable, Delivery] = 148 | ZStream 149 | .asyncZIO[Any, Throwable, Delivery] { offer => 150 | withChannel { c => 151 | attemptBlocking { 152 | c.basicConsume( 153 | QueueName.unwrap(queue), 154 | autoAck, 155 | ConsumerTag.unwrap(consumerTag), 156 | new DeliverCallback { 157 | override def handle(consumerTag: String, message: Delivery): Unit = 158 | offer(ZIO.succeed(Chunk.single(message))) 159 | }, 160 | new CancelCallback { 161 | override def handle(consumerTag: String): Unit = offer(ZIO.fail(None)) 162 | }, 163 | new ConsumerShutdownSignalCallback { 164 | override def handleShutdownSignal(consumerTag: String, sig: ShutdownSignalException): Unit = 165 | offer(ZIO.fail(Some(sig))) 166 | } 167 | ) 168 | } 169 | } 170 | } 171 | .ensuring { 172 | withChannel(c => 173 | attemptBlocking( 174 | c.basicCancel(ConsumerTag.unwrap(consumerTag)) 175 | ) 176 | ).ignore 177 | } 178 | 179 | def ack(deliveryTag: DeliveryTag, multiple: Boolean = false): ZIO[Any, Throwable, Unit] = 180 | withChannel(c => 181 | attemptBlocking( 182 | c.basicAck(deliveryTag, multiple) 183 | ) 184 | ) 185 | 186 | def ackMany(deliveryTags: Seq[DeliveryTag]): ZIO[Any, Throwable, Unit] = 187 | ack(deliveryTags.max[Long], multiple = true) 188 | 189 | def nack( 190 | deliveryTag: DeliveryTag, 191 | requeue: Boolean = false, 192 | multiple: Boolean = false 193 | ): ZIO[Any, Throwable, Unit] = 194 | withChannel(c => 195 | attemptBlocking( 196 | c.basicNack(deliveryTag, multiple, requeue) 197 | ) 198 | ) 199 | 200 | def nackMany(deliveryTags: Seq[DeliveryTag], requeue: Boolean = false): ZIO[Any, Throwable, Unit] = 201 | nack(deliveryTags.max[Long], requeue, multiple = true) 202 | 203 | def publish( 204 | exchange: ExchangeName, 205 | body: Array[Byte], 206 | routingKey: RoutingKey = RoutingKey(""), 207 | mandatory: Boolean = false, 208 | immediate: Boolean = false, 209 | props: AMQP.BasicProperties = new AMQP.BasicProperties() 210 | ): ZIO[Any, Throwable, Unit] = 211 | withChannel(c => 212 | attemptBlocking( 213 | c.basicPublish( 214 | ExchangeName.unwrap(exchange), 215 | RoutingKey.unwrap(routingKey), 216 | mandatory, 217 | immediate, 218 | props, 219 | body 220 | ) 221 | ) 222 | ) 223 | 224 | /** 225 | * Returns the number of messages in a queue ready to be delivered to consumers. This method assumes the queue exists. 226 | * If it doesn't, the channels will be closed with an exception. 227 | * 228 | * @param queue 229 | * the name of the queue 230 | * @return 231 | * the number of messages in ready state 232 | */ 233 | def messageCount(queue: QueueName): ZIO[Any, Throwable, Long] = withChannelBlocking { c => 234 | c.messageCount(QueueName.unwrap(queue)) 235 | } 236 | 237 | /** 238 | * Returns the number of consumers on a queue. This method assumes the queue exists. If it doesn't, the channel will 239 | * be closed with an exception. 240 | * 241 | * @param queue 242 | * the name of the queue 243 | * @return 244 | * the number of consumers 245 | */ 246 | def consumerCount(queue: QueueName): ZIO[Any, Throwable, Long] = withChannelBlocking { c => 247 | c.consumerCount(QueueName.unwrap(queue)) 248 | } 249 | 250 | /** 251 | * Purges the contents of the given queue. 252 | * 253 | * @param queue 254 | * the name of the queue 255 | * @return 256 | * purge-confirm if the purge was executed successfully 257 | */ 258 | def purgeQueue(queue: QueueName): ZIO[Any, Throwable, PurgeOk] = withChannelBlocking { c => 259 | c.queuePurge(QueueName.unwrap(queue)) 260 | } 261 | 262 | private[amqp] def withChannel[T](f: RChannel => Task[T]) = 263 | access.withPermit(f(channel)) 264 | 265 | private[amqp] def withChannelBlocking[R, T](f: RChannel => T) = 266 | access.withPermit(attemptBlocking(f(channel))) 267 | } 268 | 269 | object Amqp { 270 | 271 | /** 272 | * Creates a Connection 273 | * 274 | * @param factory 275 | * Connection factory 276 | * @return 277 | * Connection as a managed resource 278 | */ 279 | def connect(factory: ConnectionFactory): ZIO[Scope, Throwable, Connection] = 280 | ZIO.acquireRelease(attemptBlocking(factory.newConnection()))(c => ZIO.attempt(c.close()).orDie) 281 | 282 | def connect(uri: URI): ZIO[Scope, Throwable, Connection] = { 283 | val factory = new ConnectionFactory() 284 | factory.setUri(uri) 285 | connect(factory) 286 | } 287 | def connect(amqpConfig: AMQPConfig): ZIO[Scope, Throwable, Connection] = { 288 | val factory = new ConnectionFactory() 289 | factory.setUri(amqpConfig.toUri) 290 | connect(factory) 291 | } 292 | 293 | /** 294 | * Creates a Channel that is safe for concurrent access 295 | * 296 | * @param connection 297 | * @return 298 | */ 299 | 300 | def createChannel(connection: Connection): ZIO[Scope, Throwable, Channel] = 301 | (for { 302 | channel <- ZIO.attempt(connection.createChannel()) 303 | permit <- Semaphore.make(1) 304 | } yield new Channel(channel, permit)).withFinalizer(_.withChannel(c => attemptBlocking(c.close())).orDie) 305 | 306 | } 307 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------