├── README.md ├── amqprs ├── clippy.toml ├── src │ ├── frame │ │ ├── heartbeat.rs │ │ ├── method │ │ │ ├── access.rs │ │ │ ├── confirm.rs │ │ │ ├── tx.rs │ │ │ ├── mod.rs │ │ │ ├── channel.rs │ │ │ ├── exchange.rs │ │ │ ├── queue.rs │ │ │ └── connection.rs │ │ ├── content_body.rs │ │ ├── error.rs │ │ ├── protocol_header.rs │ │ ├── constants.rs │ │ └── mod.rs │ ├── test_utils.rs │ ├── api │ │ ├── compliance_asserts.rs │ │ ├── mod.rs │ │ ├── error.rs │ │ ├── channel │ │ │ ├── confim.rs │ │ │ └── tx.rs │ │ ├── security.rs │ │ ├── consumer.rs │ │ └── callbacks.rs │ ├── net │ │ ├── error.rs │ │ ├── mod.rs │ │ ├── writer_handler.rs │ │ ├── channel_manager.rs │ │ └── channel_id_repo.rs │ └── lib.rs ├── tests │ ├── test_update_secret.rs │ ├── common │ │ └── mod.rs │ ├── test_publish_return.rs │ ├── test_heartbeat.rs │ ├── test_publish.rs │ ├── test_io_error_handling.rs │ ├── test_callback.rs │ ├── test_mixed_type_consumers.rs │ └── test_get.rs ├── Cargo.toml └── README.md ├── architecture.png ├── .gitignore ├── .github ├── .codecov.yml └── workflows │ ├── actions.yml │ ├── benchmarks.yml │ ├── regression_test.yml │ └── test_coverage.yml ├── amqp_serde ├── README.md ├── src │ ├── lib.rs │ └── error.rs └── Cargo.toml ├── examples ├── src │ ├── field_table.rs │ ├── longlive_basic_consumer.rs │ ├── callbacks_impl.rs │ ├── tls.rs │ ├── mtls.rs │ └── basic_pub_sub.rs ├── run_examples.sh └── Cargo.toml ├── tidy_style.sh ├── Cargo.toml ├── benchmarks ├── run_bench.sh ├── run_native.sh ├── run_criterion.sh ├── run_consume_benchmark.sh ├── Cargo.toml └── src │ ├── common │ └── mod.rs │ ├── native_pub_amqprs.rs │ ├── native_pub_lapin.rs │ ├── native_consume_amqprs.rs │ ├── native_consume_lapin.rs │ ├── basic_pub_bencher.rs │ └── basic_pub_criterion.rs ├── rabbitmq_conf └── custom.conf ├── regression_test.sh ├── docker-compose.yml ├── LICENSE ├── prepare_release.sh └── start_rabbitmq.sh /README.md: -------------------------------------------------------------------------------- 1 | amqprs/README.md -------------------------------------------------------------------------------- /amqprs/clippy.toml: -------------------------------------------------------------------------------- 1 | msrv = "1.71" 2 | -------------------------------------------------------------------------------- /architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gftea/amqprs/HEAD/architecture.png -------------------------------------------------------------------------------- /amqprs/src/frame/heartbeat.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Debug, Serialize, Deserialize)] 4 | pub struct HeartBeat; 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /.vscode 3 | *.profraw 4 | *.profdata 5 | 6 | # Local RabbitMQ Configuration & Secrets 7 | rabbitmq_conf/client 8 | rabbitmq_conf/server -------------------------------------------------------------------------------- /.github/.codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | patch: 4 | default: 5 | threshold: 0.05% 6 | project: 7 | default: 8 | threshold: 0.05% -------------------------------------------------------------------------------- /amqp_serde/README.md: -------------------------------------------------------------------------------- 1 | # amqp_serde 2 | 3 | AMQP 0-9-1 data types definition and impelementation. 4 | 5 | This crate is being used in [amqprs](https://github.com/gftea/amqprs). -------------------------------------------------------------------------------- /amqprs/src/frame/method/access.rs: -------------------------------------------------------------------------------- 1 | /// Deprecated: see [RabbitMQ Spec Differences](https://www.rabbitmq.com/spec-differences.html) 2 | #[allow(dead_code)] 3 | pub struct Request; // Deprecated 4 | 5 | #[allow(dead_code)] 6 | pub struct RequestOk; // Deprecated 7 | -------------------------------------------------------------------------------- /examples/src/field_table.rs: -------------------------------------------------------------------------------- 1 | use amqprs::FieldName; 2 | use amqprs::FieldTable; 3 | use amqprs::FieldValue; 4 | 5 | fn main() { 6 | let mut ft = FieldTable::new(); 7 | 8 | ft.insert("x-message-ttl".try_into().unwrap(), FieldValue::l(60000)); 9 | } 10 | -------------------------------------------------------------------------------- /amqp_serde/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod de; 2 | mod error; 3 | mod ser; 4 | 5 | /////////////////////////////////////////////// 6 | pub mod types; 7 | pub use de::{from_bytes, Deserializer}; 8 | pub use error::{Error, Result}; 9 | pub use ser::{to_buffer, to_bytes, Serializer}; 10 | -------------------------------------------------------------------------------- /tidy_style.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | 4 | 5 | function trim_trailing_whitespaces () { 6 | src_file=$1 7 | awk -i inplace '{ sub(/[ \t]+$/, ""); print }' $src_file 8 | } 9 | 10 | 11 | target_files=$(find . -type f -name "*.rs" -not -path '*/target/*') 12 | 13 | for src_file in ${target_files[@]}; do 14 | trim_trailing_whitespaces $src_file 15 | done 16 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | 3 | members = ["amqprs", "amqp_serde", "examples", "benchmarks"] 4 | 5 | resolver = "2" 6 | 7 | [workspace.package] 8 | rust-version = "1.71" 9 | 10 | # [profile.bench] 11 | # opt-level = 3 12 | # debug = false 13 | # lto = true 14 | # codegen-units = 1 15 | 16 | 17 | [workspace.dependencies] 18 | serde_bytes = { version = "0.11" } 19 | rustls = { version = "0.23" } 20 | -------------------------------------------------------------------------------- /amqprs/src/frame/method/confirm.rs: -------------------------------------------------------------------------------- 1 | use amqp_serde::types::Boolean; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | #[derive(Debug, Serialize, Deserialize)] 5 | pub struct Select { 6 | no_wait: Boolean, 7 | } 8 | 9 | impl Select { 10 | pub fn new(no_wait: Boolean) -> Self { 11 | Self { no_wait } 12 | } 13 | } 14 | 15 | #[derive(Debug, Serialize, Deserialize)] 16 | pub struct SelectOk; 17 | -------------------------------------------------------------------------------- /amqprs/src/frame/content_body.rs: -------------------------------------------------------------------------------- 1 | use serde::Serialize; 2 | 3 | use super::Frame; 4 | 5 | #[derive(Debug, Serialize)] 6 | pub struct ContentBody { 7 | #[serde(with = "serde_bytes")] 8 | pub(crate) inner: Vec, 9 | } 10 | 11 | impl ContentBody { 12 | pub fn new(inner: Vec) -> Self { 13 | Self { inner } 14 | } 15 | pub fn into_frame(self) -> Frame { 16 | Frame::ContentBody(self) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /amqprs/src/frame/method/tx.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Debug, Serialize, Deserialize)] 4 | pub struct TxSelect; 5 | #[derive(Debug, Serialize, Deserialize)] 6 | pub struct TxSelectOk; 7 | #[derive(Debug, Serialize, Deserialize)] 8 | pub struct TxCommit; 9 | #[derive(Debug, Serialize, Deserialize)] 10 | pub struct TxCommitOk; 11 | #[derive(Debug, Serialize, Deserialize)] 12 | pub struct TxRollback; 13 | #[derive(Debug, Serialize, Deserialize)] 14 | pub struct TxRollbackOk; 15 | -------------------------------------------------------------------------------- /amqprs/src/test_utils.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | use tracing_subscriber::{fmt, prelude::*, EnvFilter}; 3 | 4 | ////////////////////////////////////////////////////////////////// 5 | // construct a subscriber that prints formatted traces to stdout 6 | #[cfg(test)] 7 | pub fn setup_logging() { 8 | // global subscriber with log level according to RUST_LOG 9 | tracing_subscriber::registry() 10 | .with(fmt::layer()) 11 | .with(EnvFilter::from_default_env()) 12 | .try_init() 13 | .ok(); 14 | } 15 | -------------------------------------------------------------------------------- /amqprs/src/frame/error.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | #[derive(Debug)] 4 | pub enum Error { 5 | Corrupted, 6 | SerdeError(String), 7 | } 8 | 9 | impl From for Error { 10 | fn from(err: amqp_serde::Error) -> Self { 11 | Self::SerdeError(err.to_string()) 12 | } 13 | } 14 | 15 | impl fmt::Display for Error { 16 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 17 | match self { 18 | Error::Corrupted => f.write_str("corrupted frame"), 19 | Error::SerdeError(msg) => write!(f, "serde error: {}", msg), 20 | } 21 | } 22 | } 23 | 24 | impl std::error::Error for Error {} 25 | -------------------------------------------------------------------------------- /examples/run_examples.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # set -x 3 | 4 | excludes=("longlive_basic_consumer") 5 | export RUSTFLAGS="$RUSTFLAGS -A dead_code -A unused_variables" 6 | 7 | function is_excluded () { 8 | name=$1 9 | for x in ${excludes[@]}; do 10 | if [[ "$x" = "$name" ]];then 11 | return 1 12 | fi 13 | done 14 | return 0 15 | } 16 | 17 | all_examples=$(cargo run --release --example 2>&1 | grep -E '^ ') 18 | 19 | for example in ${all_examples[@]}; do 20 | is_excluded $example 21 | res=$? 22 | if [[ $res = 0 ]];then 23 | cargo run --release --example $example --all-features 24 | fi 25 | done 26 | -------------------------------------------------------------------------------- /amqp_serde/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "amqp_serde" 3 | version = "0.4.3" 4 | edition = "2021" 5 | rust-version = { workspace = true } 6 | license = "MIT" 7 | description = "Serde implementation for AMQP 0-9-1 types" 8 | repository = "https://github.com/gftea/amqprs" 9 | keywords = ["amqp", "rabbitmq", "serde"] 10 | documentation = "https://docs.rs/amqp_serde/latest/amqp_serde/" 11 | 12 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 13 | 14 | [dependencies] 15 | serde = { version = "1.0", features = ["derive"] } 16 | bytes = { version = "1.0" } 17 | serde_bytes = { workspace = true } 18 | 19 | [dev-dependencies] 20 | -------------------------------------------------------------------------------- /benchmarks/run_bench.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | CARGO_OPTS="-F traces" 4 | TARGET="basic_pub_bencher" 5 | 6 | # build "bench" profile first, might allow cooldown of system before test begins 7 | BUILD_CMD="cargo bench $CARGO_OPTS --no-run" 8 | BENCH_EXE=$(${BUILD_CMD} 2>&1 | egrep "Executable.+${TARGET}.rs" | sed -E 's/.+\((.+)\)/\1/') 9 | echo $BENCH_EXE 10 | 11 | # run separately, otherwise there is runtime conflict/error 12 | ARGS="--bench --verbose" 13 | if [[ $GITHUB_ACTIONS == true ]]; then 14 | ARGS="$ARGS --plotting-backend plotters" 15 | else 16 | ARGS="$ARGS --plotting-backend gnuplot" 17 | fi 18 | 19 | sleep 3 20 | $BENCH_EXE $ARGS amqprs 21 | sleep 3 22 | $BENCH_EXE $ARGS lapin 23 | 24 | -------------------------------------------------------------------------------- /rabbitmq_conf/custom.conf: -------------------------------------------------------------------------------- 1 | auth_mechanisms.1 = PLAIN 2 | auth_mechanisms.2 = AMQPLAIN 3 | auth_mechanisms.3 = EXTERNAL 4 | auth_mechanisms.4 = RABBIT-CR-DEMO 5 | 6 | log.console = true 7 | log.console.level = debug 8 | 9 | log.default.level = debug 10 | listeners.ssl.default = 5671 11 | 12 | ssl_options.cacertfile = /bitnami/tls-test/ca_certificate.pem 13 | ssl_options.certfile = /bitnami/tls-test/server_AMQPRS_TEST_certificate.pem 14 | ssl_options.keyfile = /bitnami/tls-test/server_AMQPRS_TEST_key.pem 15 | ssl_options.verify = verify_peer 16 | ssl_options.fail_if_no_peer_cert = true 17 | ssl_cert_login_from = subject_alternative_name 18 | ssl_cert_login_san_type = dns 19 | ssl_cert_login_san_index = 1 20 | 21 | # private key password 22 | # ssl_options.password = bunnies 23 | -------------------------------------------------------------------------------- /benchmarks/run_native.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | CARGO_OPTS="-F traces" 4 | 5 | # build "bench" profile first, might allow cooldown of system before test begins 6 | BUILD_CMD="cargo bench $CARGO_OPTS --no-run" 7 | 8 | amqprs_exe=$(${BUILD_CMD} 2>&1 | egrep "Executable.+/native_pub_amqprs.rs" | sed -E 's/.+\((.+)\)/\1/') 9 | lapin_exe=$(${BUILD_CMD} 2>&1 | egrep "Executable.+/native_pub_lapin.rs" | sed -E 's/.+\((.+)\)/\1/') 10 | echo $amqprs_exe $lapin_exe 11 | 12 | # run strace's profiling 13 | strace -c $amqprs_exe 14 | strace -c $lapin_exe 15 | 16 | # run perf's profiling 17 | sudo perf stat -d $amqprs_exe 18 | sudo perf stat -d $lapin_exe 19 | 20 | sudo perf record -o perf-amqprs.data $amqprs_exe 21 | sudo perf report -i perf-amqprs.data 22 | 23 | sudo perf record -o perf-lapin.data $lapin_exe 24 | sudo perf report -i perf-lapin.data 25 | -------------------------------------------------------------------------------- /.github/workflows/actions.yml: -------------------------------------------------------------------------------- 1 | name: 'Close stale issues and PRs' 2 | on: 3 | schedule: 4 | - cron: '30 1 * * *' 5 | 6 | jobs: 7 | stale: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/stale@v7 11 | with: 12 | stale-issue-message: 'This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days.' 13 | stale-pr-message: 'This PR is stale because it has been open 45 days with no activity. Remove stale label or comment or this will be closed in 10 days.' 14 | close-issue-message: 'This issue was closed because it has been stalled for 5 days with no activity.' 15 | close-pr-message: 'This PR was closed because it has been stalled for 10 days with no activity.' 16 | days-before-issue-stale: 30 17 | days-before-pr-stale: 45 18 | days-before-issue-close: 5 19 | days-before-pr-close: -1 -------------------------------------------------------------------------------- /regression_test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | #// regression test before release 3 | set -x 4 | 5 | function check_result () { 6 | ret=$? 7 | if [[ $ret != 0 ]];then 8 | echo "early exit due to error!" 9 | exit $ret 10 | fi 11 | } 12 | 13 | # all examples 14 | ./examples/run_examples.sh 15 | check_result 16 | 17 | # features combination 18 | cargo test 19 | check_result 20 | 21 | cargo test -F traces 22 | check_result 23 | 24 | cargo test -F compliance_assert 25 | check_result 26 | 27 | cargo test -F tls 28 | check_result 29 | 30 | cargo test -F urispec 31 | check_result 32 | 33 | cargo test --all-features 34 | check_result 35 | 36 | # clippy, warnings not allowed 37 | cargo clippy --all-features -- -Dwarnings 38 | check_result 39 | 40 | # docs build 41 | cargo doc -p amqprs --all-features --open 42 | check_result 43 | 44 | # cargo msrv 45 | cargo msrv 46 | check_result 47 | 48 | # dry-run publish 49 | cargo publish -p amqprs --all-features --dry-run 50 | check_result 51 | -------------------------------------------------------------------------------- /benchmarks/run_criterion.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -x 4 | 5 | # check environments 6 | # uname -a 7 | # lsb_release -a 8 | # rustc -V 9 | # cargo -V 10 | # cc -v # gcc linker for libc 11 | # lscpu 12 | 13 | # # check dependencies 14 | # cargo tree -i tokio -e all 15 | # cargo tree -i amqprs -e all 16 | # cargo tree -i lapin -e all 17 | 18 | CARGO_OPTS="-F traces" 19 | TARGET="basic_pub_criterion" 20 | 21 | # build "bench" profile first, might allow cooldown of system before test begins 22 | BUILD_CMD="cargo bench $CARGO_OPTS --no-run" 23 | BENCH_EXE=$(${BUILD_CMD} 2>&1 | egrep "Executable.+${TARGET}.rs" | sed -E 's/.+\((.+)\)/\1/') 24 | echo $BENCH_EXE 25 | 26 | # run separately, otherwise there is runtime conflict/error 27 | ARGS="--bench --verbose" 28 | if [[ $GITHUB_ACTIONS == true ]]; then 29 | ARGS="$ARGS --plotting-backend plotters" 30 | else 31 | ARGS="$ARGS --plotting-backend gnuplot" 32 | fi 33 | 34 | sleep 3 35 | $BENCH_EXE $ARGS amqprs 36 | sleep 3 37 | $BENCH_EXE $ARGS lapin 38 | -------------------------------------------------------------------------------- /amqprs/tests/test_update_secret.rs: -------------------------------------------------------------------------------- 1 | use amqprs::{ 2 | callbacks::{DefaultChannelCallback, DefaultConnectionCallback}, 3 | connection::Connection, 4 | }; 5 | mod common; 6 | 7 | #[tokio::test(flavor = "multi_thread", worker_threads = 2)] 8 | async fn test_update_secret() { 9 | common::setup_logging(); 10 | 11 | // open a connection to RabbitMQ server 12 | let args = common::build_conn_args(); 13 | 14 | let connection = Connection::open(&args).await.unwrap(); 15 | connection 16 | .register_callback(DefaultConnectionCallback) 17 | .await 18 | .unwrap(); 19 | // open a channel on the connection 20 | let channel = connection.open_channel(None).await.unwrap(); 21 | channel 22 | .register_callback(DefaultChannelCallback) 23 | .await 24 | .unwrap(); 25 | 26 | connection 27 | .update_secret("123456", "secret expired") 28 | .await 29 | .unwrap(); 30 | 31 | // close 32 | channel.close().await.unwrap(); 33 | connection.close().await.unwrap(); 34 | } 35 | -------------------------------------------------------------------------------- /amqprs/src/api/compliance_asserts.rs: -------------------------------------------------------------------------------- 1 | //! Utility functions for compliance assert 2 | //! See [Specs](https://www.rabbitmq.com/resources/specs/amqp0-9-1.extended.xml) 3 | 4 | #[inline] 5 | pub(crate) fn assert_regexp(value: &str) { 6 | // regexp: [a-zA-Z0-9-_.:] 7 | assert!(!value.chars().any(|c| !c.is_alphanumeric() 8 | && c != '-' 9 | && c != '_' 10 | && c != '.' 11 | && c != ':')); 12 | } 13 | 14 | #[inline] 15 | pub(crate) fn assert_length(value: &str) { 16 | // max length: 127 17 | assert!(value.len() < 128); 18 | } 19 | 20 | #[inline] 21 | pub(crate) fn assert_notnull(value: &str) { 22 | assert_ne!(value, ""); 23 | } 24 | 25 | #[inline] 26 | pub(crate) fn assert_exchange_name(value: &str) { 27 | assert_length(value); 28 | assert_regexp(value); 29 | } 30 | 31 | #[inline] 32 | pub(crate) fn assert_queue_name(value: &str) { 33 | assert_length(value); 34 | assert_regexp(value); 35 | } 36 | 37 | #[inline] 38 | pub(crate) fn assert_path(value: &str) { 39 | assert_notnull(value); 40 | assert_length(value); 41 | } 42 | -------------------------------------------------------------------------------- /amqprs/src/frame/method/mod.rs: -------------------------------------------------------------------------------- 1 | use amqp_serde::types::ShortUint; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | ////////////////////////////////////////////////////////// 5 | mod access; 6 | mod basic; 7 | mod channel; 8 | mod confirm; 9 | mod connection; 10 | mod exchange; 11 | mod queue; 12 | mod tx; 13 | 14 | pub use basic::*; 15 | pub use channel::*; 16 | pub use confirm::*; 17 | pub use connection::*; 18 | pub use exchange::*; 19 | pub use queue::*; 20 | pub use tx::*; 21 | ////////////////////////////////////////////////////////// 22 | #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Hash)] 23 | pub struct MethodHeader { 24 | class_id: ShortUint, 25 | method_id: ShortUint, 26 | } 27 | 28 | impl MethodHeader { 29 | pub const fn new(class_id: ShortUint, method_id: ShortUint) -> Self { 30 | Self { 31 | class_id, 32 | method_id, 33 | } 34 | } 35 | 36 | pub fn class_id(&self) -> ShortUint { 37 | self.class_id 38 | } 39 | 40 | pub fn method_id(&self) -> ShortUint { 41 | self.method_id 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | services: 4 | rabbitmq: 5 | restart: on-failure 6 | image: bitnamilegacy/rabbitmq:3.11.15 7 | expose: 8 | - "15672" 9 | - "5672" 10 | - "5671" 11 | - "15675" 12 | - "1883" 13 | ports: 14 | - "15672:15672" 15 | - "5672:5672" 16 | - "5671:5671" 17 | - "15675:15675" 18 | - "1883:1883" 19 | networks: 20 | integration-net: 21 | aliases: 22 | - rabbitmq 23 | environment: 24 | - RABBITMQ_USERNAME=user 25 | - RABBITMQ_PASSWORD=bitnami 26 | - RABBITMQ_VHOST=/ 27 | - RABBITMQ_PLUGINS=rabbitmq_management,rabbitmq_management_agent,rabbitmq_web_dispatch,rabbitmq_auth_mechanism_ssl 28 | 29 | volumes: 30 | - "./rabbitmq_conf/custom.conf:/bitnami/rabbitmq/conf/custom.conf" 31 | - "./rabbitmq_conf/server:/bitnami/tls-test" 32 | - rmq_data:/bitnami/rabbitmq 33 | 34 | networks: 35 | integration-net: 36 | driver: bridge 37 | 38 | volumes: 39 | rmq_data: 40 | driver_opts: 41 | type: tmpfs 42 | device: tmpfs 43 | o: "size=35840m" 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 gftea 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/benchmarks.yml: -------------------------------------------------------------------------------- 1 | name: bench 2 | run-name: "Benchmarks" 3 | 4 | on: [push, workflow_dispatch] 5 | 6 | env: 7 | CARGO_TERM_COLOR: always 8 | 9 | jobs: 10 | 11 | ubuntu: 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v4 16 | 17 | - name: Start RabbitMQ server 18 | run: ./start_rabbitmq.sh 19 | 20 | - name: run pub native 21 | run: ./benchmarks/run_native.sh 22 | 23 | - name: run pub criterion 24 | run: ./benchmarks/run_criterion.sh 25 | 26 | - name: run consume benchmark 27 | run: ./benchmarks/run_consume_benchmark.sh 28 | 29 | # #! issue with macos: 30 | # #! - take long time to setup docker! 31 | # #! - bench test take significantly much longer time! 32 | # macos: 33 | # runs-on: macos-11 34 | 35 | # steps: 36 | # - uses: actions/checkout@v4 37 | # - uses: docker-practice/actions-setup-docker@master 38 | 39 | # - name: Start RabbitMQ server 40 | # run: ./start_rabbitmq.sh 41 | 42 | # - name: run native executable 43 | # run: ./benchmarks/run_native.sh 44 | 45 | # - name: run bencher 46 | # run: ./benchmarks/run_bench.sh 47 | 48 | # - name: run criterion 49 | # run: ./benchmarks/run_criterion.sh -------------------------------------------------------------------------------- /examples/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "examples" 3 | version = "0.0.0" 4 | publish = false 5 | edition = "2021" 6 | license = "MIT" 7 | description = "Examples for amqprs" 8 | repository = "https://github.com/gftea/amqprs" 9 | 10 | # If you copy one of the examples into a new project, you should be using 11 | # [dependencies] instead, and delete the **path**. 12 | [dev-dependencies] 13 | amqprs = { path = "../amqprs" } 14 | amqp_serde = { path = "../amqp_serde" } 15 | tokio = { version = "1" } 16 | async-trait = { version = "0.1" } 17 | tracing = { version = "0.1" } 18 | tracing-subscriber = { version = "0.3", features = ["env-filter"] } 19 | rustls = { workspace = true } 20 | # features for example target 21 | [features] 22 | example-tls = ["amqprs/tls", "amqprs/traces"] 23 | default = ["amqprs/traces"] 24 | 25 | [[example]] 26 | name = "basic_pub_sub" 27 | path = "src/basic_pub_sub.rs" 28 | 29 | [[example]] 30 | name = "longlive_basic_consumer" 31 | path = "src/longlive_basic_consumer.rs" 32 | 33 | [[example]] 34 | name = "callbacks_impl" 35 | path = "src/callbacks_impl.rs" 36 | 37 | [[example]] 38 | name = "tls" 39 | path = "src/tls.rs" 40 | required-features = ["example-tls"] 41 | 42 | [[example]] 43 | name = "mtls" 44 | path = "src/mtls.rs" 45 | required-features = ["example-tls"] 46 | 47 | [[example]] 48 | name = "field_table" 49 | path = "src/field_table.rs" 50 | -------------------------------------------------------------------------------- /benchmarks/run_consume_benchmark.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # set -x 4 | 5 | CARGO_OPTS="-F traces" 6 | 7 | # build "bench" profile first, might allow cooldown of system before test begins 8 | BUILD_CMD="cargo bench $CARGO_OPTS --no-run" 9 | 10 | amqprs_exe=$(${BUILD_CMD} 2>&1 | egrep "Executable.+/native_consume_amqprs.rs" | sed -E 's/.+\((.+)\)/\1/') 11 | lapin_exe=$(${BUILD_CMD} 2>&1 | egrep "Executable.+/native_consume_lapin.rs" | sed -E 's/.+\((.+)\)/\1/') 12 | echo $amqprs_exe $lapin_exe 13 | 14 | # run strace's profiling 15 | strace -c $amqprs_exe 16 | strace -c $lapin_exe 17 | 18 | # run perf's profiling 19 | sudo perf stat -d $amqprs_exe 20 | sudo perf stat -d $lapin_exe 21 | 22 | sudo perf record -o perf-amqprs.data $amqprs_exe 23 | sudo perf report -i perf-amqprs.data 24 | 25 | sudo perf record -o perf-lapin.data $lapin_exe 26 | sudo perf report -i perf-lapin.data 27 | 28 | # run criterion 29 | TARGET="basic_consume_criterion" 30 | BENCH_EXE=$(${BUILD_CMD} 2>&1 | egrep "Executable.+${TARGET}.rs" | sed -E 's/.+\((.+)\)/\1/') 31 | echo $BENCH_EXE 32 | 33 | # run separately, otherwise there is runtime conflict/error 34 | ARGS="--bench --verbose" 35 | if [[ $GITHUB_ACTIONS == true ]]; then 36 | ARGS="$ARGS --plotting-backend plotters" 37 | else 38 | ARGS="$ARGS --plotting-backend gnuplot" 39 | fi 40 | 41 | sleep 3 42 | $BENCH_EXE $ARGS amqprs 43 | sleep 3 44 | $BENCH_EXE $ARGS lapin -------------------------------------------------------------------------------- /benchmarks/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "benchmarks" 3 | version = "0.0.0" 4 | publish = false 5 | edition = "2021" 6 | license = "MIT" 7 | description = "Benchmarks for amqprs" 8 | repository = "https://github.com/gftea/amqprs" 9 | 10 | [dependencies] 11 | # benchmark tools 12 | criterion_bencher_compat = { version = "0.4.0" } 13 | criterion = { version = "0.4" } 14 | 15 | # common 16 | tokio = { version = "1", features = ["full"] } 17 | async-trait = { version = "0.1" } 18 | tracing = { version = "0.1" } 19 | tracing-subscriber = { version = "0.3", features = ["env-filter"] } 20 | 21 | # amqprs client 22 | amqprs = { path = "../amqprs" } 23 | 24 | # lapin client 25 | lapin = { version = "*" } 26 | tokio-executor-trait = "2.1.0" 27 | tokio-reactor-trait = "1.1.0" 28 | 29 | # [features] 30 | 31 | 32 | [[bench]] 33 | name = "basic_pub_bencher" 34 | path = "src/basic_pub_bencher.rs" 35 | harness = false 36 | 37 | [[bench]] 38 | name = "basic_pub_criterion" 39 | path = "src/basic_pub_criterion.rs" 40 | harness = false 41 | 42 | [[bench]] 43 | name = "native_pub_amqprs" 44 | path = "src/native_pub_amqprs.rs" 45 | harness = false 46 | 47 | [[bench]] 48 | name = "native_pub_lapin" 49 | path = "src/native_pub_lapin.rs" 50 | harness = false 51 | 52 | 53 | [[bench]] 54 | name = "basic_consume_criterion" 55 | path = "src/basic_consume_criterion.rs" 56 | harness = false 57 | 58 | 59 | [[bench]] 60 | name = "native_consume_amqprs" 61 | path = "src/native_consume_amqprs.rs" 62 | harness = false 63 | 64 | [[bench]] 65 | name = "native_consume_lapin" 66 | path = "src/native_consume_lapin.rs" 67 | harness = false 68 | -------------------------------------------------------------------------------- /prepare_release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | #// prepare release 3 | 4 | function check_result () { 5 | ret=$? 6 | if [[ $ret != 0 ]];then 7 | echo "early exit due to error!" 8 | exit $ret 9 | fi 10 | } 11 | 12 | # get semver of amqprs 13 | version=$(egrep '^version = "(.+)"$' -o amqprs/Cargo.toml | cut -d '"' -f2) 14 | 15 | # check semver 16 | semver_regex="[0-9]+\.[0-9]+\.[0-9]+" 17 | if ! [[ $version =~ $semver_regex ]]; then 18 | echo "error, check semantic version: '$version'" 19 | exit 1 20 | fi 21 | 22 | # dry run publish check 23 | cargo publish -p amqprs --all-features --dry-run --allow-dirty 24 | check_result 25 | 26 | # check contents to be packaged into crate 27 | cargo package --list --allow-dirty 28 | check_result 29 | 30 | # check size of crate 31 | ls -hl target/package/amqprs-${version}.crate 32 | check_result 33 | 34 | read -p "Are you going to release v${version}? " ans 35 | if [ "$ans" != "y" ]; then 36 | exit 0 37 | fi 38 | 39 | read -p 'make a commit? ' ans 40 | if [ "$ans" = "y" ]; then 41 | read -p 'additional commit message: ' message 42 | git commit -a -m "prepare release v${version}. ${message}" 43 | git log -1 44 | fi 45 | 46 | read -p 'push commit? ' ans 47 | if [ "$ans" = "y" ]; then 48 | git push 49 | fi 50 | 51 | read -p "push tag v${version}? " ans 52 | if [ "$ans" = "y" ]; then 53 | git tag -a "v${version}" -m "v${version}" 54 | git push origin "v${version}" 55 | git log -1 56 | fi 57 | 58 | read -p 'Want to publish to crates.io? ' ans 59 | if [ "$ans" = "y" ]; then 60 | cargo publish -p amqprs --all-features 61 | fi 62 | -------------------------------------------------------------------------------- /amqprs/src/net/error.rs: -------------------------------------------------------------------------------- 1 | use std::{fmt, io}; 2 | 3 | use crate::frame; 4 | use tokio::sync::mpsc::error::SendError; 5 | 6 | #[derive(Debug)] 7 | pub(crate) enum Error { 8 | NetworkIo(String), 9 | SyncChannel(String), 10 | Serde(String), 11 | Framing(String), 12 | Callback, 13 | PeerShutdown, 14 | Interrupted, 15 | } 16 | 17 | impl From for Error { 18 | fn from(err: io::Error) -> Self { 19 | Error::NetworkIo(err.to_string()) 20 | } 21 | } 22 | impl From for Error { 23 | fn from(err: amqp_serde::Error) -> Self { 24 | Error::Serde(err.to_string()) 25 | } 26 | } 27 | impl From for Error { 28 | fn from(err: frame::Error) -> Self { 29 | Error::Framing(err.to_string()) 30 | } 31 | } 32 | impl From> for Error { 33 | fn from(err: SendError) -> Self { 34 | Error::SyncChannel(err.to_string()) 35 | } 36 | } 37 | 38 | impl fmt::Display for Error { 39 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 40 | match self { 41 | Error::NetworkIo(msg) => write!(f, "network io error: {}", msg), 42 | Error::SyncChannel(msg) => write!(f, "internal communication error: {}", msg), 43 | Error::Serde(msg) => write!(f, "serde error: {}", msg), 44 | Error::Framing(msg) => write!(f, "framing error: {}", msg), 45 | Error::Callback => write!(f, "callback error"), 46 | Error::PeerShutdown => f.write_str("peer shutdown"), 47 | Error::Interrupted => f.write_str("connection interrupted"), 48 | } 49 | } 50 | } 51 | 52 | impl std::error::Error for Error {} 53 | -------------------------------------------------------------------------------- /amqprs/src/net/mod.rs: -------------------------------------------------------------------------------- 1 | mod channel_id_repo; 2 | mod channel_manager; 3 | mod error; 4 | mod reader_handler; 5 | mod split_connection; 6 | mod writer_handler; 7 | 8 | pub(crate) use channel_manager::*; 9 | pub(crate) use error::*; 10 | pub(crate) use reader_handler::*; 11 | pub(crate) use split_connection::*; 12 | pub(crate) use writer_handler::*; 13 | ///////////////////////////////////////////////////////////////////////////// 14 | use crate::{ 15 | api::callbacks::ConnectionCallback, 16 | frame::{Frame, MethodHeader}, 17 | }; 18 | use amqp_serde::types::AmqpChannelId; 19 | use tokio::sync::oneshot; 20 | 21 | pub type OutgoingMessage = (AmqpChannelId, Frame); 22 | 23 | pub(crate) type IncomingMessage = Frame; 24 | 25 | pub(crate) struct RegisterChannelResource { 26 | /// If None, `net` handler will allocate a channel id for client 27 | pub channel_id: Option, 28 | /// send `None` to client if `net` handler fail to allocate a channel id 29 | pub acker: oneshot::Sender>, 30 | pub resource: ChannelResource, 31 | } 32 | 33 | pub(crate) struct RegisterResponder { 34 | pub channel_id: AmqpChannelId, 35 | pub method_header: &'static MethodHeader, 36 | pub responder: oneshot::Sender, 37 | pub acker: oneshot::Sender<()>, 38 | } 39 | 40 | pub(crate) struct RegisterConnectionCallback { 41 | pub callback: Box, 42 | } 43 | 44 | pub(crate) enum ConnManagementCommand { 45 | RegisterChannelResource(RegisterChannelResource), 46 | DeregisterChannelResource(AmqpChannelId), 47 | 48 | RegisterResponder(RegisterResponder), 49 | RegisterConnectionCallback(RegisterConnectionCallback), 50 | } 51 | -------------------------------------------------------------------------------- /amqprs/tests/common/mod.rs: -------------------------------------------------------------------------------- 1 | use amqprs::connection::OpenConnectionArguments; 2 | use tracing_subscriber::{fmt, prelude::*, EnvFilter}; 3 | 4 | // construct a subscriber that prints formatted traces to stdout 5 | pub fn setup_logging() { 6 | // global subscriber with log level according to RUST_LOG 7 | tracing_subscriber::registry() 8 | .with(fmt::layer()) 9 | .with(EnvFilter::from_default_env()) 10 | .try_init() 11 | .ok(); 12 | } 13 | 14 | #[cfg(not(feature = "tls"))] 15 | pub fn build_conn_args() -> OpenConnectionArguments { 16 | OpenConnectionArguments::new("localhost", 5672, "user", "bitnami") 17 | } 18 | #[cfg(feature = "tls")] 19 | pub fn build_conn_args() -> OpenConnectionArguments { 20 | let _ = rustls::crypto::aws_lc_rs::default_provider().install_default(); 21 | 22 | // TLS specific configuration 23 | let current_dir = std::env::current_dir().unwrap(); 24 | let current_dir = current_dir.join("../rabbitmq_conf/client/"); 25 | 26 | let root_ca_cert = current_dir.join("ca_certificate.pem"); 27 | let client_cert = current_dir.join("client_AMQPRS_TEST_certificate.pem"); 28 | let client_private_key = current_dir.join("client_AMQPRS_TEST_key.pem"); 29 | // domain should match the certificate/key files 30 | let domain = "AMQPRS_TEST"; 31 | OpenConnectionArguments::new("localhost", 5671, "user", "bitnami") 32 | .tls_adaptor( 33 | amqprs::tls::TlsAdaptor::with_client_auth( 34 | Some(root_ca_cert.as_path()), 35 | client_cert.as_path(), 36 | client_private_key.as_path(), 37 | domain.to_owned(), 38 | ) 39 | .unwrap(), 40 | ) 41 | .finish() 42 | } 43 | -------------------------------------------------------------------------------- /amqprs/src/frame/protocol_header.rs: -------------------------------------------------------------------------------- 1 | use amqp_serde::types::*; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | #[derive(Serialize, Deserialize, Debug, PartialEq)] 5 | struct ProtocolName(Octect, Octect, Octect, Octect); 6 | 7 | #[derive(Serialize, Deserialize, Debug, PartialEq)] 8 | struct ProtocolVersion { 9 | major: Octect, 10 | minor: Octect, 11 | revision: Octect, 12 | } 13 | 14 | #[derive(Serialize, Deserialize, Debug)] 15 | pub struct ProtocolHeader { 16 | name: ProtocolName, 17 | id: Octect, 18 | version: ProtocolVersion, 19 | } 20 | 21 | impl Default for ProtocolHeader { 22 | fn default() -> Self { 23 | Self { 24 | name: ProtocolName(b'A', b'M', b'Q', b'P'), 25 | id: 0, 26 | version: ProtocolVersion { 27 | major: 0, 28 | minor: 9, 29 | revision: 1, 30 | }, 31 | } 32 | } 33 | } 34 | 35 | #[cfg(test)] 36 | mod tests { 37 | use amqp_serde::{from_bytes, to_bytes}; 38 | 39 | use crate::frame::protocol_header::{ProtocolName, ProtocolVersion}; 40 | 41 | use super::ProtocolHeader; 42 | 43 | #[test] 44 | fn test_serialize() { 45 | let data = ProtocolHeader::default(); 46 | let frame = to_bytes(&data).unwrap(); 47 | assert_eq!([65, 77, 81, 80, 0, 0, 9, 1].to_vec(), frame); 48 | } 49 | 50 | #[test] 51 | fn test_deserialize() { 52 | let data = [65, 77, 81, 80, 0, 0, 9, 1]; 53 | let frame: ProtocolHeader = from_bytes(&data).unwrap(); 54 | let ProtocolHeader { name, id, version } = frame; 55 | assert_eq!(ProtocolName(b'A', b'M', b'Q', b'P'), name); 56 | assert_eq!(0, id); 57 | assert_eq!( 58 | ProtocolVersion { 59 | major: 0, 60 | minor: 9, 61 | revision: 1 62 | }, 63 | version 64 | ); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /start_rabbitmq.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | COMMON_NAME=AMQPRS_TEST 4 | USERNAME=user 5 | 6 | # Create directories for rabbitmq server and client and alter permissions 7 | #------------------------ 8 | mkdir -p rabbitmq_conf/server 9 | mkdir -p rabbitmq_conf/client 10 | sudo chown -R 1001:root rabbitmq_conf/server 11 | sudo chmod 755 rabbitmq_conf/server 12 | sudo chmod 400 rabbitmq_conf/server/* 13 | sudo chmod 444 rabbitmq_conf/client/* 14 | 15 | # generate tls cert/key 16 | #------------------------ 17 | git clone https://github.com/rabbitmq/tls-gen tls-gen 18 | cd tls-gen/basic 19 | make CN=$COMMON_NAME CLIENT_ALT_NAME=$USERNAME 20 | make verify CN=$COMMON_NAME 21 | make info CN=$COMMON_NAME 22 | ls -lha ./result 23 | cd - 24 | 25 | # copy client files 26 | sudo cp tls-gen/basic/result/ca_* rabbitmq_conf/client 27 | sudo cp tls-gen/basic/result/client_* rabbitmq_conf/client 28 | # copy server files 29 | sudo cp tls-gen/basic/result/ca_* rabbitmq_conf/server 30 | sudo cp tls-gen/basic/result/server_* rabbitmq_conf/server 31 | # clean up 32 | rm -rf tls-gen 33 | 34 | # to make sure the cert/key files have correct permissions 35 | # and owners within container after bind mount 36 | #------------------------ 37 | 38 | # copy server files to temparory folder for test 39 | # `1001` is the default user of `bitnami/rabbitmq` container 40 | sudo chown -R 1001:root rabbitmq_conf/server 41 | # strict permissions is mandatory for TLS cert/key files 42 | sudo chmod 755 rabbitmq_conf/server 43 | sudo chmod 400 rabbitmq_conf/server/* 44 | sudo chmod 444 rabbitmq_conf/client/* 45 | 46 | # start rabbitmq server 47 | docker compose down 48 | docker compose up -d 49 | 50 | # # verify tls connection 51 | # echo "---------- Start rabbitmq now, then come back ... ---------------" 52 | # read -p "After rabbitmq started, press 'y' to verify TLS connection: " ans 53 | # if [ "$ans" = "y" ]; then 54 | # cd rabbitmq_conf/client 55 | # openssl s_client -connect localhost:5671 -cert client_${COMMON_NAME}_certificate.pem -key client_${COMMON_NAME}_key.pem -CAfile ca_certificate.pem 56 | # fi -------------------------------------------------------------------------------- /amqprs/src/api/mod.rs: -------------------------------------------------------------------------------- 1 | use self::error::Error; 2 | use crate::FieldTable; 3 | 4 | pub(in crate::api) type Result = std::result::Result; 5 | 6 | // macro should appear before module declaration 7 | #[macro_use] 8 | pub(crate) mod helpers { 9 | 10 | macro_rules! synchronous_request { 11 | ($tx:expr, $msg:expr, $rx:expr, $response:path, $err:path) => {{ 12 | $tx.send($msg).await?; 13 | match $rx.await? { 14 | $response(_, method) => Ok(method), 15 | unexpected => Err($err(unexpected.to_string())), 16 | } 17 | }}; 18 | } 19 | 20 | macro_rules! unwrap_expected_method { 21 | ($frame:expr, $variant:path, $err:expr) => { 22 | match $frame { 23 | $variant(_, method) => Ok(method), 24 | _ => Err($err), 25 | } 26 | }; 27 | } 28 | 29 | macro_rules! impl_chainable_setter { 30 | ($(#[$($attrss:tt)*])* $field_name:ident, $input_type:ty) => { 31 | $(#[$($attrss)*])* 32 | pub fn $field_name(&mut self, $field_name: $input_type) -> &mut Self { 33 | self.$field_name = $field_name; 34 | self 35 | } 36 | 37 | }; 38 | } 39 | 40 | macro_rules! impl_chainable_alias_setter { 41 | ($(#[$($attrss:tt)*])* $method_name:ident, $field_name:ident, $input_type:ty) => { 42 | $(#[$($attrss)*])* 43 | pub fn $method_name(&mut self, $field_name: $input_type) -> &mut Self { 44 | self.$field_name = $field_name; 45 | self 46 | } 47 | 48 | }; 49 | } 50 | 51 | // pub(crate) use impl_chainable_setter; 52 | } 53 | 54 | ///////////////////////////////////////////////////////////////////////////// 55 | #[cfg(feature = "compliance_assert")] 56 | mod compliance_asserts; 57 | #[cfg(feature = "tls")] 58 | pub mod tls; 59 | 60 | pub mod callbacks; 61 | pub mod channel; 62 | pub mod connection; 63 | pub mod consumer; 64 | pub mod error; 65 | pub mod security; 66 | -------------------------------------------------------------------------------- /amqprs/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "amqprs" 3 | version = "2.1.3" 4 | edition = "2021" 5 | rust-version = { workspace = true } 6 | license = "MIT" 7 | readme = "README.md" 8 | description = "AMQP 0-9-1 client implementation for RabbitMQ" 9 | repository = "https://github.com/gftea/amqprs" 10 | keywords = ["amqp", "rabbitmq", "async", "tokio", "lock-free"] 11 | categories = ["network-programming", "asynchronous"] 12 | documentation = "https://docs.rs/amqprs/latest/amqprs/" 13 | 14 | exclude = ["*.profraw", "*.profdata"] 15 | 16 | [package.metadata.docs.rs] 17 | # Whether to pass `--all-features` to Cargo (default: false) 18 | all-features = true 19 | 20 | 21 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 22 | [features] 23 | default = [] 24 | compliance_assert = [] 25 | traces = ["tracing"] 26 | tls = [ 27 | "tokio-rustls", 28 | "rustls-pemfile", 29 | "webpki-roots", 30 | "rustls-pki-types", 31 | "rustls-webpki", 32 | ] 33 | urispec = ["uriparse"] 34 | 35 | [dependencies] 36 | tokio = { version = "1", features = [ 37 | "rt", 38 | "rt-multi-thread", 39 | "sync", 40 | "net", 41 | "io-util", 42 | "time", 43 | "macros", 44 | ] } 45 | bytes = { version = "1.2" } 46 | serde = { version = "1.0", features = ["derive"] } 47 | amqp_serde = { path = "../amqp_serde", version = "0.4.3" } 48 | async-trait = "0.1" 49 | tracing = { version = "0.1", optional = true } 50 | uriparse = { version = "0.6", optional = true } 51 | serde_bytes = { workspace = true } 52 | 53 | # SSL/TLS dependencies 54 | tokio-rustls = { version = "0.26", optional = true, default-features = false } 55 | rustls-pemfile = { version = "2.1.2", optional = true } 56 | rustls-webpki = { version = "0.103", optional = true, default-features = false } 57 | webpki-roots = { version = "0.26", optional = true } 58 | rustls-pki-types = { version = "1.7.0", optional = true } 59 | 60 | [dev-dependencies] 61 | tracing = { version = "0.1" } 62 | tracing-subscriber = { version = "0.3", features = ["env-filter"] } 63 | rustls = { workspace = true, features = ["aws-lc-rs"] } 64 | -------------------------------------------------------------------------------- /amqprs/tests/test_publish_return.rs: -------------------------------------------------------------------------------- 1 | use amqprs::{ 2 | callbacks::{DefaultChannelCallback, DefaultConnectionCallback}, 3 | channel::BasicPublishArguments, 4 | connection::Connection, 5 | BasicProperties, 6 | }; 7 | use tokio::time; 8 | mod common; 9 | 10 | #[tokio::test(flavor = "multi_thread", worker_threads = 2)] 11 | async fn test_publish_return() { 12 | common::setup_logging(); 13 | 14 | // open a connection to RabbitMQ server 15 | let args = common::build_conn_args(); 16 | 17 | let connection = Connection::open(&args).await.unwrap(); 18 | connection 19 | .register_callback(DefaultConnectionCallback) 20 | .await 21 | .unwrap(); 22 | 23 | // open a channel on the connection 24 | let channel = connection.open_channel(None).await.unwrap(); 25 | channel 26 | .register_callback(DefaultChannelCallback) 27 | .await 28 | .unwrap(); 29 | 30 | let exchange_name = "amq.topic"; 31 | let exchange_type = "topic"; 32 | 33 | // publish with mandatory = true 34 | // message is not routable due to no queue yet 35 | let args = BasicPublishArguments::new(exchange_name, exchange_type) 36 | .mandatory(true) 37 | .finish(); 38 | 39 | channel 40 | .basic_publish( 41 | BasicProperties::default(), 42 | b"undeliverable message".to_vec(), 43 | args.clone(), 44 | ) 45 | .await 46 | .unwrap(); 47 | 48 | // publish zero size contnet 49 | channel 50 | .basic_publish(BasicProperties::default(), Vec::new(), args.clone()) 51 | .await 52 | .unwrap(); 53 | // publish message of size > frame_max 54 | let body_size = connection.frame_max() as usize + 10; 55 | channel 56 | .basic_publish(BasicProperties::default(), vec![1; body_size], args.clone()) 57 | .await 58 | .unwrap(); 59 | // wait for publish is done 60 | time::sleep(time::Duration::from_secs(2)).await; 61 | 62 | // close 63 | channel.close().await.unwrap(); 64 | connection.close().await.unwrap(); 65 | } 66 | -------------------------------------------------------------------------------- /amqprs/src/frame/constants.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | use amqp_serde::types::{LongUint, Octect, ShortUint}; 4 | 5 | pub const FRAME_HEADER_SIZE: usize = 7; 6 | pub const DEFAULT_CONN_CHANNEL: ShortUint = 0; 7 | 8 | pub const FRAME_METHOD: Octect = 1; 9 | pub const FRAME_CONTENT_HEADER: Octect = 2; 10 | pub const FRAME_CONTENT_BODY: Octect = 3; 11 | pub const FRAME_HEARTBEAT: Octect = 8; 12 | 13 | pub const FRAME_END: Octect = 206; 14 | 15 | pub const FRAME_MIN_SIZE: LongUint = 8192; 16 | 17 | /// all reply code are unsigned 16bit integer 18 | pub const REPLY_SUCCESS: ShortUint = 200; //This reply code is reserved for future use 19 | 20 | /// soft error for channel 21 | pub const CONTENT_TOO_LARGE: ShortUint = 311; 22 | pub const NO_ROUTE: ShortUint = 312; 23 | pub const NO_CONSUMERS: ShortUint = 313; 24 | pub const ACCESS_REFUSED: ShortUint = 403; 25 | pub const NOT_FOUND: ShortUint = 404; 26 | pub const RESOURCE_LOCKED: ShortUint = 405; 27 | pub const PRECONDITION_FAILED: ShortUint = 406; 28 | 29 | /// hard error for connection 30 | pub const CONNECTION_FORCED: ShortUint = 320; 31 | pub const INVALID_PATH: ShortUint = 402; 32 | pub const FRAME_ERROR: ShortUint = 501; 33 | pub const SYNTAX_ERROR: ShortUint = 502; 34 | pub const COMMAND_INVALID: ShortUint = 503; 35 | pub const CHANNEL_ERROR: ShortUint = 504; 36 | pub const UNEXPECTED_FRAME: ShortUint = 505; 37 | pub const RESOURCE_ERROR: ShortUint = 506; 38 | pub const NOT_ALLOWED: ShortUint = 530; 39 | pub const NOT_IMPLEMENTED: ShortUint = 540; 40 | pub const INTERNAL_ERROR: ShortUint = 541; 41 | 42 | /// class id 43 | pub const CLASS_CONNECTION: ShortUint = 10; 44 | pub const CLASS_CHANNEL: ShortUint = 20; 45 | pub const CLASS_EXCHANGE: ShortUint = 40; 46 | pub const CLASS_QUEUE: ShortUint = 50; 47 | pub const CLASS_BASIC: ShortUint = 60; 48 | pub const CLASS_TX: ShortUint = 90; 49 | 50 | /// Delivery Mode: transient/non-persistent 51 | /// 52 | /// See [Delivery Metadata](https://www.rabbitmq.com/consumers.html#message-properties). 53 | pub const DELIVERY_MODE_TRANSIENT: u8 = 1; 54 | 55 | /// Delivery Mode: persistent 56 | /// 57 | /// See [Delivery Metadata](https://www.rabbitmq.com/consumers.html#message-properties). 58 | pub const DELIVERY_MODE_PERSISTENT: u8 = 2; 59 | -------------------------------------------------------------------------------- /amqp_serde/src/error.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Serde Developers 2 | // 3 | // Licensed under the Apache License, Version 2.0 or the MIT license 5 | // , at your 6 | // option. This file may not be copied, modified, or distributed 7 | // except according to those terms. 8 | 9 | use serde::{de, ser}; 10 | use std::fmt; 11 | 12 | pub type Result = std::result::Result; 13 | 14 | // This is a bare-bones implementation. A real library would provide additional 15 | // information in its error type, for example the line and column at which the 16 | // error occurred, the byte offset into the input, or the current key being 17 | // processed. 18 | #[derive(Debug)] 19 | pub enum Error { 20 | // One or more variants that can be created by data structures through the 21 | // `ser::Error` and `de::Error` traits. For example the Serialize impl for 22 | // Mutex might return an error because the mutex is poisoned, or the 23 | // Deserialize impl for a struct may return an error because a required 24 | // field is missing. 25 | Message(String), 26 | 27 | // Zero or more variants that can be created directly by the Serializer and 28 | // Deserializer without going through `ser::Error` and `de::Error`. These 29 | // are specific to the format, in this case JSON. 30 | Eof, 31 | Syntax, 32 | Incomplete, 33 | ExpectedLength, 34 | } 35 | 36 | impl ser::Error for Error { 37 | fn custom(msg: T) -> Self { 38 | Error::Message(msg.to_string()) 39 | } 40 | } 41 | 42 | impl de::Error for Error { 43 | fn custom(msg: T) -> Self { 44 | Error::Message(msg.to_string()) 45 | } 46 | } 47 | 48 | impl fmt::Display for Error { 49 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 50 | match self { 51 | Error::Message(msg) => write!(f, "{}", msg), 52 | Error::Eof => f.write_str("unexpected end of input"), 53 | Error::Syntax => f.write_str("unexpected syntax"), 54 | Error::Incomplete => f.write_str("incomplete deserializaton"), 55 | Error::ExpectedLength => f.write_str("expect length value before raw bytes"), 56 | } 57 | } 58 | } 59 | 60 | impl std::error::Error for Error {} 61 | -------------------------------------------------------------------------------- /amqprs/tests/test_heartbeat.rs: -------------------------------------------------------------------------------- 1 | use amqprs::{ 2 | channel::{BasicConsumeArguments, QueueBindArguments, QueueDeclareArguments}, 3 | connection::Connection, 4 | consumer::DefaultConsumer, 5 | }; 6 | 7 | use tokio::time; 8 | use tracing::info; 9 | mod common; 10 | 11 | #[tokio::test(flavor = "multi_thread", worker_threads = 2)] 12 | async fn test_customized_heartbeat() { 13 | common::setup_logging(); 14 | 15 | // open a connection to RabbitMQ server, 16 | let mut args = common::build_conn_args(); 17 | // set heartbeat = 10s 18 | args.heartbeat(10); 19 | 20 | let connection = Connection::open(&args).await.unwrap(); 21 | 22 | // open a channel on the connection 23 | let channel = connection.open_channel(None).await.unwrap(); 24 | 25 | // declare a queue 26 | let (queue_name, ..) = channel 27 | .queue_declare(QueueDeclareArguments::default()) 28 | .await 29 | .unwrap() 30 | .unwrap(); 31 | 32 | // bind the queue to exchange 33 | channel 34 | .queue_bind(QueueBindArguments::new( 35 | &queue_name, 36 | "amq.topic", 37 | "eiffel.#", 38 | )) 39 | .await 40 | .unwrap(); 41 | 42 | // start consumer with given name 43 | let args = BasicConsumeArguments::new(&queue_name, "test_multi_consume"); 44 | 45 | channel 46 | .basic_consume(DefaultConsumer::new(args.no_ack), args) 47 | .await 48 | .unwrap(); 49 | 50 | info!("-------------no heartbeat required--------------------"); 51 | 52 | // normal interval is the heartbeat timeout / 2 53 | let interval: u64 = (connection.heartbeat() / 2).into(); 54 | let num_loop = 5; 55 | 56 | for _ in 0..num_loop { 57 | time::sleep(time::Duration::from_secs(interval - 1)).await; 58 | // if any frame sent between heartbeat deadline, the heartbeat deadline will be updated. 59 | // during this loop, do not expect any heartbeat sent 60 | channel.flow(true).await.unwrap(); 61 | } 62 | 63 | info!("------------------now heartbeat should be sent as required interval------------------"); 64 | 65 | // now heartbeat should be sent as required interval 66 | time::sleep(time::Duration::from_secs(interval * num_loop)).await; 67 | channel.close().await.unwrap(); 68 | connection.close().await.unwrap(); 69 | } 70 | -------------------------------------------------------------------------------- /examples/src/longlive_basic_consumer.rs: -------------------------------------------------------------------------------- 1 | use amqprs::{ 2 | callbacks::{DefaultChannelCallback, DefaultConnectionCallback}, 3 | channel::{BasicConsumeArguments, QueueBindArguments, QueueDeclareArguments}, 4 | connection::{Connection, OpenConnectionArguments}, 5 | consumer::DefaultConsumer, 6 | }; 7 | use tokio::sync::Notify; 8 | 9 | use tracing_subscriber::{fmt, prelude::*, EnvFilter}; 10 | 11 | #[tokio::main(flavor = "multi_thread", worker_threads = 2)] 12 | async fn main() { 13 | // construct a subscriber that prints formatted traces to stdout 14 | // global subscriber with log level according to RUST_LOG 15 | tracing_subscriber::registry() 16 | .with(fmt::layer()) 17 | .with(EnvFilter::from_default_env()) 18 | .try_init() 19 | .ok(); 20 | 21 | // open a connection to RabbitMQ server 22 | let connection = Connection::open(&OpenConnectionArguments::new( 23 | "localhost", 24 | 5672, 25 | "user", 26 | "bitnami", 27 | )) 28 | .await 29 | .unwrap(); 30 | connection 31 | .register_callback(DefaultConnectionCallback) 32 | .await 33 | .unwrap(); 34 | 35 | // open a channel on the connection 36 | let channel = connection.open_channel(None).await.unwrap(); 37 | channel 38 | .register_callback(DefaultChannelCallback) 39 | .await 40 | .unwrap(); 41 | 42 | // declare a server-named transient queue 43 | let (queue_name, _, _) = channel 44 | .queue_declare(QueueDeclareArguments::default()) 45 | .await 46 | .unwrap() 47 | .unwrap(); 48 | 49 | // bind the queue to exchange 50 | let routing_key = "amqprs.example"; 51 | let exchange_name = "amq.topic"; 52 | channel 53 | .queue_bind(QueueBindArguments::new( 54 | &queue_name, 55 | exchange_name, 56 | routing_key, 57 | )) 58 | .await 59 | .unwrap(); 60 | 61 | ////////////////////////////////////////////////////////////////////////////// 62 | // start consumer, auto ack 63 | let args = BasicConsumeArguments::new(&queue_name, "basic_consumer") 64 | .manual_ack(false) 65 | .finish(); 66 | 67 | channel 68 | .basic_consume(DefaultConsumer::new(args.no_ack), args) 69 | .await 70 | .unwrap(); 71 | 72 | // consume forever 73 | println!("consume forever..., ctrl+c to exit"); 74 | let guard = Notify::new(); 75 | guard.notified().await; 76 | } 77 | -------------------------------------------------------------------------------- /amqprs/tests/test_publish.rs: -------------------------------------------------------------------------------- 1 | use amqprs::{ 2 | callbacks::{DefaultChannelCallback, DefaultConnectionCallback}, 3 | channel::{BasicPublishArguments, ExchangeDeclareArguments, ExchangeType}, 4 | connection::Connection, 5 | BasicProperties, 6 | }; 7 | use tokio::time; 8 | mod common; 9 | 10 | #[tokio::test(flavor = "multi_thread", worker_threads = 2)] 11 | async fn test_publish() { 12 | common::setup_logging(); 13 | 14 | // open a connection to RabbitMQ server 15 | let args = common::build_conn_args(); 16 | 17 | let connection = Connection::open(&args).await.unwrap(); 18 | connection 19 | .register_callback(DefaultConnectionCallback) 20 | .await 21 | .unwrap(); 22 | // open a channel on the connection 23 | let channel = connection.open_channel(None).await.unwrap(); 24 | channel 25 | .register_callback(DefaultChannelCallback) 26 | .await 27 | .unwrap(); 28 | 29 | let exchange_name = "amq.topic"; 30 | 31 | // create arguments for exchange_declare 32 | let args = ExchangeDeclareArguments::of_type(exchange_name, ExchangeType::Topic) 33 | .passive(true) 34 | .finish(); 35 | 36 | // declare exchange 37 | channel.exchange_declare(args).await.unwrap(); 38 | 39 | // contents to publish 40 | let content = String::from( 41 | r#" 42 | { 43 | "data": "publish test", 44 | } 45 | "#, 46 | ) 47 | .into_bytes(); 48 | 49 | // create arguments for basic_publish 50 | let args = BasicPublishArguments::new(exchange_name, "amqprs.example"); 51 | 52 | let num_loop = 5000; 53 | for _ in 0..num_loop { 54 | // basic publish 55 | channel 56 | .basic_publish(BasicProperties::default(), content.clone(), args.clone()) 57 | .await 58 | .unwrap(); 59 | } 60 | for _ in 0..num_loop { 61 | // publish zero size content 62 | channel 63 | .basic_publish(BasicProperties::default(), content.clone(), args.clone()) 64 | .await 65 | .unwrap(); 66 | } 67 | // publish message of size > frame_max 68 | let body_size = connection.frame_max() as usize + 10; 69 | channel 70 | .basic_publish(BasicProperties::default(), vec![1; body_size], args.clone()) 71 | .await 72 | .unwrap(); 73 | 74 | // wait for publish is done 75 | time::sleep(time::Duration::from_secs(10)).await; 76 | 77 | // close 78 | channel.close().await.unwrap(); 79 | connection.close().await.unwrap(); 80 | } 81 | -------------------------------------------------------------------------------- /.github/workflows/regression_test.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | run-name: "Regression Test" 3 | 4 | 5 | on: 6 | schedule: 7 | - cron: '30 1 * * *' 8 | 9 | push: 10 | branches: ["main"] 11 | paths-ignore: 12 | - '**/README.md' 13 | - '**/*.png' 14 | - 'rabbitmq_spec/**' 15 | - 'prepare_release.sh' 16 | - 'regression_test.sh' 17 | 18 | workflow_dispatch: 19 | 20 | pull_request: 21 | branches: ["main"] 22 | paths-ignore: 23 | - '**/README.md' 24 | - '**/*.png' 25 | - 'rabbitmq_spec/**' 26 | - 'prepare_release.sh' 27 | - 'regression_test.sh' 28 | 29 | env: 30 | CARGO_TERM_COLOR: always 31 | 32 | jobs: 33 | builds: 34 | runs-on: ubuntu-latest 35 | 36 | steps: 37 | - uses: actions/checkout@v4 38 | 39 | - name: Check rust version 40 | run: rustc -V; cargo -V; rustup -V 41 | 42 | - name: cargo build (debug, all-features) 43 | run: cargo build --all-features --verbose 44 | 45 | - name: cargo clippy (all-features) 46 | run: cargo clippy --all-features -- -Dwarnings 47 | 48 | check_msrv: 49 | runs-on: ubuntu-latest 50 | 51 | strategy: 52 | matrix: 53 | rust: 54 | - stable 55 | - '1.71' 56 | steps: 57 | - uses: actions/checkout@v4 58 | - uses: actions-rs/toolchain@v1 59 | with: 60 | profile: minimal 61 | toolchain: ${{ matrix.rust }} 62 | override: true 63 | - uses: actions-rs/cargo@v1 64 | with: 65 | command: check 66 | args: --all-features 67 | 68 | test_examples: 69 | runs-on: ubuntu-latest 70 | 71 | steps: 72 | - uses: actions/checkout@v4 73 | 74 | - name: Start RabbitMQ server 75 | run: ./start_rabbitmq.sh 76 | 77 | - name: examples 78 | run: ./examples/run_examples.sh 79 | 80 | test_features_combination: 81 | runs-on: ubuntu-latest 82 | 83 | strategy: 84 | matrix: 85 | feat: 86 | - -F default 87 | - -F traces 88 | - -F compliance_assert 89 | - -F tls 90 | - -F urispec 91 | - --all-features --release 92 | steps: 93 | - name: Check rust version 94 | run: rustc -V; cargo -V; rustup -V 95 | 96 | - uses: actions/checkout@v4 97 | - name: Start RabbitMQ server 98 | run: ./start_rabbitmq.sh 99 | 100 | - uses: actions-rs/cargo@v1 101 | with: 102 | command: test 103 | args: ${{ matrix.feat }} -------------------------------------------------------------------------------- /.github/workflows/test_coverage.yml: -------------------------------------------------------------------------------- 1 | name: codecov 2 | run-name: "Test Coverage" 3 | 4 | # on: [push, workflow_dispatch] 5 | 6 | on: 7 | push: 8 | branches: ["main"] 9 | paths-ignore: 10 | - '**/README.md' 11 | - '**/*.png' 12 | - 'rabbitmq_spec/**' 13 | - 'prepare_release.sh' 14 | - 'regression_test.sh' 15 | workflow_dispatch: 16 | 17 | pull_request: 18 | branches: ["main"] 19 | paths-ignore: 20 | - '**/README.md' 21 | - '**/*.png' 22 | - 'rabbitmq_spec/**' 23 | - 'prepare_release.sh' 24 | - 'regression_test.sh' 25 | 26 | env: 27 | CARGO_TERM_COLOR: always 28 | 29 | jobs: 30 | # codecov: 31 | # runs-on: ubuntu-latest 32 | # steps: 33 | # - uses: actions/checkout@v4 34 | # - name: Start RabbitMQ server 35 | # run: ./start_rabbitmq.sh 36 | # - name: Cargo test instrumented 37 | # run: LLVM_PROFILE_FILE="default_%m.profraw" RUSTFLAGS="-C instrument-coverage -C codegen-units=1" cargo test -p amqprs --all-features --tests 38 | # - name: Merge coverage data 39 | # run: llvm-profdata-14 merge -sparse amqprs/default_*.profraw -o amqprs.profdata 40 | # - name: Generate coverage report 41 | # run: | 42 | # object_list=$(for file in $(LLVM_PROFILE_FILE="default_%m.profraw" RUSTFLAGS="-C instrument-coverage -C codegen-units=1" cargo test -p amqprs --all-features --tests --no-run --message-format=json | jq -r "select(.profile.test == true) | .filenames[]"); do printf "%s %s " --object $file; done) 43 | # llvm-cov-14 report --use-color --ignore-filename-regex='/.cargo/registry' --ignore-filename-regex='rustc/' --ignore-filename-regex='tests/' --instr-profile=amqprs.profdata ${object_list} 44 | 45 | codecov: 46 | runs-on: ubuntu-latest 47 | environment: ci 48 | steps: 49 | - name: Checkout sources 50 | uses: actions/checkout@v4 51 | - name: Start RabbitMQ server 52 | run: ./start_rabbitmq.sh 53 | - name: Install rust toolchain 54 | uses: actions-rs/toolchain@v1 55 | with: 56 | toolchain: nightly 57 | override: true 58 | default: true 59 | components: llvm-tools 60 | 61 | - name: Install cargo-llvm-cov 62 | run: cargo install cargo-llvm-cov 63 | 64 | - name: Cargo update 65 | run: cargo update -p proc-macro2 66 | 67 | - name: Measure coverage 68 | run: RUST_LOG=trace cargo llvm-cov --all-features --lcov --output-path ./lcov.info 69 | 70 | - name: Report to codecov.io 71 | uses: codecov/codecov-action@v4 72 | with: 73 | files: ./lcov.info 74 | fail_ci_if_error: true 75 | env: 76 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} -------------------------------------------------------------------------------- /examples/src/callbacks_impl.rs: -------------------------------------------------------------------------------- 1 | //! Template for callbacks implementation. 2 | //! 3 | use amqprs::{ 4 | callbacks::{ChannelCallback, ConnectionCallback}, 5 | channel::Channel, 6 | connection::{Connection, OpenConnectionArguments}, 7 | Ack, BasicProperties, Cancel, Close, CloseChannel, Nack, Return, 8 | }; 9 | use async_trait::async_trait; 10 | 11 | //////////////////////////////////////////////////////////////////////////////// 12 | type Result = std::result::Result; 13 | 14 | //////////////////////////////////////////////////////////////////////////////// 15 | struct ExampleConnectionCallback; 16 | 17 | #[allow(unused_variables, /* template */)] 18 | #[async_trait] 19 | impl ConnectionCallback for ExampleConnectionCallback { 20 | async fn close(&mut self, connection: &Connection, close: Close) -> Result<()> { 21 | Ok(()) 22 | } 23 | 24 | async fn blocked(&mut self, connection: &Connection, reason: String) {} 25 | async fn unblocked(&mut self, connection: &Connection) {} 26 | async fn secret_updated(&mut self, connection: &Connection) {} 27 | } 28 | 29 | //////////////////////////////////////////////////////////////////////////////// 30 | struct ExampleChannelCallback; 31 | 32 | #[allow(unused_variables, /* template */)] 33 | #[async_trait] 34 | impl ChannelCallback for ExampleChannelCallback { 35 | async fn close(&mut self, channel: &Channel, close: CloseChannel) -> Result<()> { 36 | Ok(()) 37 | } 38 | async fn cancel(&mut self, channel: &Channel, cancel: Cancel) -> Result<()> { 39 | Ok(()) 40 | } 41 | async fn flow(&mut self, channel: &Channel, active: bool) -> Result { 42 | Ok(true) 43 | } 44 | async fn publish_ack(&mut self, channel: &Channel, ack: Ack) {} 45 | async fn publish_nack(&mut self, channel: &Channel, nack: Nack) {} 46 | async fn publish_return( 47 | &mut self, 48 | channel: &Channel, 49 | ret: Return, 50 | basic_properties: BasicProperties, 51 | content: Vec, 52 | ) { 53 | } 54 | } 55 | 56 | //////////////////////////////////////////////////////////////////////////////// 57 | #[tokio::main(flavor = "multi_thread", worker_threads = 2)] 58 | async fn main() { 59 | // open a connection to RabbitMQ server 60 | let args = OpenConnectionArguments::new("localhost", 5672, "user", "bitnami"); 61 | 62 | let connection = Connection::open(&args).await.unwrap(); 63 | connection 64 | .register_callback(ExampleConnectionCallback) 65 | .await 66 | .unwrap(); 67 | 68 | // open a channel on the connection 69 | let channel = connection.open_channel(None).await.unwrap(); 70 | channel 71 | .register_callback(ExampleChannelCallback) 72 | .await 73 | .unwrap(); 74 | } 75 | -------------------------------------------------------------------------------- /amqprs/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! AMQP 0-9-1 client implementation compatible with RabbitMQ. 2 | //! 3 | //! It is currently based on async tokio runtime, and cannot be configured to use another runtime by user. 4 | //! 5 | //! User usually starts by openning an AMQP [`Connection`] and register the connection [`callbacks`], 6 | //! then open an AMQP [`Channel`] on the connection and register the channel [`callbacks`]. 7 | //! 8 | //! 9 | //! # Quick Start 10 | //! 11 | //! ```rust 12 | //! use amqprs::{ 13 | //! callbacks, 14 | //! security::SecurityCredentials, 15 | //! connection::{OpenConnectionArguments, Connection}, 16 | //! }; 17 | //! 18 | //! # #[tokio::main] 19 | //! # async fn main() { 20 | //! // Build arguments for new connection. 21 | //! let args = OpenConnectionArguments::new("localhost", 5672, "user", "bitnami"); 22 | //! // Open an AMQP connection with given arguments. 23 | //! let connection = Connection::open(&args).await.unwrap(); 24 | //! 25 | //! // Register connection level callbacks. 26 | //! // In production, user should create its own type and implement trait `ConnectionCallback`. 27 | //! connection.register_callback(callbacks::DefaultConnectionCallback).await.unwrap(); 28 | //! 29 | //! // ... Now, ready to use the connection ... 30 | //! 31 | //! // Open an AMQP channel on this connection. 32 | //! let channel = connection.open_channel(None).await.unwrap(); 33 | //! // Register channel level callbacks. 34 | //! // In production, user should create its own type and implement trait `ChannelCallback`. 35 | //! channel.register_callback(callbacks::DefaultChannelCallback).await.unwrap(); 36 | //! 37 | //! // ... Now, ready to use the channel ... 38 | //! // For examples: 39 | //! channel.flow(true).await.unwrap(); 40 | //! 41 | //! // gracefully shutdown. 42 | //! channel.close().await.unwrap(); 43 | //! connection.close().await.unwrap(); 44 | //! # } 45 | //! ``` 46 | //! 47 | //! # Optional Features 48 | //! 49 | //! - "traces": enable `tracing` in the library. 50 | //! - "compliance_assert": enable compliance assertion according to AMQP spec. 51 | //! If enabled, library always check user inputs and `panic` if any non-compliance. 52 | //! If disabled, then it relies on server to reject. 53 | //! - "tls": enable SSL/TLS. 54 | //! - "urispec": enable support of [RabbitMQ URI Specification](https://www.rabbitmq.com/uri-spec.html) 55 | //! 56 | //! [`Connection`]: connection/struct.Connection.html 57 | //! [`Channel`]: channel/struct.Channel.html 58 | //! [`callbacks`]: callbacks/index.html 59 | //! 60 | ///////////////////////////////////////////////////////////////////////////// 61 | #[cfg(test)] 62 | mod test_utils; 63 | 64 | ///////////////////////////////////////////////////////////////////////////// 65 | mod api; 66 | mod frame; 67 | mod net; 68 | 69 | /// public API and types 70 | pub use api::*; 71 | pub use frame::Ack; 72 | pub use frame::BasicProperties; 73 | pub use frame::Cancel; 74 | pub use frame::Close; 75 | pub use frame::CloseChannel; 76 | pub use frame::Deliver; 77 | pub use frame::GetOk; 78 | pub use frame::Nack; 79 | pub use frame::Return; 80 | 81 | pub use frame::DELIVERY_MODE_PERSISTENT; 82 | pub use frame::DELIVERY_MODE_TRANSIENT; 83 | 84 | pub use amqp_serde::types::*; 85 | -------------------------------------------------------------------------------- /amqprs/src/api/error.rs: -------------------------------------------------------------------------------- 1 | //! Error type can be returned by the APIs. 2 | 3 | use crate::net; 4 | 5 | use std::fmt; 6 | use tokio::sync::{mpsc::error::SendError, oneshot::error::RecvError}; 7 | #[cfg(feature = "urispec")] 8 | use uriparse::uri_reference::URIReferenceError; 9 | 10 | /// A list of errors can be returned by the APIs. 11 | #[derive(Debug)] 12 | #[non_exhaustive] 13 | pub enum Error { 14 | /// Error when using an amqp(s) uri. Usually due to incorrect usage by user. 15 | UriError(String), 16 | /// Error during openning a connection. 17 | ConnectionOpenError(String), 18 | /// Error during closing a connection. 19 | ConnectionCloseError(String), 20 | /// Error when using the connection. Usually due to incorrect usage by user. 21 | ConnectionUseError(String), 22 | /// Error during openning a channel. 23 | ChannelOpenError(String), 24 | /// Error during closing a channel. 25 | ChannelCloseError(String), 26 | /// Error when using the channel. Usually due to incorrect usage by user. 27 | ChannelUseError(String), 28 | /// Error occurs in network layer. 29 | NetworkError(String), 30 | /// Error in sending or receiving messages via internal communication channel. 31 | /// Usually due to incorrect usage by user. 32 | InternalChannelError(String), 33 | /// Error during updating the secret 34 | UpdateSecretError(String), 35 | } 36 | 37 | #[cfg(feature = "urispec")] 38 | impl From for Error { 39 | fn from(err: URIReferenceError) -> Self { 40 | Self::UriError(err.to_string()) 41 | } 42 | } 43 | 44 | impl From for Error { 45 | fn from(err: net::Error) -> Self { 46 | Self::NetworkError(err.to_string()) 47 | } 48 | } 49 | impl From> for Error { 50 | fn from(err: SendError) -> Self { 51 | Self::InternalChannelError(err.to_string()) 52 | } 53 | } 54 | impl From for Error { 55 | fn from(err: RecvError) -> Self { 56 | Self::InternalChannelError(err.to_string()) 57 | } 58 | } 59 | 60 | impl fmt::Display for Error { 61 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 62 | match self { 63 | Error::UriError(msg) => { 64 | write!(f, "AMQP(S) URI error: {}", msg) 65 | } 66 | Error::NetworkError(msg) => write!(f, "AMQP network error: {}", msg), 67 | Error::ConnectionOpenError(msg) => write!(f, "AMQP connection open error: {}", msg), 68 | Error::ConnectionCloseError(msg) => write!(f, "AMQP connection close error: {}", msg), 69 | Error::ConnectionUseError(msg) => write!(f, "AMQP connection usage error: {}", msg), 70 | Error::ChannelOpenError(msg) => write!(f, "AMQP channel open error: {}", msg), 71 | Error::ChannelUseError(msg) => write!(f, "AMQP channel usage error: {}", msg), 72 | Error::ChannelCloseError(msg) => write!(f, "AMQP channel close error: {}", msg), 73 | Error::InternalChannelError(msg) => { 74 | write!(f, "AMQP internal communication error: {}", msg) 75 | } 76 | Error::UpdateSecretError(msg) => write!(f, "AMQP update secret error: {}", msg), 77 | } 78 | } 79 | } 80 | 81 | impl std::error::Error for Error {} 82 | -------------------------------------------------------------------------------- /amqprs/src/frame/method/channel.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use crate::frame::REPLY_SUCCESS; 4 | use amqp_serde::types::{Boolean, LongStr, ShortStr, ShortUint}; 5 | use serde::{Deserialize, Serialize}; 6 | 7 | #[derive(Debug, Serialize, Deserialize, Default)] 8 | pub struct OpenChannel { 9 | out_of_band: ShortStr, 10 | } 11 | 12 | impl OpenChannel { 13 | pub(crate) fn new() -> Self { 14 | Self { 15 | out_of_band: "".try_into().unwrap(), 16 | } 17 | } 18 | } 19 | 20 | #[derive(Debug, Serialize, Deserialize)] 21 | pub struct OpenChannelOk { 22 | pub(crate) channel_id: LongStr, 23 | } 24 | 25 | /// Used by channel [`close`] callback. 26 | /// 27 | /// AMQP method frame [close](https://github.com/rabbitmq/amqp-0.9.1-spec/blob/main/docs/amqp-0-9-1-reference.md#channel.close). 28 | /// 29 | /// [`close`]: callbacks/trait.ChannelCallback.html#tymethod.close 30 | // TX + RX 31 | #[derive(Debug, Serialize, Deserialize)] 32 | pub struct CloseChannel { 33 | reply_code: ShortUint, 34 | reply_text: ShortStr, 35 | class_id: ShortUint, 36 | method_id: ShortUint, 37 | } 38 | 39 | impl CloseChannel { 40 | pub fn reply_code(&self) -> u16 { 41 | self.reply_code 42 | } 43 | 44 | pub fn reply_text(&self) -> &String { 45 | self.reply_text.as_ref() 46 | } 47 | 48 | pub fn class_id(&self) -> u16 { 49 | self.class_id 50 | } 51 | 52 | pub fn method_id(&self) -> u16 { 53 | self.method_id 54 | } 55 | } 56 | 57 | impl fmt::Display for CloseChannel { 58 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 59 | f.write_fmt(format_args!( 60 | "'{}: {}', (class_id = {}, method_id = {})", 61 | self.reply_code(), 62 | self.reply_text(), 63 | self.class_id(), 64 | self.method_id() 65 | )) 66 | } 67 | } 68 | impl Default for CloseChannel { 69 | // compliance: 70 | // Indicates that the method completed successfully. This reply code is 71 | // reserved for future use - the current protocol design does not use positive 72 | // confirmation and reply codes are sent only in case of an error. 73 | fn default() -> Self { 74 | Self { 75 | reply_code: REPLY_SUCCESS, 76 | reply_text: ShortStr::default(), 77 | class_id: 0, 78 | method_id: 0, 79 | } 80 | } 81 | } 82 | #[derive(Debug, Serialize, Deserialize, Default)] 83 | pub struct CloseChannelOk; 84 | 85 | // TX + RX 86 | #[derive(Debug, Serialize, Deserialize, Default)] 87 | pub struct Flow { 88 | pub(crate) active: Boolean, 89 | } 90 | 91 | impl Flow { 92 | pub fn new(active: Boolean) -> Self { 93 | Self { active } 94 | } 95 | } 96 | 97 | // TX + RX 98 | #[derive(Debug, Serialize, Deserialize, Default)] 99 | pub struct FlowOk { 100 | pub(crate) active: Boolean, 101 | } 102 | 103 | impl FlowOk { 104 | pub fn new(active: Boolean) -> Self { 105 | Self { active } 106 | } 107 | } 108 | 109 | #[cfg(test)] 110 | mod tests { 111 | use super::Flow; 112 | 113 | #[test] 114 | fn test_default() { 115 | assert_eq!(false, Flow::default().active); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /benchmarks/src/common/mod.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use amqprs::{ 4 | channel::{BasicAckArguments, Channel}, 5 | consumer::AsyncConsumer, 6 | BasicProperties, Deliver, 7 | }; 8 | use tokio::sync::Notify; 9 | use tracing_subscriber::{fmt, prelude::*, EnvFilter}; 10 | 11 | /// We use Fibonacci sequences to generate size list for publish messages 12 | pub struct Fib { 13 | max: usize, 14 | state: usize, 15 | n_1: usize, 16 | n_2: usize, 17 | } 18 | 19 | impl Fib { 20 | pub fn new(max: usize) -> Self { 21 | assert!(max > 0); 22 | Self { 23 | max, 24 | state: 0, 25 | n_1: 1, 26 | n_2: 1, 27 | } 28 | } 29 | } 30 | 31 | impl Iterator for Fib { 32 | type Item = usize; 33 | 34 | fn next(&mut self) -> Option { 35 | if self.state == self.max { 36 | return None; 37 | } 38 | self.state += 1; 39 | 40 | if self.state == 1 || self.state == 2 { 41 | return Some(1); 42 | } 43 | let current = self.n_1 + self.n_2; 44 | self.n_2 = self.n_1; 45 | self.n_1 = current; 46 | 47 | Some(current) 48 | } 49 | } 50 | 51 | /// common algorithm for generating size list 52 | pub fn get_size_list(limit: usize) -> Vec { 53 | // construct message size list for publish 54 | let fib = Fib::new(100); 55 | let mut msg_size_list = vec![0]; 56 | for v in fib { 57 | // println!("{}", v); 58 | msg_size_list.push(v); 59 | // avoid OOM error, stop after 1st size > limit 60 | if v > limit { 61 | break; 62 | } 63 | } 64 | // total: len(msg_size_list) * 2^m 65 | let m = 5; 66 | for _ in 0..m { 67 | msg_size_list.extend_from_within(0..); 68 | } 69 | msg_size_list 70 | } 71 | 72 | /// common runtime config 73 | pub fn rt() -> tokio::runtime::Runtime { 74 | tokio::runtime::Builder::new_current_thread() 75 | .enable_all() 76 | .build() 77 | .unwrap() 78 | } 79 | 80 | pub fn setup_tracing() { 81 | // construct a subscriber that prints formatted traces to stdout 82 | // global subscriber with log level according to RUST_LOG 83 | tracing_subscriber::registry() 84 | .with(fmt::layer()) 85 | .with(EnvFilter::from_default_env()) 86 | .try_init() 87 | .ok(); 88 | } 89 | 90 | pub struct BenchMarkConsumer { 91 | end_tag: u64, 92 | notify: Arc, 93 | } 94 | 95 | impl BenchMarkConsumer { 96 | pub fn new(end_tag: u64, notify: Arc) -> Self { 97 | Self { end_tag, notify } 98 | } 99 | } 100 | 101 | #[async_trait::async_trait] 102 | impl AsyncConsumer for BenchMarkConsumer { 103 | async fn consume( 104 | &mut self, 105 | channel: &Channel, 106 | deliver: Deliver, 107 | _basic_properties: BasicProperties, 108 | _content: Vec, 109 | ) { 110 | // check all messages received 111 | if deliver.delivery_tag() % self.end_tag == 0 { 112 | // println!("{} % {}", deliver.delivery_tag(), self.end_tag); 113 | channel 114 | .basic_ack(BasicAckArguments::new(deliver.delivery_tag(), true)) 115 | .await 116 | .unwrap(); 117 | self.notify.notify_one(); 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /benchmarks/src/native_pub_amqprs.rs: -------------------------------------------------------------------------------- 1 | use amqprs::{ 2 | callbacks::{DefaultChannelCallback, DefaultConnectionCallback}, 3 | channel::{ 4 | BasicPublishArguments, QueueBindArguments, QueueDeclareArguments, QueuePurgeArguments, 5 | }, 6 | connection::{Connection, OpenConnectionArguments}, 7 | BasicProperties, 8 | }; 9 | mod common; 10 | use common::*; 11 | 12 | fn main() { 13 | setup_tracing(); 14 | 15 | let rt = rt(); 16 | 17 | rt.block_on(async { 18 | let connection = Connection::open(&OpenConnectionArguments::new( 19 | "localhost", 20 | 5672, 21 | "user", 22 | "bitnami", 23 | )) 24 | .await 25 | .unwrap(); 26 | connection 27 | .register_callback(DefaultConnectionCallback) 28 | .await 29 | .unwrap(); 30 | 31 | let channel = connection.open_channel(None).await.unwrap(); 32 | channel 33 | .register_callback(DefaultChannelCallback) 34 | .await 35 | .unwrap(); 36 | 37 | let rounting_key = "bench.amqprs.pub"; 38 | let exchange_name = "amq.topic"; 39 | let queue_name = "bench-amqprs-q"; 40 | // declare a queue 41 | let (_, _, _) = channel 42 | .queue_declare(QueueDeclareArguments::new(queue_name)) 43 | .await 44 | .unwrap() 45 | .unwrap(); 46 | // bind queue to exchange 47 | channel 48 | .queue_bind(QueueBindArguments::new( 49 | queue_name, 50 | exchange_name, 51 | rounting_key, 52 | )) 53 | .await 54 | .unwrap(); 55 | 56 | let pubargs = BasicPublishArguments::new(exchange_name, rounting_key); 57 | let declargs = QueueDeclareArguments::new(queue_name) 58 | .passive(true) 59 | .finish(); 60 | 61 | let msg_size_list = get_size_list(connection.frame_max() as usize); 62 | let count = msg_size_list.len(); 63 | // purge queue 64 | channel 65 | .queue_purge(QueuePurgeArguments::new(queue_name)) 66 | .await 67 | .unwrap(); 68 | let (_, msg_cnt, _) = channel 69 | .queue_declare( 70 | QueueDeclareArguments::new(queue_name) 71 | .passive(true) 72 | .finish(), 73 | ) 74 | .await 75 | .unwrap() 76 | .unwrap(); 77 | assert_eq!(0, msg_cnt); 78 | ////////////////////////////////////////////////////////////////////////////// 79 | let now = std::time::Instant::now(); 80 | // publish messages of variable sizes 81 | for &i in msg_size_list.iter().take(count) { 82 | channel 83 | .basic_publish(BasicProperties::default(), vec![0xc5; i], pubargs.clone()) 84 | .await 85 | .unwrap(); 86 | } 87 | // check all messages arrived at queue 88 | loop { 89 | let (_, msg_cnt, _) = channel 90 | .queue_declare(declargs.clone()) 91 | .await 92 | .unwrap() 93 | .unwrap(); 94 | if count == msg_cnt as usize { 95 | break; 96 | } 97 | } 98 | println!("amqprs benchmarks: {:?}", now.elapsed()); 99 | ////////////////////////////////////////////////////////////////////////////// 100 | 101 | channel.close().await.unwrap(); 102 | connection.close().await.unwrap(); 103 | }); 104 | } 105 | -------------------------------------------------------------------------------- /benchmarks/src/native_pub_lapin.rs: -------------------------------------------------------------------------------- 1 | use lapin::{ 2 | options::{BasicPublishOptions, QueueBindOptions, QueueDeclareOptions, QueuePurgeOptions}, 3 | types::FieldTable, 4 | BasicProperties, Connection, ConnectionProperties, 5 | }; 6 | use tokio_executor_trait::Tokio; 7 | mod common; 8 | use common::*; 9 | 10 | fn main() { 11 | setup_tracing(); 12 | 13 | let rt = rt(); 14 | let options = ConnectionProperties::default() 15 | // Use tokio executor and reactor. 16 | // At the moment the reactor is only available for unix. 17 | .with_executor(Tokio::default().with_handle(rt.handle().clone())) 18 | .with_reactor(tokio_reactor_trait::Tokio); 19 | 20 | rt.block_on(async { 21 | let uri = "amqp://user:bitnami@localhost:5672"; 22 | let connection = Connection::connect(uri, options).await.unwrap(); 23 | let channel = connection.create_channel().await.unwrap(); 24 | 25 | let rounting_key = "bench.lapin.pub"; 26 | let exchange_name = "amq.topic"; 27 | let queue_name = "bench-lapin-q"; 28 | 29 | channel 30 | .queue_declare( 31 | queue_name, 32 | QueueDeclareOptions::default(), 33 | FieldTable::default(), 34 | ) 35 | .await 36 | .unwrap(); 37 | channel 38 | .queue_bind( 39 | queue_name, 40 | exchange_name, 41 | rounting_key, 42 | QueueBindOptions::default(), 43 | FieldTable::default(), 44 | ) 45 | .await 46 | .unwrap(); 47 | 48 | let pubopts = BasicPublishOptions::default(); 49 | let declopts = QueueDeclareOptions { 50 | passive: true, 51 | ..Default::default() 52 | }; 53 | 54 | let msg_size_list = get_size_list(connection.configuration().frame_max() as usize); 55 | 56 | let count = msg_size_list.len(); 57 | // purge queue 58 | channel 59 | .queue_purge(queue_name, QueuePurgeOptions::default()) 60 | .await 61 | .unwrap(); 62 | let q_state = channel 63 | .queue_declare(queue_name, declopts, FieldTable::default()) 64 | .await 65 | .unwrap(); 66 | 67 | assert_eq!(0, q_state.message_count()); 68 | 69 | ////////////////////////////////////////////////////////////////////////////// 70 | let now = std::time::Instant::now(); 71 | 72 | // publish messages of variable sizes 73 | for &i in msg_size_list.iter().take(count) { 74 | let _confirm = channel 75 | .basic_publish( 76 | exchange_name, 77 | rounting_key, 78 | pubopts, 79 | &vec![0xc5; i], 80 | BasicProperties::default(), 81 | ) 82 | .await 83 | .unwrap() 84 | .await 85 | .unwrap(); 86 | } 87 | // check all messages arrived at queue 88 | loop { 89 | let q_state = channel 90 | .queue_declare(queue_name, declopts, FieldTable::default()) 91 | .await 92 | .unwrap(); 93 | if count == q_state.message_count() as usize { 94 | break; 95 | } 96 | } 97 | println!("lapin benchmarks: {:?}", now.elapsed()); 98 | ////////////////////////////////////////////////////////////////////////////// 99 | 100 | channel.close(0, "").await.unwrap(); 101 | connection.close(0, "").await.unwrap(); 102 | }); 103 | } 104 | -------------------------------------------------------------------------------- /amqprs/src/api/channel/confim.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | api::error::Error, 3 | frame::{Frame, Select, SelectOk}, 4 | }; 5 | 6 | use super::{Channel, Result}; 7 | 8 | /// Arguments for [`confirm_select`] 9 | /// 10 | /// See [AMQP_0-9-1 Reference](https://github.com/rabbitmq/amqp-0.9.1-spec/blob/main/docs/amqp-0-9-1-reference.md#confirm.select). 11 | /// 12 | /// [`confirm_select`]: struct.Channel.html#method.confirm_select 13 | #[derive(Debug, Clone, Default)] 14 | pub struct ConfirmSelectArguments { 15 | /// Default: `false` 16 | pub no_wait: bool, 17 | } 18 | 19 | impl ConfirmSelectArguments { 20 | /// Create new arguments with defaults. 21 | pub fn new(no_wait: bool) -> Self { 22 | Self { no_wait } 23 | } 24 | } 25 | 26 | /// APIs for AMQP confirm class. 27 | impl Channel { 28 | /// See [AMQP_0-9-1 Reference](https://github.com/rabbitmq/amqp-0.9.1-spec/blob/main/docs/amqp-0-9-1-reference.md#confirm.select). 29 | /// 30 | /// # Errors 31 | /// 32 | /// Returns error if any failure in comunication with server. 33 | pub async fn confirm_select(&self, args: ConfirmSelectArguments) -> Result<()> { 34 | let select = Select::new(args.no_wait); 35 | if args.no_wait { 36 | self.shared 37 | .outgoing_tx 38 | .send((self.shared.channel_id, select.into_frame())) 39 | .await?; 40 | Ok(()) 41 | } else { 42 | let responder_rx = self.register_responder(SelectOk::header()).await?; 43 | 44 | let _method = synchronous_request!( 45 | self.shared.outgoing_tx, 46 | (self.shared.channel_id, select.into_frame()), 47 | responder_rx, 48 | Frame::SelectOk, 49 | Error::ChannelUseError 50 | )?; 51 | Ok(()) 52 | } 53 | } 54 | } 55 | 56 | #[cfg(test)] 57 | mod tests { 58 | use std::time::Duration; 59 | 60 | use tokio::time; 61 | 62 | use crate::{ 63 | callbacks::{DefaultChannelCallback, DefaultConnectionCallback}, 64 | channel::BasicPublishArguments, 65 | connection::{Connection, OpenConnectionArguments}, 66 | test_utils::setup_logging, 67 | BasicProperties, 68 | }; 69 | 70 | use super::ConfirmSelectArguments; 71 | 72 | #[tokio::test] 73 | async fn test_publish_confirm_mode() { 74 | setup_logging(); 75 | 76 | let args = OpenConnectionArguments::new("localhost", 5672, "user", "bitnami"); 77 | 78 | let connection = Connection::open(&args).await.unwrap(); 79 | connection 80 | .register_callback(DefaultConnectionCallback) 81 | .await 82 | .unwrap(); 83 | 84 | let channel = connection.open_channel(None).await.unwrap(); 85 | channel 86 | .register_callback(DefaultChannelCallback) 87 | .await 88 | .unwrap(); 89 | 90 | // set to publish confirm mode 91 | channel 92 | .confirm_select(ConfirmSelectArguments::default()) 93 | .await 94 | .unwrap(); 95 | 96 | let args = BasicPublishArguments::new("amq.topic", "amqprs.test.transaction"); 97 | 98 | let basic_properties = BasicProperties::default().with_persistence(true).finish(); 99 | 100 | let content = String::from("AMQPRS test publish confirm").into_bytes(); 101 | 102 | channel 103 | .basic_publish(basic_properties, content, args) 104 | .await 105 | .unwrap(); 106 | 107 | time::sleep(Duration::from_millis(100)).await; 108 | channel.close().await.unwrap(); 109 | connection.close().await.unwrap(); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /amqprs/tests/test_io_error_handling.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use amqprs::{ 4 | callbacks::{DefaultChannelCallback, DefaultConnectionCallback}, 5 | channel::{ExchangeDeclareArguments, ExchangeType}, 6 | connection::Connection, 7 | }; 8 | use tokio::time; 9 | use tracing::info; 10 | 11 | mod common; 12 | 13 | #[ignore = "Need to test manually by triggering io failure"] 14 | #[tokio::test(flavor = "multi_thread", worker_threads = 2)] 15 | async fn test_net_io_err_handling() { 16 | common::setup_logging(); 17 | 18 | // open a connection to RabbitMQ server 19 | let conn_args = common::build_conn_args(); 20 | 21 | let connection = Connection::open(&conn_args).await.unwrap(); 22 | connection 23 | .register_callback(DefaultConnectionCallback) 24 | .await 25 | .unwrap(); 26 | // open a channel on the connection 27 | let channel = connection.open_channel(None).await.unwrap(); 28 | channel 29 | .register_callback(DefaultChannelCallback) 30 | .await 31 | .unwrap(); 32 | 33 | let exchange_name = "amq.topic"; 34 | 35 | // create arguments for exchange_declare 36 | let args = ExchangeDeclareArguments::of_type(exchange_name, ExchangeType::Topic) 37 | .passive(true) 38 | .finish(); 39 | 40 | // declare exchange 41 | channel.exchange_declare(args).await.unwrap(); 42 | 43 | // test if copy of same connection will also get notified 44 | let connection2 = connection.clone(); 45 | let handle2 = tokio::spawn(async move { 46 | // wait on io failure 47 | // if return `true`, it means got notification due to network failure, we try to recreate connection 48 | // if return `false`, the connection may already be closed, or closed later by client or server. 49 | if connection2.listen_network_io_failure().await { 50 | loop { 51 | time::sleep(Duration::from_secs(10)).await; 52 | 53 | if let Ok(recover_conn) = Connection::open(&common::build_conn_args()).await { 54 | // ... do some recovery... 55 | let recover_ch = recover_conn.open_channel(None).await.unwrap(); 56 | assert!(true == recover_ch.is_open()); 57 | assert!(true == recover_conn.is_open()); 58 | info!("Recovery: channel {}", recover_ch); 59 | break; 60 | }; 61 | } 62 | } 63 | // here, old connection should be closed no matter due to network failure or closed by server 64 | assert!(connection2.is_open() == false); 65 | }); 66 | let handle1 = tokio::spawn(async move { 67 | // wait on io failure 68 | // if return `true`, it means got notification due to network failure, we try to recreate connection 69 | // if return `false`, the connection may already be closed, or closed later by client or server. 70 | if connection.listen_network_io_failure().await { 71 | loop { 72 | time::sleep(Duration::from_secs(10)).await; 73 | 74 | if let Ok(recover_conn) = Connection::open(&common::build_conn_args()).await { 75 | // ... do some recovery... 76 | let recover_ch = recover_conn.open_channel(None).await.unwrap(); 77 | assert!(true == recover_ch.is_open()); 78 | assert!(true == recover_conn.is_open()); 79 | info!("Recovery: channel {}", recover_ch); 80 | break; 81 | }; 82 | } 83 | } 84 | // here, old connection should be closed no matter due to network failure or closed by server 85 | assert!(connection.is_open() == false); 86 | }); 87 | handle1.await.unwrap(); 88 | handle2.await.unwrap(); 89 | } 90 | -------------------------------------------------------------------------------- /amqprs/src/api/security.rs: -------------------------------------------------------------------------------- 1 | //! This module provides configuration API of Security and Access Control. 2 | //! 3 | //! The configuration is used as part of [`OpenConnectionArguments`] value. 4 | //! 5 | //! [`OpenConnectionArguments`]: ../connection/struct.OpenConnectionArguments.html 6 | //! [`Connection::open`]: ../connection/struct.Connection.html#method.open 7 | use amqp_serde::{ 8 | to_buffer, 9 | types::{LongStr, ShortStr}, 10 | }; 11 | use bytes::BytesMut; 12 | 13 | /// Credentials used to open a connection. 14 | #[derive(Clone, PartialEq)] 15 | pub struct SecurityCredentials { 16 | username: String, 17 | password: String, 18 | mechanism: AuthenticationMechanism, 19 | } 20 | 21 | #[allow(clippy::upper_case_acronyms)] 22 | #[derive(Clone, Debug, PartialEq)] 23 | #[non_exhaustive] 24 | enum AuthenticationMechanism { 25 | PLAIN, 26 | AMQPLAIN, 27 | EXTERNAL, 28 | // RABBIT-CR-DEMO, 29 | } 30 | 31 | impl SecurityCredentials { 32 | /// Create and return a SASL/PLAIN credential with given `username` and `password`. 33 | /// 34 | /// See [RabbitMQ access control](https://www.rabbitmq.com/access-control.html#mechanisms). 35 | pub fn new_plain(username: &str, password: &str) -> Self { 36 | Self { 37 | username: username.to_owned(), 38 | password: password.to_owned(), 39 | mechanism: AuthenticationMechanism::PLAIN, 40 | } 41 | } 42 | /// Create and return a AMQPLAIN credential with given `username` and `password`. 43 | /// 44 | /// See [RabbitMQ access control](https://www.rabbitmq.com/access-control.html#mechanisms). 45 | pub fn new_amqplain(username: &str, password: &str) -> Self { 46 | Self { 47 | username: username.to_owned(), 48 | password: password.to_owned(), 49 | mechanism: AuthenticationMechanism::AMQPLAIN, 50 | } 51 | } 52 | 53 | /// Create and return EXTERNAL without credentials 54 | /// 55 | /// This must be used together with mTLS connection. 56 | pub fn new_external() -> Self { 57 | Self { 58 | username: "".to_owned(), 59 | password: "".to_owned(), 60 | mechanism: AuthenticationMechanism::EXTERNAL, 61 | } 62 | } 63 | 64 | /// Get the name of authentication mechanism of current credential 65 | pub(crate) fn get_mechanism_name(&self) -> &str { 66 | match self.mechanism { 67 | AuthenticationMechanism::PLAIN => "PLAIN", 68 | AuthenticationMechanism::AMQPLAIN => "AMQPLAIN", 69 | AuthenticationMechanism::EXTERNAL => "EXTERNAL", 70 | } 71 | } 72 | /// Get the security challenge `response` string, to be sent to server. 73 | pub(crate) fn get_response(&self) -> String { 74 | match self.mechanism { 75 | AuthenticationMechanism::PLAIN => format!("\0{}\0{}", self.username, self.password), 76 | AuthenticationMechanism::AMQPLAIN => { 77 | let mut buf = BytesMut::new(); 78 | to_buffer( 79 | &<&str as TryInto>::try_into("LOGIN").unwrap(), 80 | &mut buf, 81 | ) 82 | .unwrap(); 83 | to_buffer(&'S', &mut buf).unwrap(); 84 | to_buffer( 85 | &<&str as TryInto>::try_into(&self.username).unwrap(), 86 | &mut buf, 87 | ) 88 | .unwrap(); 89 | 90 | to_buffer( 91 | &<&str as TryInto>::try_into("PASSWORD").unwrap(), 92 | &mut buf, 93 | ) 94 | .unwrap(); 95 | to_buffer(&'S', &mut buf).unwrap(); 96 | to_buffer( 97 | &<&str as TryInto>::try_into(&self.password).unwrap(), 98 | &mut buf, 99 | ) 100 | .unwrap(); 101 | String::from_utf8(buf.to_vec()).unwrap() 102 | } 103 | AuthenticationMechanism::EXTERNAL => "".to_string(), 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /amqprs/src/net/writer_handler.rs: -------------------------------------------------------------------------------- 1 | use amqp_serde::types::ShortUint; 2 | use tokio::{ 3 | sync::{broadcast, mpsc}, 4 | task::yield_now, 5 | time, 6 | }; 7 | #[cfg(feature = "traces")] 8 | use tracing::{debug, error, info, trace}; 9 | 10 | use crate::{ 11 | connection::Connection, 12 | frame::{Frame, HeartBeat, DEFAULT_CONN_CHANNEL}, 13 | }; 14 | 15 | use super::{BufIoWriter, OutgoingMessage}; 16 | 17 | pub(crate) struct WriterHandler { 18 | stream: BufIoWriter, 19 | /// receiver half to forward outgoing messages from AMQ connection/channel to server 20 | outgoing_rx: mpsc::Receiver, 21 | /// listener of shutdown signal 22 | shutdown: broadcast::Receiver, 23 | /// connection 24 | amqp_connection: Connection, 25 | } 26 | 27 | impl WriterHandler { 28 | pub fn new( 29 | stream: BufIoWriter, 30 | outgoing_rx: mpsc::Receiver, 31 | shutdown: broadcast::Receiver, 32 | amqp_connection: Connection, 33 | ) -> Self { 34 | Self { 35 | stream, 36 | outgoing_rx, 37 | shutdown, 38 | amqp_connection, 39 | } 40 | } 41 | 42 | pub async fn run_until_shutdown(mut self, heartbeat: ShortUint) { 43 | // to take in acount network delay and congestion 44 | // heartbeat should be sent at a interval of timeout / 2 45 | let interval: u64 = (heartbeat / 2).into(); 46 | let mut expiration = time::Instant::now() + time::Duration::from_secs(interval); 47 | 48 | loop { 49 | tokio::select! { 50 | biased; 51 | 52 | channel_frame = self.outgoing_rx.recv() => { 53 | let (channel_id, frame) = match channel_frame { 54 | None => break, 55 | Some(v) => v, 56 | }; 57 | if let Err(err) = self.stream.write_frame(channel_id, frame, self.amqp_connection.frame_max()).await { 58 | #[cfg(feature="tracing")] 59 | error!("failed to send frame over connection {}, cause: {}", self.amqp_connection, err); 60 | break; 61 | } 62 | expiration = time::Instant::now() + time::Duration::from_secs(interval); 63 | #[cfg(feature="tracing")] 64 | trace!("connection {} heartbeat deadline is updated to {:?}", self.amqp_connection, expiration); 65 | } 66 | _ = time::sleep_until(expiration) => { 67 | if expiration <= time::Instant::now() { 68 | expiration = time::Instant::now() + time::Duration::from_secs(interval); 69 | 70 | if let Err(err) = self.stream.write_frame(DEFAULT_CONN_CHANNEL, Frame::HeartBeat(HeartBeat), self.amqp_connection.frame_max()).await { 71 | #[cfg(feature="tracing")] 72 | error!("failed to send heartbeat over connection {}, cause: {}", self.amqp_connection, err); 73 | break; 74 | } 75 | #[cfg(feature="tracing")] 76 | debug!("sent heartbeat over connection {}", self.amqp_connection,); 77 | } 78 | } 79 | _ = self.shutdown.recv() => { 80 | #[cfg(feature="tracing")] 81 | info!("received shutdown notification for connection {}", self.amqp_connection); 82 | // try to give last chance for last message. 83 | yield_now().await; 84 | break; 85 | } 86 | else => { 87 | break; 88 | } 89 | } 90 | } 91 | self.amqp_connection.set_is_open(false); 92 | 93 | if let Err(err) = self.stream.close().await { 94 | #[cfg(feature = "traces")] 95 | error!( 96 | "failed to close i/o writer of connection {}, cause: {}", 97 | self.amqp_connection, err 98 | ); 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /benchmarks/src/native_consume_amqprs.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use amqprs::{ 4 | callbacks::{DefaultChannelCallback, DefaultConnectionCallback}, 5 | channel::{ 6 | BasicConsumeArguments, BasicPublishArguments, QueueBindArguments, QueueDeclareArguments, 7 | QueuePurgeArguments, 8 | }, 9 | connection::{Connection, OpenConnectionArguments}, 10 | BasicProperties, 11 | }; 12 | mod common; 13 | use common::*; 14 | use tokio::sync::Notify; 15 | 16 | fn main() { 17 | setup_tracing(); 18 | 19 | let rt = rt(); 20 | 21 | rt.block_on(async { 22 | let connection = Connection::open(&OpenConnectionArguments::new( 23 | "localhost", 24 | 5672, 25 | "user", 26 | "bitnami", 27 | )) 28 | .await 29 | .unwrap(); 30 | connection 31 | .register_callback(DefaultConnectionCallback) 32 | .await 33 | .unwrap(); 34 | 35 | let channel = connection.open_channel(None).await.unwrap(); 36 | channel 37 | .register_callback(DefaultChannelCallback) 38 | .await 39 | .unwrap(); 40 | 41 | let rounting_key = "bench.amqprs.consume"; 42 | let exchange_name = "amq.topic"; 43 | let queue_name = "bench-amqprs-q"; 44 | // declare a queue 45 | let (_, _, _) = channel 46 | .queue_declare(QueueDeclareArguments::new(queue_name)) 47 | .await 48 | .unwrap() 49 | .unwrap(); 50 | // bind queue to exchange 51 | channel 52 | .queue_bind(QueueBindArguments::new( 53 | queue_name, 54 | exchange_name, 55 | rounting_key, 56 | )) 57 | .await 58 | .unwrap(); 59 | 60 | let pubargs = BasicPublishArguments::new(exchange_name, rounting_key); 61 | let declargs = QueueDeclareArguments::new(queue_name) 62 | .passive(true) 63 | .finish(); 64 | 65 | let msg_size_list = get_size_list(connection.frame_max() as usize); 66 | let count = msg_size_list.len(); 67 | // purge queue 68 | channel 69 | .queue_purge(QueuePurgeArguments::new(queue_name)) 70 | .await 71 | .unwrap(); 72 | let (_, msg_cnt, _) = channel 73 | .queue_declare( 74 | QueueDeclareArguments::new(queue_name) 75 | .passive(true) 76 | .finish(), 77 | ) 78 | .await 79 | .unwrap() 80 | .unwrap(); 81 | assert_eq!(0, msg_cnt); 82 | 83 | // publish messages of variable sizes 84 | for &i in msg_size_list.iter().take(count) { 85 | channel 86 | .basic_publish(BasicProperties::default(), vec![0xc5; i], pubargs.clone()) 87 | .await 88 | .unwrap(); 89 | } 90 | // check all messages arrived at queue 91 | loop { 92 | let (_, msg_cnt, _) = channel 93 | .queue_declare(declargs.clone()) 94 | .await 95 | .unwrap() 96 | .unwrap(); 97 | if count == msg_cnt as usize { 98 | break; 99 | } 100 | } 101 | 102 | let consume_args = BasicConsumeArguments::new(queue_name, ""); 103 | let notifyer = Arc::new(Notify::new()); 104 | let notifyee = notifyer.clone(); 105 | let bench_consumer = BenchMarkConsumer::new(count as u64, notifyer); 106 | 107 | ////////////////////////////////////////////////////////////////////////////// 108 | let now = std::time::Instant::now(); 109 | // Tell the consumer not to send ack 110 | channel 111 | .basic_consume(bench_consumer, consume_args) 112 | .await 113 | .unwrap(); 114 | 115 | // wait for all messages delivered to consumer 116 | notifyee.notified().await; 117 | let eclapsed = now.elapsed(); 118 | println!("amqprs consumer benchmarks: {eclapsed:?}"); 119 | 120 | ////////////////////////////////////////////////////////////////////////////// 121 | 122 | channel.close().await.unwrap(); 123 | connection.close().await.unwrap(); 124 | }); 125 | } 126 | -------------------------------------------------------------------------------- /examples/src/tls.rs: -------------------------------------------------------------------------------- 1 | use amqprs::{ 2 | callbacks::{DefaultChannelCallback, DefaultConnectionCallback}, 3 | channel::{ 4 | BasicConsumeArguments, BasicPublishArguments, QueueBindArguments, QueueDeclareArguments, 5 | }, 6 | connection::{Connection, OpenConnectionArguments}, 7 | consumer::DefaultConsumer, 8 | BasicProperties, 9 | }; 10 | use tokio::time; 11 | use tracing_subscriber::{fmt, prelude::*, EnvFilter}; 12 | 13 | use amqprs::tls::TlsAdaptor; 14 | 15 | #[tokio::main(flavor = "multi_thread", worker_threads = 2)] 16 | async fn main() { 17 | // construct a subscriber that prints formatted traces to stdout 18 | // global subscriber with log level according to RUST_LOG 19 | tracing_subscriber::registry() 20 | .with(fmt::layer()) 21 | .with(EnvFilter::from_default_env()) 22 | .try_init() 23 | .ok(); 24 | 25 | //////////////////////////////////////////////////////////////// 26 | // TLS specific configuration 27 | let current_dir = std::env::current_dir().unwrap(); 28 | let current_dir = current_dir.join("rabbitmq_conf/client/"); 29 | 30 | // domain should match the certificate/key files 31 | let domain = "AMQPRS_TEST"; 32 | let root_ca_cert = current_dir.join("ca_certificate.pem"); 33 | let client_cert = current_dir.join("client_AMQPRS_TEST_certificate.pem"); 34 | let client_private_key = current_dir.join("client_AMQPRS_TEST_key.pem"); 35 | // Install default crypto provider for TLS. 36 | rustls::crypto::aws_lc_rs::default_provider() 37 | .install_default() 38 | .expect("Must install crypto provider for tls."); 39 | 40 | let args = OpenConnectionArguments::new("localhost", 5671, "user", "bitnami") 41 | .tls_adaptor( 42 | TlsAdaptor::with_client_auth( 43 | Some(root_ca_cert.as_path()), 44 | client_cert.as_path(), 45 | client_private_key.as_path(), 46 | domain.to_owned(), 47 | ) 48 | .unwrap(), 49 | ) 50 | .finish(); 51 | 52 | //////////////////////////////////////////////////////////////// 53 | // everything below should be the same as regular connection 54 | // open a connection to RabbitMQ server 55 | let connection = Connection::open(&args).await.unwrap(); 56 | connection 57 | .register_callback(DefaultConnectionCallback) 58 | .await 59 | .unwrap(); 60 | 61 | // open a channel on the connection 62 | let channel = connection.open_channel(None).await.unwrap(); 63 | channel 64 | .register_callback(DefaultChannelCallback) 65 | .await 66 | .unwrap(); 67 | 68 | // declare a queue 69 | let (queue_name, _, _) = channel 70 | .queue_declare(QueueDeclareArguments::default()) 71 | .await 72 | .unwrap() 73 | .unwrap(); 74 | 75 | // bind the queue to exchange 76 | let rounting_key = "amqprs.example"; 77 | let exchange_name = "amq.topic"; 78 | channel 79 | .queue_bind(QueueBindArguments::new( 80 | &queue_name, 81 | exchange_name, 82 | rounting_key, 83 | )) 84 | .await 85 | .unwrap(); 86 | 87 | ////////////////////////////////////////////////////////////////////////////// 88 | // start consumer with given name 89 | let args = BasicConsumeArguments::new(&queue_name, "example_basic_pub_sub"); 90 | 91 | channel 92 | .basic_consume(DefaultConsumer::new(args.no_ack), args) 93 | .await 94 | .unwrap(); 95 | 96 | ////////////////////////////////////////////////////////////////////////////// 97 | // publish message 98 | let content = String::from( 99 | r#" 100 | { 101 | "publisher": "example" 102 | "data": "Hello, amqprs!" 103 | } 104 | "#, 105 | ) 106 | .into_bytes(); 107 | 108 | // create arguments for basic_publish 109 | let args = BasicPublishArguments::new(exchange_name, rounting_key); 110 | 111 | channel 112 | .basic_publish(BasicProperties::default(), content, args) 113 | .await 114 | .unwrap(); 115 | 116 | // keep the `channel` and `connection` object from dropping before pub/sub is done. 117 | // channel/connection will be closed when drop. 118 | time::sleep(time::Duration::from_secs(1)).await; 119 | // explicitly close 120 | channel.close().await.unwrap(); 121 | connection.close().await.unwrap(); 122 | } 123 | -------------------------------------------------------------------------------- /examples/src/mtls.rs: -------------------------------------------------------------------------------- 1 | use amqprs::security::SecurityCredentials; 2 | use amqprs::{ 3 | callbacks::{DefaultChannelCallback, DefaultConnectionCallback}, 4 | channel::{ 5 | BasicConsumeArguments, BasicPublishArguments, QueueBindArguments, QueueDeclareArguments, 6 | }, 7 | connection::{Connection, OpenConnectionArguments}, 8 | consumer::DefaultConsumer, 9 | BasicProperties, 10 | }; 11 | use tokio::time; 12 | use tracing_subscriber::{fmt, prelude::*, EnvFilter}; 13 | 14 | use amqprs::tls::TlsAdaptor; 15 | 16 | #[tokio::main(flavor = "multi_thread", worker_threads = 2)] 17 | async fn main() { 18 | // construct a subscriber that prints formatted traces to stdout 19 | // global subscriber with log level according to RUST_LOG 20 | tracing_subscriber::registry() 21 | .with(fmt::layer()) 22 | .with(EnvFilter::from_default_env()) 23 | .try_init() 24 | .ok(); 25 | 26 | //////////////////////////////////////////////////////////////// 27 | // TLS specific configuration 28 | let current_dir = std::env::current_dir().unwrap(); 29 | let current_dir = current_dir.join("rabbitmq_conf/client/"); 30 | 31 | let root_ca_cert = current_dir.join("ca_certificate.pem"); 32 | let client_cert = current_dir.join("client_AMQPRS_TEST_certificate.pem"); 33 | let client_private_key = current_dir.join("client_AMQPRS_TEST_key.pem"); 34 | // domain should match the certificate/key files 35 | let domain = "AMQPRS_TEST"; 36 | // Install default crypto provider for TLS. 37 | rustls::crypto::aws_lc_rs::default_provider() 38 | .install_default() 39 | .expect("Must install crypto provider for tls."); 40 | 41 | let args = OpenConnectionArguments::new("localhost", 5671, "", "") 42 | .tls_adaptor( 43 | TlsAdaptor::with_client_auth( 44 | Some(root_ca_cert.as_path()), 45 | client_cert.as_path(), 46 | client_private_key.as_path(), 47 | domain.to_owned(), 48 | ) 49 | .unwrap(), 50 | ) 51 | .credentials(SecurityCredentials::new_external()) 52 | .finish(); 53 | 54 | //////////////////////////////////////////////////////////////// 55 | // everything below should be the same as regular connection 56 | // open a connection to RabbitMQ server 57 | let connection = Connection::open(&args).await.unwrap(); 58 | connection 59 | .register_callback(DefaultConnectionCallback) 60 | .await 61 | .unwrap(); 62 | 63 | // open a channel on the connection 64 | let channel = connection.open_channel(None).await.unwrap(); 65 | channel 66 | .register_callback(DefaultChannelCallback) 67 | .await 68 | .unwrap(); 69 | 70 | // declare a queue 71 | let (queue_name, _, _) = channel 72 | .queue_declare(QueueDeclareArguments::default()) 73 | .await 74 | .unwrap() 75 | .unwrap(); 76 | 77 | // bind the queue to exchange 78 | let rounting_key = "amqprs.example"; 79 | let exchange_name = "amq.topic"; 80 | channel 81 | .queue_bind(QueueBindArguments::new( 82 | &queue_name, 83 | exchange_name, 84 | rounting_key, 85 | )) 86 | .await 87 | .unwrap(); 88 | 89 | ////////////////////////////////////////////////////////////////////////////// 90 | // start consumer with given name 91 | let args = BasicConsumeArguments::new(&queue_name, "example_basic_pub_sub"); 92 | 93 | channel 94 | .basic_consume(DefaultConsumer::new(args.no_ack), args) 95 | .await 96 | .unwrap(); 97 | 98 | ////////////////////////////////////////////////////////////////////////////// 99 | // publish message 100 | let content = String::from( 101 | r#" 102 | { 103 | "publisher": "example" 104 | "data": "Hello, amqprs!" 105 | } 106 | "#, 107 | ) 108 | .into_bytes(); 109 | 110 | // create arguments for basic_publish 111 | let args = BasicPublishArguments::new(exchange_name, rounting_key); 112 | 113 | channel 114 | .basic_publish(BasicProperties::default(), content, args) 115 | .await 116 | .unwrap(); 117 | 118 | // keep the `channel` and `connection` object from dropping before pub/sub is done. 119 | // channel/connection will be closed when drop. 120 | time::sleep(time::Duration::from_secs(1)).await; 121 | // explicitly close 122 | channel.close().await.unwrap(); 123 | connection.close().await.unwrap(); 124 | } 125 | -------------------------------------------------------------------------------- /amqprs/src/net/channel_manager.rs: -------------------------------------------------------------------------------- 1 | use std::collections::{BTreeMap, HashMap}; 2 | 3 | use amqp_serde::types::{AmqpChannelId, ShortUint}; 4 | use tokio::sync::{mpsc::UnboundedSender, oneshot}; 5 | 6 | use crate::frame::MethodHeader; 7 | 8 | use super::{channel_id_repo::ChannelIdRepository, IncomingMessage}; 9 | 10 | pub(crate) struct ChannelResource { 11 | /// responder to acknowledge synchronous request 12 | /// responders are oneshot channel, which are not dedicated resource for channel 13 | pub responders: HashMap<&'static MethodHeader, oneshot::Sender>, 14 | 15 | /// connection's default channel does not have dispatcher 16 | /// each channel has one and only one dispatcher 17 | pub dispatcher: Option>, 18 | } 19 | 20 | impl ChannelResource { 21 | pub(crate) fn new(dispatcher: Option>) -> Self { 22 | Self { 23 | responders: HashMap::new(), 24 | dispatcher, 25 | // amqp_channel, 26 | // callback: None, 27 | } 28 | } 29 | } 30 | pub(super) struct ChannelManager { 31 | /// channel id manager to allocate, reserve, and free id 32 | /// Keep the id manager out of AMQ connection type and use registeration machanism to manage id, 33 | /// which allows a slim Connection type that can be easily shared concurrently 34 | /// If we have the id manager in AMQ Connection type, then shared Connection object need to be mutable concurrently 35 | channel_id_repo: ChannelIdRepository, 36 | 37 | /// channel resource registery store 38 | resource: BTreeMap, 39 | } 40 | 41 | impl ChannelManager { 42 | pub fn new(channel_max: ShortUint) -> Self { 43 | Self { 44 | channel_id_repo: ChannelIdRepository::new(channel_max), 45 | resource: BTreeMap::new(), 46 | } 47 | } 48 | /// Insert channel resource, when open a new channel 49 | pub fn insert_resource( 50 | &mut self, 51 | channel_id: Option, 52 | resource: ChannelResource, 53 | ) -> Option { 54 | let id = match channel_id { 55 | // reserve channel id as requested 56 | Some(id) => { 57 | // connection's default channel 0 is static reserved 58 | if id == 0 || self.channel_id_repo.reserve(id) { 59 | match self.resource.insert(id, resource) { 60 | Some(_old) => unreachable!("implementation error"), 61 | None => id, 62 | } 63 | } else { 64 | // fail to reserve the id 65 | return None; 66 | } 67 | } 68 | // allocate a channel id 69 | None => { 70 | // allocate id never fail 71 | let id = self.channel_id_repo.allocate(); 72 | match self.resource.insert(id, resource) { 73 | Some(_old) => unreachable!("implementation error"), 74 | None => id, 75 | } 76 | } 77 | }; 78 | 79 | Some(id) 80 | } 81 | 82 | /// remove channel resource, when channel to be closed 83 | pub fn remove_resource(&mut self, channel_id: &AmqpChannelId) -> Option { 84 | assert!( 85 | self.channel_id_repo.release(*channel_id), 86 | "release a free id, implementation error" 87 | ); 88 | // remove responder means channel is to be closed 89 | self.resource.remove(channel_id) 90 | } 91 | 92 | pub fn get_dispatcher( 93 | &self, 94 | channel_id: &AmqpChannelId, 95 | ) -> Option<&UnboundedSender> { 96 | self.resource.get(channel_id)?.dispatcher.as_ref() 97 | } 98 | 99 | pub fn insert_responder( 100 | &mut self, 101 | channel_id: &AmqpChannelId, 102 | method_header: &'static MethodHeader, 103 | responder: oneshot::Sender, 104 | ) -> Option> { 105 | self.resource 106 | .get_mut(channel_id)? 107 | .responders 108 | .insert(method_header, responder) 109 | } 110 | 111 | pub fn remove_responder( 112 | &mut self, 113 | channel_id: &AmqpChannelId, 114 | method_header: &'static MethodHeader, 115 | ) -> Option> { 116 | self.resource 117 | .get_mut(channel_id)? 118 | .responders 119 | .remove(method_header) 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /examples/src/basic_pub_sub.rs: -------------------------------------------------------------------------------- 1 | use amqp_serde::types::FieldTable; 2 | use amqprs::{ 3 | callbacks::{DefaultChannelCallback, DefaultConnectionCallback}, 4 | channel::{ 5 | BasicConsumeArguments, BasicPublishArguments, QueueBindArguments, QueueDeclareArguments, 6 | }, 7 | connection::{Connection, OpenConnectionArguments}, 8 | consumer::DefaultConsumer, 9 | BasicProperties, 10 | }; 11 | use tokio::time; 12 | use tracing_subscriber::{fmt, prelude::*, EnvFilter}; 13 | 14 | #[tokio::main(flavor = "multi_thread", worker_threads = 2)] 15 | async fn main() { 16 | // construct a subscriber that prints formatted traces to stdout 17 | // global subscriber with log level according to RUST_LOG 18 | tracing_subscriber::registry() 19 | .with(fmt::layer()) 20 | .with(EnvFilter::from_default_env()) 21 | .try_init() 22 | .ok(); 23 | 24 | // open a connection to RabbitMQ server 25 | let connection = Connection::open(&OpenConnectionArguments::new( 26 | "localhost", 27 | 5672, 28 | "user", 29 | "bitnami", 30 | )) 31 | .await 32 | .unwrap(); 33 | connection 34 | .register_callback(DefaultConnectionCallback) 35 | .await 36 | .unwrap(); 37 | 38 | // open a channel on the connection 39 | let channel = connection.open_channel(None).await.unwrap(); 40 | channel 41 | .register_callback(DefaultChannelCallback) 42 | .await 43 | .unwrap(); 44 | 45 | // declare a durable queue 46 | let (queue_name, _, _) = channel 47 | .queue_declare(QueueDeclareArguments::durable_client_named( 48 | "amqprs.examples.basic", 49 | )) 50 | .await 51 | .unwrap() 52 | .unwrap(); 53 | 54 | // bind the queue to exchange 55 | let routing_key = "amqprs.example"; 56 | let exchange_name = "amq.topic"; 57 | channel 58 | .queue_bind(QueueBindArguments::new( 59 | &queue_name, 60 | exchange_name, 61 | routing_key, 62 | )) 63 | .await 64 | .unwrap(); 65 | 66 | ////////////////////////////////////////////////////////////////////////////// 67 | // start consumer with given name 68 | let args = BasicConsumeArguments::new(&queue_name, "example_basic_pub_sub"); 69 | 70 | channel 71 | .basic_consume(DefaultConsumer::new(args.no_ack), args) 72 | .await 73 | .unwrap(); 74 | 75 | ////////////////////////////////////////////////////////////////////////////// 76 | // publish message 77 | let content = String::from( 78 | r#" 79 | { 80 | "publisher": "example" 81 | "data": "Hello, amqprs!" 82 | } 83 | "#, 84 | ) 85 | .into_bytes(); 86 | 87 | // create arguments for basic_publish 88 | let args = BasicPublishArguments::new(exchange_name, routing_key); 89 | let mut headers = FieldTable::new(); 90 | headers.insert("key".try_into().unwrap(), "value".try_into().unwrap()); 91 | 92 | let props = BasicProperties::default() 93 | .with_app_id("app_id") 94 | .with_cluster_id("cluster_id") 95 | .with_content_encoding("content_encoding") 96 | .with_content_type("content_type") 97 | .with_correlation_id("correlation_id") 98 | .with_expiration("100000") 99 | .with_message_id("message_id") 100 | .with_message_type("message_type") 101 | .with_persistence(true) 102 | .with_priority(1) 103 | .with_reply_to("reply_to") 104 | .with_timestamp(1743000001) 105 | .with_user_id("user") 106 | .with_headers(headers) 107 | .finish(); 108 | 109 | channel.basic_publish(props, content, args).await.unwrap(); 110 | 111 | // Check connection should still open and no network i/o failure after publish 112 | match time::timeout( 113 | time::Duration::from_secs(1), 114 | connection.listen_network_io_failure(), 115 | ) 116 | .await 117 | { 118 | Ok(is_failure) => { 119 | panic!( 120 | "Unexpected network I/O failure: {is_failure}, connection is_open status: {}", 121 | connection.is_open() 122 | ); 123 | } 124 | Err(_) => { 125 | tracing::debug!("Network I/O OK after publish"); 126 | assert!(connection.is_open(), "Connection should be still open"); 127 | } 128 | } 129 | // keep the `channel` and `connection` object from dropping before pub/sub is done. 130 | // channel/connection will be closed when drop. 131 | time::sleep(time::Duration::from_secs(1)).await; 132 | // explicitly close 133 | channel.close().await.unwrap(); 134 | connection.close().await.unwrap(); 135 | } 136 | -------------------------------------------------------------------------------- /amqprs/tests/test_callback.rs: -------------------------------------------------------------------------------- 1 | use amqprs::{ 2 | callbacks::{ChannelCallback, DefaultChannelCallback, DefaultConnectionCallback}, 3 | channel::{ 4 | BasicConsumeArguments, Channel, ExchangeDeclareArguments, ExchangeType, 5 | QueueDeclareArguments, QueueDeleteArguments, 6 | }, 7 | connection::Connection, 8 | consumer::DefaultConsumer, 9 | error::Error, 10 | Ack, BasicProperties, Cancel, CloseChannel, Nack, Return, 11 | }; 12 | use async_trait::async_trait; 13 | use std::{sync::Arc, time::Duration}; 14 | use tokio::sync::Notify; 15 | 16 | mod common; 17 | 18 | #[tokio::test(flavor = "multi_thread", worker_threads = 2)] 19 | #[should_panic = "InternalChannelError(\"channel closed\")"] 20 | async fn test_connection_callback() { 21 | common::setup_logging(); 22 | 23 | // open a connection to RabbitMQ server 24 | let args = common::build_conn_args(); 25 | 26 | let connection = Connection::open(&args).await.unwrap(); 27 | 28 | connection 29 | .register_callback(DefaultConnectionCallback) 30 | .await 31 | .unwrap(); 32 | 33 | // open a channel on the connection 34 | let channel = connection.open_channel(None).await.unwrap(); 35 | 36 | // expect panic because invalid exchange type will cause server to shutdown connection, 37 | // the connection callback for `Close` method should be called and all internal channel services are closed 38 | // which results in error of below API call 39 | channel 40 | .exchange_declare(ExchangeDeclareArguments::new("amq.direct", "invalid_type")) 41 | .await 42 | .unwrap(); 43 | } 44 | 45 | #[tokio::test(flavor = "multi_thread", worker_threads = 2)] 46 | #[should_panic = "InternalChannelError(\"channel closed\")"] 47 | async fn test_channel_callback() { 48 | common::setup_logging(); 49 | 50 | // open a connection to RabbitMQ server 51 | let args = common::build_conn_args(); 52 | 53 | let connection = Connection::open(&args).await.unwrap(); 54 | 55 | // open a channel on the connection 56 | let channel = connection.open_channel(None).await.unwrap(); 57 | channel 58 | .register_callback(DefaultChannelCallback) 59 | .await 60 | .unwrap(); 61 | 62 | // expect panic because "amp.topic" is `durable = true`, we declare "durable = false", 63 | // which is the default value in arguments. 64 | let args = ExchangeDeclareArguments::of_type("amq.topic", ExchangeType::Topic); 65 | channel.exchange_declare(args).await.unwrap(); 66 | } 67 | 68 | #[tokio::test] 69 | async fn test_channel_callback_close() { 70 | common::setup_logging(); 71 | 72 | // open a connection to RabbitMQ server 73 | let args = common::build_conn_args(); 74 | let connection = Connection::open(&args).await.unwrap(); 75 | 76 | // open a channel on the connection 77 | let channel = connection.open_channel(None).await.unwrap(); 78 | let cancel_notify = Arc::new(Notify::new()); 79 | channel 80 | .register_callback(ChannelCancelCallback { 81 | cancel_notify: Arc::clone(&cancel_notify), 82 | }) 83 | .await 84 | .unwrap(); 85 | 86 | // declare queue 87 | const QUEUE_NAME: &str = "test-channel-callback-close"; 88 | let args = QueueDeclareArguments::default() 89 | .queue(QUEUE_NAME.to_string()) 90 | .finish(); 91 | channel.queue_declare(args).await.unwrap(); 92 | 93 | // start consumer 94 | let args = BasicConsumeArguments::default() 95 | .queue(QUEUE_NAME.to_string()) 96 | .finish(); 97 | channel 98 | .basic_consume(DefaultConsumer::new(true), args) 99 | .await 100 | .unwrap(); 101 | 102 | // delete queue so RabbitMQ produce consumer cancel notification 103 | let args = QueueDeleteArguments::default() 104 | .queue(QUEUE_NAME.to_string()) 105 | .finish(); 106 | channel.queue_delete(args).await.unwrap(); 107 | 108 | // wait for callback 109 | tokio::time::timeout(Duration::from_secs(5), cancel_notify.notified()) 110 | .await 111 | .expect("ChannelCallback::cancel() should be called when queue was deleted"); 112 | } 113 | 114 | struct ChannelCancelCallback { 115 | cancel_notify: Arc, 116 | } 117 | 118 | #[async_trait] 119 | impl ChannelCallback for ChannelCancelCallback { 120 | async fn close(&mut self, _channel: &Channel, _close: CloseChannel) -> Result<(), Error> { 121 | Ok(()) 122 | } 123 | async fn cancel(&mut self, _channel: &Channel, _cancel: Cancel) -> Result<(), Error> { 124 | self.cancel_notify.notify_one(); 125 | 126 | Ok(()) 127 | } 128 | async fn flow(&mut self, _channel: &Channel, active: bool) -> Result { 129 | Ok(active) 130 | } 131 | async fn publish_ack(&mut self, _channel: &Channel, _ack: Ack) {} 132 | async fn publish_nack(&mut self, _channel: &Channel, _nack: Nack) {} 133 | async fn publish_return( 134 | &mut self, 135 | _channel: &Channel, 136 | _ret: Return, 137 | _basic_properties: BasicProperties, 138 | _content: Vec, 139 | ) { 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /amqprs/src/frame/method/exchange.rs: -------------------------------------------------------------------------------- 1 | use amqp_serde::types::{AmqpExchangeName, Boolean, FieldTable, Octect, ShortStr, ShortUint}; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | mod bit_flag { 5 | pub mod declare { 6 | // continous bits packed into one or more octets, starting from the low bit in each octet. 7 | pub const NO_WAIT: u8 = 0b0001_0000; 8 | pub const INTERNAL: u8 = 0b0000_1000; 9 | pub const AUTO_DELETE: u8 = 0b0000_0100; 10 | pub const DURABLE: u8 = 0b0000_0010; 11 | pub const PASSIVE: u8 = 0b0000_0001; 12 | } 13 | pub mod delete { 14 | 15 | pub const IF_UNUSED: u8 = 0b0000_0001; 16 | pub const NO_WAIT: u8 = 0b0000_0010; 17 | } 18 | } 19 | 20 | #[derive(Debug, Serialize, Deserialize)] 21 | pub struct Declare { 22 | ticket: ShortUint, 23 | exchange: AmqpExchangeName, 24 | typ: ShortStr, 25 | bits: Octect, 26 | arguments: FieldTable, 27 | } 28 | 29 | impl Declare { 30 | pub fn new( 31 | ticket: ShortUint, 32 | exchange: AmqpExchangeName, 33 | typ: ShortStr, 34 | arguments: FieldTable, 35 | ) -> Self { 36 | Self { 37 | ticket, 38 | exchange, 39 | typ, 40 | bits: 0, 41 | arguments, 42 | } 43 | } 44 | 45 | pub fn set_passive(&mut self, value: bool) { 46 | if value { 47 | self.bits |= bit_flag::declare::PASSIVE; 48 | } else { 49 | self.bits &= !bit_flag::declare::PASSIVE; 50 | } 51 | } 52 | pub fn set_durable(&mut self, value: bool) { 53 | if value { 54 | self.bits |= bit_flag::declare::DURABLE; 55 | } else { 56 | self.bits &= !bit_flag::declare::DURABLE; 57 | } 58 | } 59 | 60 | pub fn set_auto_delete(&mut self, value: bool) { 61 | if value { 62 | self.bits |= bit_flag::declare::AUTO_DELETE; 63 | } else { 64 | self.bits &= !bit_flag::declare::AUTO_DELETE; 65 | } 66 | } 67 | 68 | pub fn set_internal(&mut self, value: bool) { 69 | if value { 70 | self.bits |= bit_flag::declare::INTERNAL; 71 | } else { 72 | self.bits &= !bit_flag::declare::INTERNAL; 73 | } 74 | } 75 | 76 | pub fn set_no_wait(&mut self, value: bool) { 77 | if value { 78 | self.bits |= bit_flag::declare::NO_WAIT; 79 | } else { 80 | self.bits &= !bit_flag::declare::NO_WAIT; 81 | } 82 | } 83 | } 84 | 85 | #[derive(Debug, Serialize, Deserialize)] 86 | pub struct DeclareOk; 87 | 88 | #[derive(Debug, Serialize, Deserialize, Default)] 89 | pub struct Delete { 90 | ticket: ShortUint, 91 | exchange: AmqpExchangeName, 92 | bits: Octect, 93 | } 94 | impl Delete { 95 | pub fn new(ticket: ShortUint, exchange: AmqpExchangeName) -> Self { 96 | Self { 97 | ticket, 98 | exchange, 99 | bits: 0, 100 | } 101 | } 102 | 103 | pub fn set_if_unused(&mut self, value: bool) { 104 | if value { 105 | self.bits |= bit_flag::delete::IF_UNUSED; 106 | } else { 107 | self.bits &= !bit_flag::delete::IF_UNUSED; 108 | } 109 | } 110 | 111 | pub fn set_no_wait(&mut self, value: bool) { 112 | if value { 113 | self.bits |= bit_flag::delete::NO_WAIT; 114 | } else { 115 | self.bits &= !bit_flag::delete::NO_WAIT; 116 | } 117 | } 118 | } 119 | #[derive(Debug, Serialize, Deserialize)] 120 | pub struct DeleteOk; 121 | 122 | #[derive(Debug, Serialize, Deserialize)] 123 | pub struct Bind { 124 | ticket: ShortUint, 125 | destination: AmqpExchangeName, 126 | source: AmqpExchangeName, 127 | routing_key: ShortStr, 128 | nowait: Boolean, 129 | arguments: FieldTable, 130 | } 131 | 132 | impl Bind { 133 | pub fn new( 134 | ticket: ShortUint, 135 | destination: AmqpExchangeName, 136 | source: AmqpExchangeName, 137 | routing_key: ShortStr, 138 | nowait: Boolean, 139 | arguments: FieldTable, 140 | ) -> Self { 141 | Self { 142 | ticket, 143 | destination, 144 | source, 145 | routing_key, 146 | nowait, 147 | arguments, 148 | } 149 | } 150 | } 151 | 152 | #[derive(Debug, Serialize, Deserialize)] 153 | pub struct BindOk; 154 | 155 | #[derive(Debug, Serialize, Deserialize)] 156 | pub struct Unbind { 157 | ticket: ShortUint, 158 | destination: AmqpExchangeName, 159 | source: AmqpExchangeName, 160 | routing_key: ShortStr, 161 | nowait: Boolean, 162 | arguments: FieldTable, 163 | } 164 | 165 | impl Unbind { 166 | pub fn new( 167 | ticket: ShortUint, 168 | destination: AmqpExchangeName, 169 | source: AmqpExchangeName, 170 | routing_key: ShortStr, 171 | nowait: Boolean, 172 | arguments: FieldTable, 173 | ) -> Self { 174 | Self { 175 | ticket, 176 | destination, 177 | source, 178 | routing_key, 179 | nowait, 180 | arguments, 181 | } 182 | } 183 | } 184 | 185 | #[derive(Debug, Serialize, Deserialize)] 186 | pub struct UnbindOk; 187 | -------------------------------------------------------------------------------- /benchmarks/src/native_consume_lapin.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use lapin::{ 4 | message::DeliveryResult, 5 | options::{ 6 | BasicAckOptions, BasicConsumeOptions, BasicPublishOptions, QueueBindOptions, 7 | QueueDeclareOptions, QueuePurgeOptions, 8 | }, 9 | types::FieldTable, 10 | BasicProperties, Connection, ConnectionProperties, 11 | }; 12 | use tokio::sync::Notify; 13 | use tokio_executor_trait::Tokio; 14 | mod common; 15 | use common::*; 16 | 17 | fn main() { 18 | setup_tracing(); 19 | 20 | let rt = rt(); 21 | let options = ConnectionProperties::default() 22 | // Use tokio executor and reactor. 23 | // At the moment the reactor is only available for unix. 24 | .with_executor(Tokio::default().with_handle(rt.handle().clone())) 25 | .with_reactor(tokio_reactor_trait::Tokio); 26 | 27 | rt.block_on(async { 28 | let uri = "amqp://user:bitnami@localhost:5672"; 29 | let connection = Connection::connect(uri, options).await.unwrap(); 30 | let channel = connection.create_channel().await.unwrap(); 31 | 32 | let rounting_key = "bench.lapin.consume"; 33 | let exchange_name = "amq.topic"; 34 | let queue_name = "bench-lapin-q"; 35 | 36 | channel 37 | .queue_declare( 38 | queue_name, 39 | QueueDeclareOptions::default(), 40 | FieldTable::default(), 41 | ) 42 | .await 43 | .unwrap(); 44 | channel 45 | .queue_bind( 46 | queue_name, 47 | exchange_name, 48 | rounting_key, 49 | QueueBindOptions::default(), 50 | FieldTable::default(), 51 | ) 52 | .await 53 | .unwrap(); 54 | 55 | let pubopts = BasicPublishOptions::default(); 56 | let declopts = QueueDeclareOptions { 57 | passive: true, 58 | ..Default::default() 59 | }; 60 | 61 | let msg_size_list = get_size_list(connection.configuration().frame_max() as usize); 62 | 63 | let count = msg_size_list.len(); 64 | // purge queue 65 | channel 66 | .queue_purge(queue_name, QueuePurgeOptions::default()) 67 | .await 68 | .unwrap(); 69 | let q_state = channel 70 | .queue_declare(queue_name, declopts, FieldTable::default()) 71 | .await 72 | .unwrap(); 73 | 74 | assert_eq!(0, q_state.message_count()); 75 | 76 | // publish messages of variable sizes 77 | for &i in msg_size_list.iter().take(count) { 78 | let _confirm = channel 79 | .basic_publish( 80 | exchange_name, 81 | rounting_key, 82 | pubopts, 83 | &vec![0xc5; i], 84 | BasicProperties::default(), 85 | ) 86 | .await 87 | .unwrap() 88 | .await 89 | .unwrap(); 90 | } 91 | // check all messages arrived at queue 92 | loop { 93 | let q_state = channel 94 | .queue_declare(queue_name, declopts, FieldTable::default()) 95 | .await 96 | .unwrap(); 97 | if count == q_state.message_count() as usize { 98 | break; 99 | } 100 | } 101 | 102 | let consume_opts = BasicConsumeOptions::default(); 103 | let tbl = FieldTable::default(); 104 | let notifyer = Arc::new(Notify::new()); 105 | let notifyee = notifyer.clone(); 106 | 107 | ////////////////////////////////////////////////////////////////////////////// 108 | let now = std::time::Instant::now(); 109 | 110 | let consumer = channel 111 | .basic_consume(queue_name, "", consume_opts, tbl) 112 | .await 113 | .unwrap(); 114 | 115 | consumer.set_delegate(move |delivery: DeliveryResult| { 116 | let notifyer = notifyer.clone(); 117 | async move { 118 | let delivery = match delivery { 119 | // Carries the delivery alongside its channel 120 | Ok(Some(delivery)) => delivery, 121 | // The consumer got canceled 122 | Ok(None) => return, 123 | // Carries the error and is always followed by Ok(None) 124 | Err(error) => { 125 | dbg!("Failed to consume queue message {}", error); 126 | return; 127 | } 128 | }; 129 | 130 | if delivery.delivery_tag % count as u64 == 0 { 131 | // println!("{} % {}", delivery.delivery_tag, count); 132 | let args = BasicAckOptions { multiple: true }; 133 | delivery.ack(args).await.unwrap(); 134 | notifyer.notify_one(); 135 | } 136 | } 137 | }); 138 | // wait for all messages delivered to consumer 139 | notifyee.notified().await; 140 | let eclapsed = now.elapsed(); 141 | println!("lapin consumer benchmarks: {eclapsed:?}"); 142 | ////////////////////////////////////////////////////////////////////////////// 143 | 144 | channel.close(0, "").await.unwrap(); 145 | connection.close(0, "").await.unwrap(); 146 | }); 147 | } 148 | -------------------------------------------------------------------------------- /amqprs/README.md: -------------------------------------------------------------------------------- 1 | [![ci](https://github.com/gftea/amqprs/actions/workflows/regression_test.yml/badge.svg)](https://github.com/gftea/amqprs/actions/workflows/regression_test.yml) 2 | [![codecov](https://codecov.io/gh/gftea/amqprs/branch/main/graph/badge.svg?token=7MF92R6F60)](https://codecov.io/gh/gftea/amqprs) 3 | [![Documentation](https://docs.rs/amqprs/badge.svg)](https://docs.rs/amqprs) 4 | [![crates.io](https://img.shields.io/crates/v/amqprs.svg)](https://crates.io/crates/amqprs) 5 | [![Discord](https://img.shields.io/discord/1065607081513717900)](https://discord.gg/g7Z9TeCu28) 6 | 7 | # MSRV 8 | 9 | - Since `v2.0.0`, it has breaking changes due to rustls upgrade, and the msrv is `1.71`. 10 | - Version < `v2.0.0`: the msrv is `1.56` 11 | 12 | ## NOTE! Please upgrade to v2 version, because no bug fix or enhancement will be applied to v1 version! 13 | 14 | # What is "amqprs" 15 | 16 | Yet another RabbitMQ client implementation in rust with different design goals. 17 | 18 | The library is accepted to list in [RabbitMQ official website](https://www.rabbitmq.com/devtools.html#rust-dev). 19 | 20 | It's probably the best performance among existing Rust clients. See 21 | [Benchmarks](https://github.com/gftea/amqprs/blob/main/benchmarks/README.md). 22 | 23 | ## Design Philosophy 24 | 25 | 1. API first: easy to use and understand. Keep the API similar as python client library so that it is easier for users to move from there. 26 | 2. Minimum external dependencies: as few external crates as possible. 27 | 3. lock free: no mutex/lock in client library itself. 28 | 29 | # Design Architecture 30 | ![Lock-free Design](https://github.com/gftea/amqprs/raw/HEAD/architecture.png) 31 | 32 | # Quick Start: Consume and Publish 33 | 34 | ```rust 35 | // open a connection to RabbitMQ server 36 | let connection = Connection::open(&OpenConnectionArguments::new( 37 | "localhost", 38 | 5672, 39 | "user", 40 | "bitnami", 41 | )) 42 | .await 43 | .unwrap(); 44 | connection 45 | .register_callback(DefaultConnectionCallback) 46 | .await 47 | .unwrap(); 48 | 49 | // open a channel on the connection 50 | let channel = connection.open_channel(None).await.unwrap(); 51 | channel 52 | .register_callback(DefaultChannelCallback) 53 | .await 54 | .unwrap(); 55 | 56 | // declare a queue 57 | let (queue_name, _, _) = channel 58 | .queue_declare(QueueDeclareArguments::default()) 59 | .await 60 | .unwrap() 61 | .unwrap(); 62 | 63 | // bind the queue to exchange 64 | let routing_key = "amqprs.example"; 65 | let exchange_name = "amq.topic"; 66 | channel 67 | .queue_bind(QueueBindArguments::new( 68 | &queue_name, 69 | exchange_name, 70 | routing_key, 71 | )) 72 | .await 73 | .unwrap(); 74 | 75 | ////////////////////////////////////////////////////////////////// 76 | // start consumer with given name 77 | let args = BasicConsumeArguments::new( 78 | &queue_name, 79 | "example_basic_pub_sub" 80 | ); 81 | 82 | channel 83 | .basic_consume(DefaultConsumer::new(args.no_ack), args) 84 | .await 85 | .unwrap(); 86 | 87 | ////////////////////////////////////////////////////////////////// 88 | // publish message 89 | let content = String::from( 90 | r#" 91 | { 92 | "publisher": "example" 93 | "data": "Hello, amqprs!" 94 | } 95 | "#, 96 | ) 97 | .into_bytes(); 98 | 99 | // create arguments for basic_publish 100 | let args = BasicPublishArguments::new(exchange_name, routing_key); 101 | 102 | channel 103 | .basic_publish(BasicProperties::default(), content, args) 104 | .await 105 | .unwrap(); 106 | 107 | 108 | // channel/connection will be closed when drop. 109 | // keep the `channel` and `connection` object from dropping 110 | // before pub/sub is done. 111 | time::sleep(time::Duration::from_secs(1)).await; 112 | // explicitly close 113 | channel.close().await.unwrap(); 114 | connection.close().await.unwrap(); 115 | ``` 116 | 117 | # Typical Examples 118 | 119 | ## [Example - Publish and Subscribe](https://github.com/gftea/amqprs/blob/main/examples/src/basic_pub_sub.rs) 120 | ## [Example - SSL/TLS](https://github.com/gftea/amqprs/blob/main/examples/src/tls.rs) 121 | 122 | # Optional Features 123 | 124 | - "traces": enable `tracing` in the library. 125 | - "compliance_assert": enable compliance assertion according to AMQP spec. 126 | If enabled, library always check user inputs and `panic` if any non-compliance. 127 | If disabled, then it relies on server to reject. 128 | - "tls": enable SSL/TLS. 129 | - "urispec": enable support of [RabbitMQ URI Specification](https://www.rabbitmq.com/uri-spec.html) 130 | 131 | 132 | # Run Test Locally 133 | 134 | __Testing depends on RabbitMQ docker container.__ 135 | 136 | ```bash 137 | # start rabbitmq server 138 | ./start_rabbitmq.sh 139 | 140 | # run tests 141 | ./regression_test.sh 142 | 143 | # enable traces in test. 144 | # Note that it only takes effect if "traces" feature is enabled 145 | RUST_LOG=debug ./regression_test.sh 146 | ``` 147 | 148 | # Benchmarks 149 | 150 | [See benchmarks's README](https://github.com/gftea/amqprs/blob/main/benchmarks/README.md) 151 | 152 |
153 | Community Feedbacks 154 | 155 | #### Luc Georges @ Hugging Face 156 | 157 | > I've put amqprs in production and it's working very nicely so far! I've had spikes of publish and delivery over 10k msg/sec without breaking a sweat 158 | 159 | #### Michael Klishin @ RabbitMQ team 160 | 161 | > We usually add new clients after they get some traction in the community. But this client seems to be fairly well documented and I like the API (it is a bit complicated with some other Rust clients) 162 | 163 |
164 | 165 | -------------------------------------------------------------------------------- /amqprs/src/api/channel/tx.rs: -------------------------------------------------------------------------------- 1 | //! See [AMQP 0-9-1 extended specs](https://www.rabbitmq.com/resources/specs/amqp0-9-1.extended.xml) 2 | //! 3 | //! The Tx class allows publish and ack operations to be batched into atomic 4 | //! units of work. The intention is that all publish and ack requests issued 5 | //! within a transaction will complete successfully or none of them will. 6 | //! Servers SHOULD implement atomic transactions at least where all publish 7 | //! or ack requests affect a single queue. Transactions that cover multiple 8 | //! queues may be non-atomic, given that queues can be created and destroyed 9 | //! asynchronously, and such events do not form part of any transaction. 10 | //! Further, the behaviour of transactions with respect to the immediate and 11 | //! mandatory flags on Basic.Publish methods is not defined. 12 | use crate::{ 13 | api::{error::Error, Result}, 14 | frame::{Frame, TxCommit, TxCommitOk, TxRollback, TxRollbackOk, TxSelect, TxSelectOk}, 15 | }; 16 | 17 | use super::Channel; 18 | 19 | /// APIs for AMQP transaction class. 20 | impl Channel { 21 | /// This method sets the channel to use standard transactions. The client must use this 22 | /// method at least once on a channel before using the [`tx_commit`] or [`tx_rollback`] methods. 23 | /// 24 | /// Also see [AMQP_0-9-1 Reference](https://github.com/rabbitmq/amqp-0.9.1-spec/blob/main/docs/amqp-0-9-1-reference.md#tx.select). 25 | /// # Errors 26 | /// 27 | /// Returns error if any failure in communication with server. 28 | /// 29 | /// [`tx_commit`]: struct.Channel.html#method.tx_commit 30 | /// [`tx_rollback`]: struct.Channel.html#method.tx_rollback 31 | pub async fn tx_select(&self) -> Result<()> { 32 | let select = TxSelect; 33 | 34 | let responder_rx = self.register_responder(TxSelectOk::header()).await?; 35 | 36 | let _method = synchronous_request!( 37 | self.shared.outgoing_tx, 38 | (self.shared.channel_id, select.into_frame()), 39 | responder_rx, 40 | Frame::TxSelectOk, 41 | Error::ChannelUseError 42 | )?; 43 | Ok(()) 44 | } 45 | /// This method commits all message publications and acknowledgments performed in 46 | /// the current transaction. A new transaction starts immediately after a commit. 47 | /// 48 | /// Also see [AMQP_0-9-1 Reference](https://github.com/rabbitmq/amqp-0.9.1-spec/blob/main/docs/amqp-0-9-1-reference.md#tx.commit). 49 | /// # Errors 50 | /// 51 | /// Returns error if any failure in communication with server. 52 | pub async fn tx_commit(&self) -> Result<()> { 53 | let select = TxCommit; 54 | 55 | let responder_rx = self.register_responder(TxCommitOk::header()).await?; 56 | 57 | let _method = synchronous_request!( 58 | self.shared.outgoing_tx, 59 | (self.shared.channel_id, select.into_frame()), 60 | responder_rx, 61 | Frame::TxCommitOk, 62 | Error::ChannelUseError 63 | )?; 64 | Ok(()) 65 | } 66 | /// This method abandons all message publications and acknowledgments performed in 67 | /// the current transaction. A new transaction starts immediately after a rollback. 68 | /// Note that unacked messages will not be automatically redelivered by rollback; 69 | /// if that is required an explicit recover call should be issued. 70 | /// 71 | /// Also see [AMQP_0-9-1 Reference](https://github.com/rabbitmq/amqp-0.9.1-spec/blob/main/docs/amqp-0-9-1-reference.md#tx.rollback). 72 | /// 73 | /// # Errors 74 | /// 75 | /// Returns error if any failure in communication with server. 76 | pub async fn tx_rollback(&self) -> Result<()> { 77 | let select = TxRollback; 78 | 79 | let responder_rx = self.register_responder(TxRollbackOk::header()).await?; 80 | 81 | let _method = synchronous_request!( 82 | self.shared.outgoing_tx, 83 | (self.shared.channel_id, select.into_frame()), 84 | responder_rx, 85 | Frame::TxRollbackOk, 86 | Error::ChannelUseError 87 | )?; 88 | Ok(()) 89 | } 90 | } 91 | 92 | #[cfg(test)] 93 | mod tests { 94 | 95 | use crate::{ 96 | callbacks::{DefaultChannelCallback, DefaultConnectionCallback}, 97 | channel::BasicPublishArguments, 98 | connection::{Connection, OpenConnectionArguments}, 99 | BasicProperties, 100 | }; 101 | 102 | #[tokio::test] 103 | async fn test_tx_apis() { 104 | let args = OpenConnectionArguments::new("localhost", 5672, "user", "bitnami"); 105 | 106 | let connection = Connection::open(&args).await.unwrap(); 107 | connection 108 | .register_callback(DefaultConnectionCallback) 109 | .await 110 | .unwrap(); 111 | 112 | let channel = connection.open_channel(None).await.unwrap(); 113 | channel 114 | .register_callback(DefaultChannelCallback) 115 | .await 116 | .unwrap(); 117 | 118 | // start transaction 119 | channel.tx_select().await.unwrap(); 120 | 121 | let args = BasicPublishArguments::new("amq.topic", "amqprs.test.transaction"); 122 | 123 | let basic_properties = BasicProperties::default().with_persistence(true).finish(); 124 | 125 | let content = String::from("AMQPRS test transactions").into_bytes(); 126 | 127 | channel 128 | .basic_publish(basic_properties, content, args) 129 | .await 130 | .unwrap(); 131 | channel.tx_commit().await.unwrap(); 132 | 133 | // rollback 134 | channel.tx_rollback().await.unwrap(); 135 | 136 | channel.close().await.unwrap(); 137 | connection.close().await.unwrap(); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /amqprs/src/net/channel_id_repo.rs: -------------------------------------------------------------------------------- 1 | use amqp_serde::types::{AmqpChannelId, ShortUint}; 2 | 3 | const INITIAL_BIT_MASK: u8 = 0b1000_0000; 4 | pub(crate) struct ChannelIdRepository { 5 | /// Each bit represent two states: 1: occupied, 0: free. 6 | /// Real id is calculated by byte postion in Vec + bit postion in byte. 7 | id_state: Vec, 8 | } 9 | impl ChannelIdRepository { 10 | pub fn new(channel_max: ShortUint) -> Self { 11 | let len = match channel_max { 12 | 0 => 1 + (u16::MAX as usize - 1) / 8, 13 | max => 1 + (max as usize - 1) / 8, 14 | }; 15 | 16 | Self { 17 | id_state: vec![0; len], 18 | } 19 | } 20 | 21 | fn is_free(&mut self, pos: usize, mask: u8) -> bool { 22 | (mask & self.id_state[pos]) == 0 23 | } 24 | 25 | fn set_occupied(&mut self, pos: usize, mask: u8) { 26 | self.id_state[pos] |= mask; 27 | } 28 | 29 | fn set_free(&mut self, pos: usize, mask: u8) { 30 | self.id_state[pos] &= !mask; 31 | } 32 | 33 | fn get_pos_mask(&self, id: AmqpChannelId) -> (usize, u8) { 34 | let pos = (id as usize - 1) / 8; 35 | let mask = INITIAL_BIT_MASK >> ((id - 1) % 8); 36 | (pos, mask) 37 | } 38 | 39 | pub fn allocate(&mut self) -> AmqpChannelId { 40 | let pos = self 41 | .id_state 42 | .iter() 43 | .position(|&v| v != 0b1111_1111) 44 | .expect("id allocation never fail"); 45 | for i in 0..8 { 46 | let mask = INITIAL_BIT_MASK >> i; 47 | if self.is_free(pos, mask) { 48 | // mark it as occupied 49 | self.set_occupied(pos, mask); 50 | // calculate the real id 51 | let channel_id = pos as AmqpChannelId * 8 + i + 1; 52 | return channel_id; 53 | } 54 | } 55 | unreachable!("id allocation should always return"); 56 | } 57 | /// true: OK, false: already released 58 | pub fn release(&mut self, id: AmqpChannelId) -> bool { 59 | assert_ne!(0, id, "connection's default channel 0 cannot be released"); 60 | 61 | let (pos, mask) = self.get_pos_mask(id); 62 | if self.is_free(pos, mask) { 63 | // already released 64 | false 65 | } else { 66 | self.set_free(pos, mask); 67 | true 68 | } 69 | } 70 | 71 | /// true: OK, false: already reserved 72 | pub fn reserve(&mut self, id: AmqpChannelId) -> bool { 73 | assert_ne!(0, id, "connection's default channel 0 cannot be reserved"); 74 | let (pos, mask) = self.get_pos_mask(id); 75 | 76 | if !self.is_free(pos, mask) { 77 | // already occupied 78 | false 79 | } else { 80 | self.set_occupied(pos, mask); 81 | true 82 | } 83 | } 84 | } 85 | 86 | #[cfg(test)] 87 | mod tests { 88 | use std::collections::HashSet; 89 | 90 | use super::ChannelIdRepository; 91 | 92 | #[test] 93 | fn test_id_allocate_and_release() { 94 | let channel_max = 2047; 95 | let mut id_repo = ChannelIdRepository::new(channel_max); 96 | 97 | let mut ids = HashSet::new(); 98 | // allocate to max 99 | for _ in 0..channel_max { 100 | let id = id_repo.allocate(); 101 | // id should be unique 102 | assert_eq!(true, ids.insert(id)); 103 | } 104 | // free all 105 | for id in ids { 106 | assert_eq!(true, id_repo.release(id)); 107 | } 108 | //can allocte to max again 109 | let mut ids = HashSet::new(); 110 | 111 | for _ in 0..channel_max { 112 | let id = id_repo.allocate(); 113 | // id should be unique 114 | assert_eq!(true, ids.insert(id)); 115 | } 116 | } 117 | 118 | #[test] 119 | fn test_id_reserve_and_release() { 120 | let channel_max = 2047; 121 | let mut id_repo = ChannelIdRepository::new(channel_max); 122 | 123 | let mut ids = vec![]; 124 | // reserver all id: from '1' to max 125 | for i in 1..channel_max + 1 { 126 | assert_eq!(true, id_repo.reserve(i)); 127 | ids.push(i); 128 | } 129 | // free all 130 | for id in ids { 131 | assert_eq!(true, id_repo.release(id)); 132 | } 133 | // can allocte to max again 134 | for _ in 0..channel_max { 135 | id_repo.allocate(); 136 | } 137 | } 138 | 139 | #[test] 140 | fn test_cannot_reserve_occupied_id() { 141 | let channel_max = 2047; 142 | let mut id_repo = ChannelIdRepository::new(channel_max); 143 | 144 | let mut ids = HashSet::new(); 145 | // allocate to max 146 | for _ in 0..channel_max { 147 | let id = id_repo.allocate(); 148 | // id should be unique 149 | assert_eq!(true, ids.insert(id)); 150 | } 151 | // failed to reserve 152 | for id in ids { 153 | assert_eq!(false, id_repo.reserve(id)); 154 | } 155 | } 156 | 157 | #[test] 158 | fn test_id_allocate_and_release_with_channel_max_zero() { 159 | let channel_max = 0; 160 | let mut id_repo = ChannelIdRepository::new(channel_max); 161 | 162 | let mut ids = HashSet::new(); 163 | // allocate to max 164 | for _ in 0..u16::MAX { 165 | let id = id_repo.allocate(); 166 | // id should be unique 167 | assert_eq!(true, ids.insert(id)); 168 | } 169 | // free all 170 | for id in ids { 171 | assert_eq!(true, id_repo.release(id)); 172 | } 173 | //can allocte to max again 174 | let mut ids = HashSet::new(); 175 | 176 | for _ in 0..u16::MAX { 177 | let id = id_repo.allocate(); 178 | // id should be unique 179 | assert_eq!(true, ids.insert(id)); 180 | } 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /amqprs/src/frame/method/queue.rs: -------------------------------------------------------------------------------- 1 | use amqp_serde::types::{ 2 | AmqpExchangeName, AmqpMessageCount, AmqpQueueName, Boolean, FieldTable, LongUint, Octect, 3 | ShortStr, ShortUint, 4 | }; 5 | use serde::{Deserialize, Serialize}; 6 | 7 | mod bit_flag { 8 | pub mod declare { 9 | // continous bits packed into one or more octets, starting from the low bit in each octet. 10 | pub const NO_WAIT: u8 = 0b0001_0000; 11 | pub const AUTO_DELETE: u8 = 0b0000_1000; 12 | pub const EXCLUSIVE: u8 = 0b0000_0100; 13 | pub const DURABLE: u8 = 0b0000_0010; 14 | pub const PASSIVE: u8 = 0b0000_0001; 15 | } 16 | pub mod delete { 17 | 18 | pub const IF_UNUSED: u8 = 0b0000_0001; 19 | pub const IF_EMPTY: u8 = 0b0000_0010; 20 | pub const NO_WAIT: u8 = 0b0000_0100; 21 | } 22 | } 23 | 24 | #[derive(Debug, Serialize, Deserialize, Default)] 25 | pub struct DeclareQueue { 26 | ticket: ShortUint, 27 | queue: AmqpQueueName, 28 | bits: Octect, 29 | arguments: FieldTable, 30 | } 31 | 32 | impl DeclareQueue { 33 | pub fn new(ticket: ShortUint, queue: AmqpQueueName, arguments: FieldTable) -> Self { 34 | Self { 35 | ticket, 36 | queue, 37 | bits: 0, 38 | arguments, 39 | } 40 | } 41 | 42 | /// set passive to `true` 43 | pub fn set_passive(&mut self, value: bool) { 44 | if value { 45 | self.bits |= bit_flag::declare::PASSIVE; 46 | } else { 47 | self.bits &= !bit_flag::declare::PASSIVE; 48 | } 49 | } 50 | pub fn set_durable(&mut self, value: bool) { 51 | if value { 52 | self.bits |= bit_flag::declare::DURABLE; 53 | } else { 54 | self.bits &= !bit_flag::declare::DURABLE; 55 | } 56 | } 57 | pub fn set_auto_delete(&mut self, value: bool) { 58 | if value { 59 | self.bits |= bit_flag::declare::AUTO_DELETE; 60 | } else { 61 | self.bits &= !bit_flag::declare::AUTO_DELETE; 62 | } 63 | } 64 | pub fn set_exclusive(&mut self, value: bool) { 65 | if value { 66 | self.bits |= bit_flag::declare::EXCLUSIVE; 67 | } else { 68 | self.bits &= !bit_flag::declare::EXCLUSIVE; 69 | } 70 | } 71 | pub fn set_no_wait(&mut self, value: bool) { 72 | if value { 73 | self.bits |= bit_flag::declare::NO_WAIT; 74 | } else { 75 | self.bits &= !bit_flag::declare::NO_WAIT; 76 | } 77 | } 78 | } 79 | 80 | #[derive(Debug, Serialize, Deserialize)] 81 | pub struct DeclareQueueOk { 82 | pub(crate) queue: AmqpQueueName, 83 | pub(crate) message_count: AmqpMessageCount, 84 | pub(crate) consumer_count: LongUint, 85 | } 86 | 87 | #[derive(Debug, Serialize, Deserialize, Default)] 88 | pub struct BindQueue { 89 | ticket: ShortUint, 90 | queue: AmqpQueueName, 91 | exchange: AmqpExchangeName, 92 | routing_key: ShortStr, 93 | nowait: Boolean, 94 | arguments: FieldTable, 95 | } 96 | 97 | impl BindQueue { 98 | pub fn new( 99 | ticket: ShortUint, 100 | queue: AmqpQueueName, 101 | exchange: AmqpExchangeName, 102 | routing_key: ShortStr, 103 | nowait: Boolean, 104 | arguments: FieldTable, 105 | ) -> Self { 106 | Self { 107 | ticket, 108 | queue, 109 | exchange, 110 | routing_key, 111 | nowait, 112 | arguments, 113 | } 114 | } 115 | } 116 | 117 | #[derive(Debug, Serialize, Deserialize)] 118 | pub struct BindQueueOk; 119 | 120 | #[derive(Debug, Serialize, Deserialize, Default)] 121 | pub struct UnbindQueue { 122 | ticket: ShortUint, 123 | queue: AmqpQueueName, 124 | exchange: AmqpExchangeName, 125 | routing_key: ShortStr, 126 | arguments: FieldTable, 127 | } 128 | 129 | impl UnbindQueue { 130 | pub fn new( 131 | ticket: ShortUint, 132 | queue: AmqpQueueName, 133 | exchange: AmqpExchangeName, 134 | routing_key: ShortStr, 135 | arguments: FieldTable, 136 | ) -> Self { 137 | Self { 138 | ticket, 139 | queue, 140 | exchange, 141 | routing_key, 142 | arguments, 143 | } 144 | } 145 | } 146 | 147 | #[derive(Debug, Serialize, Deserialize)] 148 | pub struct UnbindQueueOk; 149 | 150 | #[derive(Debug, Serialize, Deserialize, Default)] 151 | pub struct DeleteQueue { 152 | ticket: ShortUint, 153 | queue: AmqpQueueName, 154 | bits: Octect, 155 | } 156 | 157 | impl DeleteQueue { 158 | pub fn new(ticket: ShortUint, queue: AmqpQueueName) -> Self { 159 | Self { 160 | ticket, 161 | queue, 162 | bits: 0, 163 | } 164 | } 165 | 166 | pub fn set_if_unused(&mut self, value: bool) { 167 | if value { 168 | self.bits |= bit_flag::delete::IF_UNUSED; 169 | } else { 170 | self.bits &= !bit_flag::delete::IF_UNUSED; 171 | } 172 | } 173 | pub fn set_if_empty(&mut self, value: bool) { 174 | if value { 175 | self.bits |= bit_flag::delete::IF_EMPTY; 176 | } else { 177 | self.bits &= !bit_flag::delete::IF_EMPTY; 178 | } 179 | } 180 | pub fn set_no_wait(&mut self, value: bool) { 181 | if value { 182 | self.bits |= bit_flag::delete::NO_WAIT; 183 | } else { 184 | self.bits &= !bit_flag::delete::NO_WAIT; 185 | } 186 | } 187 | } 188 | #[derive(Debug, Serialize, Deserialize)] 189 | pub struct DeleteQueueOk { 190 | pub(crate) message_count: AmqpMessageCount, 191 | } 192 | 193 | #[derive(Debug, Serialize, Deserialize, Default)] 194 | pub struct PurgeQueue { 195 | ticket: ShortUint, 196 | queue: AmqpQueueName, 197 | nowait: Boolean, 198 | } 199 | 200 | impl PurgeQueue { 201 | pub fn new(ticket: ShortUint, queue: AmqpQueueName, nowait: Boolean) -> Self { 202 | Self { 203 | ticket, 204 | queue, 205 | nowait, 206 | } 207 | } 208 | } 209 | 210 | #[derive(Debug, Serialize, Deserialize)] 211 | pub struct PurgeQueueOk { 212 | pub(crate) message_count: AmqpMessageCount, 213 | } 214 | -------------------------------------------------------------------------------- /amqprs/src/frame/method/connection.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use crate::frame::REPLY_SUCCESS; 4 | use amqp_serde::types::{AmqpPeerProperties, LongStr, LongUint, Octect, ShortStr, ShortUint}; 5 | use serde::{Deserialize, Serialize}; 6 | 7 | #[derive(Debug, Serialize, Deserialize)] 8 | pub struct Start { 9 | pub(crate) version_major: Octect, 10 | pub(crate) version_minor: Octect, 11 | pub(crate) server_properties: AmqpPeerProperties, 12 | pub(crate) mechanisms: LongStr, 13 | pub(crate) locales: LongStr, 14 | } 15 | 16 | #[derive(Debug, Serialize, Deserialize)] 17 | pub struct StartOk { 18 | client_properties: AmqpPeerProperties, 19 | machanisms: ShortStr, 20 | response: LongStr, 21 | locale: ShortStr, 22 | } 23 | 24 | impl StartOk { 25 | pub fn new( 26 | client_properties: AmqpPeerProperties, 27 | machanisms: ShortStr, 28 | response: LongStr, 29 | locale: ShortStr, 30 | ) -> Self { 31 | Self { 32 | client_properties, 33 | machanisms, 34 | response, 35 | locale, 36 | } 37 | } 38 | } 39 | 40 | impl Default for StartOk { 41 | fn default() -> Self { 42 | Self { 43 | client_properties: AmqpPeerProperties::new(), 44 | machanisms: "PLAIN".try_into().unwrap(), 45 | response: "\0guest\0guest".try_into().unwrap(), 46 | locale: "en_US".try_into().unwrap(), 47 | } 48 | } 49 | } 50 | 51 | #[derive(Debug, Serialize, Deserialize)] 52 | pub struct Tune { 53 | channel_max: ShortUint, 54 | frame_max: LongUint, 55 | heartbeat: ShortUint, 56 | } 57 | 58 | impl Tune { 59 | pub fn channel_max(&self) -> u16 { 60 | self.channel_max 61 | } 62 | 63 | pub fn frame_max(&self) -> u32 { 64 | self.frame_max 65 | } 66 | 67 | pub fn heartbeat(&self) -> u16 { 68 | self.heartbeat 69 | } 70 | } 71 | 72 | #[derive(Debug, Serialize, Deserialize, Default)] 73 | pub struct TuneOk { 74 | // RabbitMQ doesn't put a limit on channel-max, and treats any number in tune-ok as valid. 75 | // It does put a limit on frame-max, and checks that the value sent in tune-ok 76 | // is less than or equal. 77 | channel_max: ShortUint, 78 | frame_max: LongUint, 79 | heartbeat: ShortUint, 80 | } 81 | 82 | impl TuneOk { 83 | pub fn new(channel_max: ShortUint, frame_max: LongUint, heartbeat: ShortUint) -> Self { 84 | Self { 85 | channel_max, 86 | frame_max, 87 | heartbeat, 88 | } 89 | } 90 | } 91 | 92 | #[derive(Debug, Serialize, Deserialize)] 93 | pub struct Open { 94 | virtual_host: ShortStr, 95 | /// Deprecated: "capabilities", must be zero 96 | capabilities: ShortStr, 97 | /// Deprecated: "insist", must be zero 98 | insist: Octect, 99 | } 100 | 101 | impl Open { 102 | pub fn new(virtual_host: ShortStr, capabilities: ShortStr) -> Self { 103 | Self { 104 | virtual_host, 105 | capabilities, 106 | insist: 0, 107 | } 108 | } 109 | } 110 | 111 | impl Default for Open { 112 | fn default() -> Self { 113 | Self { 114 | virtual_host: "/".try_into().unwrap(), 115 | capabilities: ShortStr::default(), 116 | insist: 0, 117 | } 118 | } 119 | } 120 | #[derive(Debug, Serialize, Deserialize)] 121 | pub struct OpenOk { 122 | /// Deprecated: "known-hosts", must be zero 123 | know_hosts: ShortStr, 124 | } 125 | /// Used by connection's [`close`] callback. 126 | /// 127 | /// AMQP method frame [close](https://github.com/rabbitmq/amqp-0.9.1-spec/blob/main/docs/amqp-0-9-1-reference.md#connection.close). 128 | /// 129 | /// [`close`]: callbacks/trait.ConnectionCallback.html#tymethod.close 130 | // TX + RX 131 | #[derive(Debug, Serialize, Deserialize)] 132 | pub struct Close { 133 | reply_code: ShortUint, 134 | reply_text: ShortStr, 135 | class_id: ShortUint, 136 | method_id: ShortUint, 137 | } 138 | impl fmt::Display for Close { 139 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 140 | f.write_fmt(format_args!( 141 | "'{}: {}', (class_id = {}, method_id = {})", 142 | self.reply_code(), 143 | self.reply_text(), 144 | self.class_id(), 145 | self.method_id() 146 | )) 147 | } 148 | } 149 | impl Close { 150 | pub fn reply_code(&self) -> u16 { 151 | self.reply_code 152 | } 153 | 154 | pub fn reply_text(&self) -> &String { 155 | self.reply_text.as_ref() 156 | } 157 | 158 | pub fn class_id(&self) -> u16 { 159 | self.class_id 160 | } 161 | 162 | pub fn method_id(&self) -> u16 { 163 | self.method_id 164 | } 165 | } 166 | impl Default for Close { 167 | // compliance: 168 | // Indicates that the method completed successfully. This reply code is 169 | // reserved for future use - the current protocol design does not use positive 170 | // confirmation and reply codes are sent only in case of an error. 171 | fn default() -> Self { 172 | Self { 173 | reply_code: REPLY_SUCCESS, 174 | reply_text: ShortStr::default(), 175 | class_id: 0, 176 | method_id: 0, 177 | } 178 | } 179 | } 180 | 181 | #[derive(Debug, Serialize, Deserialize, Default)] 182 | pub struct CloseOk; 183 | 184 | #[derive(Debug, Serialize, Deserialize)] 185 | pub struct Secure { 186 | pub(crate) challenge: LongStr, 187 | } 188 | 189 | #[derive(Debug, Serialize, Deserialize, Default)] 190 | pub struct SecureOk { 191 | response: LongStr, 192 | } 193 | 194 | impl SecureOk { 195 | #[allow(dead_code, /* challenge response not used in PLAIN/AMQPLIAN auth machanism*/)] 196 | pub fn new(response: LongStr) -> Self { 197 | Self { response } 198 | } 199 | } 200 | 201 | // See https://www.rabbitmq.com/resources/specs/amqp0-9-1.extended.xml 202 | #[derive(Debug, Serialize, Deserialize, Default)] 203 | pub struct Blocked { 204 | pub(crate) reason: ShortStr, 205 | } 206 | 207 | impl Blocked { 208 | pub fn new(reason: ShortStr) -> Self { 209 | Self { reason } 210 | } 211 | } 212 | 213 | #[derive(Debug, Serialize, Deserialize, Default)] 214 | pub struct Unblocked; 215 | 216 | #[derive(Debug, Serialize, Deserialize)] 217 | pub struct UpdateSecret { 218 | pub(crate) new_secret: LongStr, 219 | pub(crate) reason: ShortStr, 220 | } 221 | 222 | impl UpdateSecret { 223 | pub fn new(new_secret: LongStr, reason: ShortStr) -> Self { 224 | Self { new_secret, reason } 225 | } 226 | } 227 | 228 | #[derive(Debug, Serialize, Deserialize, Default)] 229 | pub struct UpdateSecretOk; 230 | -------------------------------------------------------------------------------- /amqprs/tests/test_mixed_type_consumers.rs: -------------------------------------------------------------------------------- 1 | use amqprs::{ 2 | channel::{ 3 | BasicAckArguments, BasicConsumeArguments, BasicNackArguments, BasicPublishArguments, 4 | BasicRejectArguments, Channel, QueueBindArguments, QueueDeclareArguments, 5 | }, 6 | connection::Connection, 7 | consumer::{AsyncConsumer, BlockingConsumer}, 8 | BasicProperties, Deliver, 9 | }; 10 | use async_trait::async_trait; 11 | use tokio::time; 12 | mod common; 13 | 14 | struct MixedConsumer { 15 | auto_ack: bool, 16 | } 17 | 18 | impl MixedConsumer { 19 | pub fn new(auto_ack: bool) -> Self { 20 | Self { auto_ack } 21 | } 22 | } 23 | 24 | #[async_trait] 25 | impl AsyncConsumer for MixedConsumer { 26 | async fn consume( 27 | &mut self, 28 | channel: &Channel, 29 | deliver: Deliver, 30 | basic_properties: BasicProperties, 31 | _content: Vec, 32 | ) { 33 | tracing::info!( 34 | "receive message properties {} on channel {}", 35 | basic_properties, 36 | channel 37 | ); 38 | // ack explicitly if manual ack 39 | if !self.auto_ack { 40 | let tag = deliver.delivery_tag(); 41 | let prio = basic_properties.priority().unwrap_or_else(|| 0); 42 | if prio <= 1 { 43 | tracing::info!("ack async"); 44 | 45 | channel 46 | .basic_ack(BasicAckArguments::new(tag, false)) 47 | .await 48 | .unwrap(); 49 | } 50 | // test nack 51 | else if prio == 2 { 52 | tracing::info!("nack async"); 53 | 54 | channel 55 | .basic_nack(BasicNackArguments::new(tag, false, false)) 56 | .await 57 | .unwrap(); 58 | } 59 | // test reject 60 | else { 61 | tracing::info!("reject async"); 62 | 63 | channel 64 | .basic_reject(BasicRejectArguments::new(tag, false)) 65 | .await 66 | .unwrap(); 67 | } 68 | } 69 | } 70 | } 71 | 72 | impl BlockingConsumer for MixedConsumer { 73 | fn consume( 74 | &mut self, 75 | channel: &Channel, 76 | deliver: Deliver, 77 | basic_properties: BasicProperties, 78 | _content: Vec, 79 | ) { 80 | tracing::info!( 81 | "receive message properties {} on channel {}", 82 | basic_properties, 83 | channel 84 | ); 85 | 86 | // ack explicitly if manual ack 87 | if !self.auto_ack { 88 | let tag = deliver.delivery_tag(); 89 | let prio = basic_properties.priority().unwrap_or_else(|| 0); 90 | if prio <= 1 { 91 | tracing::info!("ack blocking"); 92 | 93 | channel 94 | .basic_ack_blocking(BasicAckArguments::new(tag, false)) 95 | .unwrap(); 96 | } 97 | // test nack 98 | else if prio == 2 { 99 | tracing::info!("nack blocking"); 100 | 101 | channel 102 | .basic_nack_blocking(BasicNackArguments::new(tag, false, false)) 103 | .unwrap(); 104 | } 105 | // test reject 106 | else { 107 | tracing::info!("reject blocking"); 108 | 109 | channel 110 | .basic_reject_blocking(BasicRejectArguments::new(tag, false)) 111 | .unwrap(); 112 | } 113 | } 114 | } 115 | } 116 | 117 | #[tokio::test] 118 | async fn test_mixed_type_consumer() { 119 | common::setup_logging(); 120 | 121 | // open a connection to RabbitMQ server 122 | let args = common::build_conn_args(); 123 | let connection = Connection::open(&args).await.unwrap(); 124 | 125 | // open a channel dedicated for consumer on the connection 126 | let consumer_channel = connection.open_channel(None).await.unwrap(); 127 | 128 | let exchange_name = "amq.topic"; 129 | // declare a queue 130 | let (queue_name, ..) = consumer_channel 131 | .queue_declare(QueueDeclareArguments::default()) 132 | .await 133 | .unwrap() 134 | .unwrap(); 135 | 136 | // bind the queue to exchange 137 | let routing_key = "mixed_consumer_type"; 138 | consumer_channel 139 | .queue_bind(QueueBindArguments::new( 140 | &queue_name, 141 | exchange_name, 142 | routing_key, 143 | )) 144 | .await 145 | .unwrap(); 146 | 147 | // start consumer with given name 148 | let args = BasicConsumeArguments::new(&queue_name, "async_consumer"); 149 | 150 | consumer_channel 151 | .basic_consume(MixedConsumer::new(args.no_ack), args) 152 | .await 153 | .unwrap(); 154 | 155 | // start consumer with generated name by server 156 | let args = BasicConsumeArguments::new(&queue_name, "blocking_consumer"); 157 | consumer_channel 158 | .basic_consume_blocking(MixedConsumer::new(args.no_ack), args) 159 | .await 160 | .unwrap(); 161 | 162 | // open a channel dedicated for publisher on the connection 163 | let pub_channel = connection.open_channel(None).await.unwrap(); 164 | // publish test messages 165 | publish_test_messages(&pub_channel, exchange_name, routing_key, 3).await; 166 | 167 | // keep the `channel` and `connection` object from dropping 168 | // NOTE: channel/connection will be closed when drop 169 | time::sleep(time::Duration::from_secs(1)).await; 170 | 171 | // explicitly close 172 | pub_channel.close().await.unwrap(); 173 | consumer_channel.close().await.unwrap(); 174 | connection.close().await.unwrap(); 175 | } 176 | 177 | async fn publish_test_messages( 178 | channel: &Channel, 179 | exchange_name: &str, 180 | routing_key: &str, 181 | num: usize, 182 | ) { 183 | // contents to publish 184 | let content = String::from("testing mixed type consumer").into_bytes(); 185 | 186 | // create arguments for basic_publish 187 | let args = BasicPublishArguments::new(exchange_name, routing_key); 188 | 189 | let mut basic_props = BasicProperties::default(); 190 | 191 | for _ in 0..num { 192 | // expect ack from consumer 193 | basic_props.with_priority(1); 194 | channel 195 | .basic_publish(basic_props.clone(), content.clone(), args.clone()) 196 | .await 197 | .unwrap(); 198 | 199 | // expect nack from consumer 200 | basic_props.with_priority(2); 201 | channel 202 | .basic_publish(basic_props.clone(), content.clone(), args.clone()) 203 | .await 204 | .unwrap(); 205 | 206 | // expect reject from consumer 207 | basic_props.with_priority(3); 208 | basic_props.with_priority(3); 209 | channel 210 | .basic_publish(basic_props.clone(), content.clone(), args.clone()) 211 | .await 212 | .unwrap(); 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /amqprs/tests/test_get.rs: -------------------------------------------------------------------------------- 1 | use amqprs::{ 2 | callbacks::{DefaultChannelCallback, DefaultConnectionCallback}, 3 | channel::{ 4 | BasicAckArguments, BasicGetArguments, BasicPublishArguments, QueueBindArguments, 5 | QueueDeclareArguments, 6 | }, 7 | connection::Connection, 8 | BasicProperties, 9 | }; 10 | use tracing::info; 11 | mod common; 12 | 13 | #[tokio::test(flavor = "multi_thread", worker_threads = 2)] 14 | async fn test_get() { 15 | common::setup_logging(); 16 | 17 | // open a connection to RabbitMQ server 18 | let args = common::build_conn_args(); 19 | 20 | let connection = Connection::open(&args).await.unwrap(); 21 | connection 22 | .register_callback(DefaultConnectionCallback) 23 | .await 24 | .unwrap(); 25 | 26 | // open a channel on the connection 27 | let channel = connection.open_channel(None).await.unwrap(); 28 | channel 29 | .register_callback(DefaultChannelCallback) 30 | .await 31 | .unwrap(); 32 | let exchange_name = "amq.topic"; 33 | // declare a queue 34 | let (queue_name, ..) = channel 35 | .queue_declare(QueueDeclareArguments::default()) 36 | .await 37 | .unwrap() 38 | .unwrap(); 39 | 40 | // bind the queue to exchange 41 | let routing_key = "get.test"; // key should also be used by publish 42 | channel 43 | .queue_bind(QueueBindArguments::new( 44 | &queue_name, 45 | exchange_name, 46 | routing_key, 47 | )) 48 | .await 49 | .unwrap(); 50 | 51 | // get empty 52 | let get_args = BasicGetArguments::new(&queue_name); 53 | 54 | // contents to publish 55 | let content = String::from( 56 | r#" 57 | { 58 | "data": "some data to publish for test" 59 | } 60 | "#, 61 | ) 62 | .into_bytes(); 63 | 64 | // create arguments for basic_publish 65 | let args = BasicPublishArguments::new(&exchange_name, routing_key); 66 | 67 | let num_loop = 3; 68 | for _ in 0..num_loop { 69 | channel 70 | .basic_publish(BasicProperties::default(), content.clone(), args.clone()) 71 | .await 72 | .unwrap(); 73 | } 74 | 75 | for i in 0..num_loop { 76 | // get single message 77 | let delivery_tag = match channel.basic_get(get_args.clone()).await.unwrap() { 78 | Some((get_ok, basic_props, content)) => { 79 | #[cfg(feature = "tracing")] 80 | info!( 81 | "Get results: 82 | {} 83 | {} 84 | Content: {}", 85 | get_ok, 86 | basic_props, 87 | std::str::from_utf8(&content).unwrap() 88 | ); 89 | // message count should decrement accordingly 90 | assert_eq!(num_loop - 1 - i, get_ok.message_count()); 91 | get_ok.delivery_tag() 92 | } 93 | None => panic!("expect get a message"), 94 | }; 95 | // ack to received message 96 | channel 97 | .basic_ack(BasicAckArguments { 98 | delivery_tag, 99 | multiple: false, 100 | }) 101 | .await 102 | .unwrap(); 103 | } 104 | 105 | // test zero size content 106 | for _ in 0..num_loop { 107 | channel 108 | .basic_publish(BasicProperties::default(), Vec::new(), args.clone()) 109 | .await 110 | .unwrap(); 111 | } 112 | 113 | for i in 0..num_loop { 114 | // get single message 115 | let delivery_tag = match channel.basic_get(get_args.clone()).await.unwrap() { 116 | Some((get_ok, basic_props, content)) => { 117 | #[cfg(feature = "tracing")] 118 | info!( 119 | "Get results: 120 | {} 121 | {} 122 | Content: {}", 123 | get_ok, 124 | basic_props, 125 | std::str::from_utf8(&content).unwrap() 126 | ); 127 | // message count should decrement accordingly 128 | assert_eq!(num_loop - 1 - i, get_ok.message_count()); 129 | get_ok.delivery_tag() 130 | } 131 | None => panic!("expect get a message"), 132 | }; 133 | // ack to received message 134 | channel 135 | .basic_ack(BasicAckArguments { 136 | delivery_tag, 137 | multiple: false, 138 | }) 139 | .await 140 | .unwrap(); 141 | } 142 | 143 | // test message of size > frame_max 144 | let body_size = connection.frame_max() as usize + 10; 145 | for _ in 0..num_loop { 146 | let content = vec![1; body_size]; 147 | 148 | channel 149 | .basic_publish(BasicProperties::default(), content, args.clone()) 150 | .await 151 | .unwrap(); 152 | } 153 | 154 | for i in 0..num_loop { 155 | // get single message 156 | let delivery_tag = match channel.basic_get(get_args.clone()).await.unwrap() { 157 | Some((get_ok, _basic_props, content)) => { 158 | assert_eq!(body_size, content.len()); 159 | // message count should decrement accordingly 160 | assert_eq!(num_loop - 1 - i, get_ok.message_count()); 161 | get_ok.delivery_tag() 162 | } 163 | None => panic!("expect get a message"), 164 | }; 165 | // ack to received message 166 | channel 167 | .basic_ack(BasicAckArguments { 168 | delivery_tag, 169 | multiple: false, 170 | }) 171 | .await 172 | .unwrap(); 173 | } 174 | // explicitly close 175 | channel.close().await.unwrap(); 176 | connection.close().await.unwrap(); 177 | } 178 | 179 | #[tokio::test(flavor = "multi_thread", worker_threads = 2)] 180 | async fn test_get_empty() { 181 | common::setup_logging(); 182 | 183 | // open a connection to RabbitMQ server 184 | let args = common::build_conn_args(); 185 | 186 | let connection = Connection::open(&args).await.unwrap(); 187 | 188 | // open a channel on the connection 189 | let channel = connection.open_channel(None).await.unwrap(); 190 | 191 | let exchange_name = "amq.topic"; 192 | // declare a queue 193 | let (queue_name, ..) = channel 194 | .queue_declare(QueueDeclareArguments::default()) 195 | .await 196 | .unwrap() 197 | .unwrap(); 198 | 199 | // bind the queue to exchange 200 | channel 201 | .queue_bind(QueueBindArguments::new( 202 | &queue_name, 203 | exchange_name, 204 | "__no_one_use_this_key__", // this make sure we receive a empty response 205 | )) 206 | .await 207 | .unwrap(); 208 | 209 | // get empty 210 | let get_message = channel 211 | .basic_get(BasicGetArguments::new(&queue_name)) 212 | .await 213 | .unwrap(); 214 | if let Some(_) = get_message { 215 | panic!("expect ReturnEmpty message"); 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /amqprs/src/api/consumer.rs: -------------------------------------------------------------------------------- 1 | //! Callback interfaces of asynchronous content data consumer. 2 | //! 3 | //! The consumer is required by [`Channel::basic_consume`] or [`Channel::basic_consume_blocking`]. 4 | //! User should create its own consumer by implementing the trait [`AsyncConsumer`] or [`BlockingConsumer`]. 5 | //! 6 | //! # Examples 7 | //! 8 | //! See implementation of [`DefaultConsumer`] and [`DefaultBlockingConsumer`]. 9 | //! 10 | //! [`Channel::basic_consume`]: ../channel/struct.Channel.html#method.basic_consume 11 | //! [`Channel::basic_consume_blocking`]: ../channel/struct.Channel.html#method.basic_consume_blocking 12 | //! 13 | use super::channel::{BasicAckArguments, Channel}; 14 | use crate::frame::{BasicProperties, Deliver}; 15 | 16 | use async_trait::async_trait; 17 | #[cfg(feature = "traces")] 18 | use tracing::info; 19 | 20 | /// Trait defines the callback interfaces for consuming asynchronous content data from server. 21 | /// 22 | /// Continously consume the content data until the consumer is cancelled or channel is closed. 23 | #[async_trait] 24 | pub trait AsyncConsumer { 25 | /// Consume a delivery from Server. 26 | /// 27 | /// Every delivery combines a [Deliver](https://github.com/rabbitmq/amqp-0.9.1-spec/blob/main/docs/amqp-0-9-1-reference.md#basic.deliver) frame, 28 | /// message propertities, and content body. 29 | /// 30 | /// # Inputs 31 | /// 32 | /// `channel`: consumer's channel reference, typically used for acknowledge the delivery. 33 | /// 34 | /// `deliver`: see [basic.deliver](https://github.com/rabbitmq/amqp-0.9.1-spec/blob/main/docs/amqp-0-9-1-reference.md#basic.deliver) 35 | /// or [delivery metadata](https://www.rabbitmq.com/consumers.html#message-properties) 36 | /// 37 | /// `basic_properties`: see [message properties](https://www.rabbitmq.com/consumers.html#message-properties). 38 | /// 39 | /// `content`: the content body 40 | /// 41 | /// # Non-blocking and blocking consumer 42 | /// 43 | /// This method is invoked in a async task context, so its implementation should NOT be CPU bound, otherwise it will starving the async runtime. 44 | /// 45 | /// For CPU bound task (blocking consumer), possible solution below 46 | /// 1. User can spawn a blocking task using [tokio::spawn_blocking](https://docs.rs/tokio/latest/tokio/task/fn.spawn_blocking.html) 47 | /// for CPU bound job, and use [tokio's mpsc channel](https://docs.rs/tokio/latest/tokio/sync/mpsc/index.html#communicating-between-sync-and-async-code) 48 | /// to cummunicate between sync and async code. 49 | /// If too many blocking tasks, user can create a thread pool shared by all blocking tasks, and this method is only to forward message 50 | /// to corresponding blocking task. 51 | /// Also check [bridging async and blocking code](https://docs.rs/tokio/latest/tokio/task/fn.spawn_blocking.html#related-apis-and-patterns-for-bridging-asynchronous-and-blocking-code). 52 | /// 53 | /// 2. Create blocking consumer by implementing trait [`BlockingConsumer`], and use [`Channel::basic_consume_blocking`] 54 | /// to start consuming message in a blocking context. 55 | /// 56 | /// [`Channel::basic_consume_blocking`]: ../channel/struct.Channel.html#method.basic_consume_blocking 57 | async fn consume( 58 | &mut self, // use `&mut self` to make trait object to be `Sync` 59 | channel: &Channel, 60 | deliver: Deliver, 61 | basic_properties: BasicProperties, 62 | content: Vec, 63 | ); 64 | } 65 | 66 | /// Default type implements the [`AsyncConsumer`]. 67 | /// 68 | /// It is used for demo and debugging purposes only. 69 | pub struct DefaultConsumer { 70 | no_ack: bool, 71 | } 72 | 73 | impl DefaultConsumer { 74 | /// Return a new consumer. 75 | /// 76 | /// See [Acknowledgement Modes](https://www.rabbitmq.com/consumers.html#acknowledgement-modes) 77 | /// 78 | /// no_ack = [`true`] means automatic ack and should NOT send ACK to server. 79 | /// 80 | /// no_ack = [`false`] means manual ack, and should send ACK message to server. 81 | pub fn new(no_ack: bool) -> Self { 82 | Self { no_ack } 83 | } 84 | } 85 | 86 | #[async_trait] 87 | impl AsyncConsumer for DefaultConsumer { 88 | async fn consume( 89 | &mut self, 90 | channel: &Channel, 91 | deliver: Deliver, 92 | _basic_properties: BasicProperties, 93 | content: Vec, 94 | ) { 95 | #[cfg(feature = "traces")] 96 | info!( 97 | "consume delivery {} on channel {}, content size: {}", 98 | deliver, 99 | channel, 100 | content.len() 101 | ); 102 | 103 | // ack explicitly if manual ack 104 | if !self.no_ack { 105 | #[cfg(feature = "traces")] 106 | info!("ack to delivery {} on channel {}", deliver, channel); 107 | let args = BasicAckArguments::new(deliver.delivery_tag(), false); 108 | channel.basic_ack(args).await.unwrap(); 109 | } 110 | } 111 | } 112 | 113 | ////////////////////////////////////////////////////////////////////////////// 114 | /// Similar as [`AsyncConsumer`] but run in a blocking context, aiming for CPU bound task. 115 | pub trait BlockingConsumer { 116 | /// Except that a blocking consumer will be run in a separate Thread, otherwise see explanation 117 | /// in [`AsyncConsumer::consume`]. 118 | /// 119 | /// If there are too many blocking consumers, user is recommended to use a thread pool for all 120 | /// blocking tasks. See possible solution in [`non-blocking and blocking consumer`]. 121 | /// 122 | /// [`non-blocking and blocking consumer`]: trait.AsyncConsumer.html#non-blocking-and-blocking-consumer 123 | fn consume( 124 | &mut self, // use `&mut self` to make trait object to be `Sync` 125 | channel: &Channel, 126 | deliver: Deliver, 127 | basic_properties: BasicProperties, 128 | content: Vec, 129 | ); 130 | } 131 | 132 | /// Default type implements the [`BlockingConsumer`]. 133 | /// 134 | /// It is used for demo and debugging purposes only. 135 | pub struct DefaultBlockingConsumer { 136 | no_ack: bool, 137 | } 138 | 139 | impl DefaultBlockingConsumer { 140 | /// Return a new consumer. 141 | /// 142 | /// See [Acknowledgement Modes](https://www.rabbitmq.com/consumers.html#acknowledgement-modes) 143 | /// 144 | /// no_ack = [`true`] means automatic ack and should NOT send ACK to server. 145 | /// 146 | /// no_ack = [`false`] means manual ack, and should send ACK message to server. 147 | pub fn new(no_ack: bool) -> Self { 148 | Self { no_ack } 149 | } 150 | } 151 | 152 | impl BlockingConsumer for DefaultBlockingConsumer { 153 | fn consume( 154 | &mut self, 155 | channel: &Channel, 156 | deliver: Deliver, 157 | _basic_properties: BasicProperties, 158 | content: Vec, 159 | ) { 160 | #[cfg(feature = "traces")] 161 | info!( 162 | "consume delivery {} on channel {}, content size: {}", 163 | deliver, 164 | channel, 165 | content.len() 166 | ); 167 | 168 | // ack explicitly if manual ack 169 | if !self.no_ack { 170 | #[cfg(feature = "traces")] 171 | info!("ack to delivery {} on channel {}", deliver, channel); 172 | let args = BasicAckArguments::new(deliver.delivery_tag(), false); 173 | // should call blocking version of API because we are in blocing context 174 | channel.basic_ack_blocking(args).unwrap(); 175 | } 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /benchmarks/src/basic_pub_bencher.rs: -------------------------------------------------------------------------------- 1 | use criterion_bencher_compat::{benchmark_group, benchmark_main, Bencher}; 2 | mod common; 3 | use common::*; 4 | 5 | /// benchmark functions for `amqprs` client 6 | mod client_amqprs { 7 | use super::{get_size_list, rt, setup_tracing, Bencher}; 8 | use amqprs::{ 9 | callbacks::{DefaultChannelCallback, DefaultConnectionCallback}, 10 | channel::{ 11 | BasicPublishArguments, QueueBindArguments, QueueDeclareArguments, QueuePurgeArguments, 12 | }, 13 | connection::{Connection, OpenConnectionArguments}, 14 | BasicProperties, 15 | }; 16 | 17 | pub fn amqprs_basic_pub(bencher: &mut Bencher) { 18 | setup_tracing(); 19 | let rt = rt(); 20 | 21 | // open a connection to RabbitMQ server 22 | let connection = rt.block_on(async { 23 | let connection = Connection::open(&OpenConnectionArguments::new( 24 | "localhost", 25 | 5672, 26 | "user", 27 | "bitnami", 28 | )) 29 | .await 30 | .unwrap(); 31 | connection 32 | .register_callback(DefaultConnectionCallback) 33 | .await 34 | .unwrap(); 35 | connection 36 | }); 37 | 38 | // open a channel on the connection 39 | let channel = rt.block_on(async { 40 | let channel = connection.open_channel(None).await.unwrap(); 41 | channel 42 | .register_callback(DefaultChannelCallback) 43 | .await 44 | .unwrap(); 45 | channel 46 | }); 47 | 48 | ////////////////////////////////////////////////////////////////////////////// 49 | // publish message 50 | let rounting_key = "bench.amqprs.pub"; 51 | let exchange_name = "amq.topic"; 52 | let queue_name = "bench-amqprs-q"; 53 | rt.block_on(async { 54 | // declare a queue 55 | let (_, _, _) = channel 56 | .queue_declare(QueueDeclareArguments::new(queue_name)) 57 | .await 58 | .unwrap() 59 | .unwrap(); 60 | // bind queue to exchange 61 | channel 62 | .queue_bind(QueueBindArguments::new( 63 | queue_name, 64 | exchange_name, 65 | rounting_key, 66 | )) 67 | .await 68 | .unwrap(); 69 | }); 70 | 71 | let pubargs = BasicPublishArguments::new(exchange_name, rounting_key); 72 | let declargs = QueueDeclareArguments::new(queue_name) 73 | .passive(true) 74 | .finish(); 75 | 76 | let msg_size_list = get_size_list(connection.frame_max() as usize); 77 | // task to be benchmarked 78 | let task = || async { 79 | let count = msg_size_list.len(); 80 | // purge queue 81 | channel 82 | .queue_purge(QueuePurgeArguments::new(queue_name)) 83 | .await 84 | .unwrap(); 85 | let (_, msg_cnt, _) = channel 86 | .queue_declare( 87 | QueueDeclareArguments::new(queue_name) 88 | .passive(true) 89 | .finish(), 90 | ) 91 | .await 92 | .unwrap() 93 | .unwrap(); 94 | assert_eq!(0, msg_cnt); 95 | // publish messages of variable sizes 96 | for &i in msg_size_list.iter().take(count) { 97 | channel 98 | .basic_publish(BasicProperties::default(), vec![0xc5; i], pubargs.clone()) 99 | .await 100 | .unwrap(); 101 | } 102 | // check all messages arrived at queue 103 | loop { 104 | let (_, msg_cnt, _) = channel 105 | .queue_declare(declargs.clone()) 106 | .await 107 | .unwrap() 108 | .unwrap(); 109 | if count == msg_cnt as usize { 110 | break; 111 | } 112 | } 113 | }; 114 | // start benchmark 115 | bencher.iter(|| { 116 | rt.block_on(task()); 117 | }); 118 | // explicitly close 119 | rt.block_on(async { 120 | channel.close().await.unwrap(); 121 | connection.close().await.unwrap(); 122 | }); 123 | } 124 | } 125 | 126 | /// benchmark functions for `lapin` client 127 | mod client_lapin { 128 | use super::{get_size_list, rt, setup_tracing, Bencher}; 129 | use lapin::{ 130 | options::{BasicPublishOptions, QueueBindOptions, QueueDeclareOptions, QueuePurgeOptions}, 131 | types::FieldTable, 132 | BasicProperties, Connection, ConnectionProperties, 133 | }; 134 | use tokio_executor_trait::Tokio; 135 | 136 | pub fn lapin_basic_pub(bencher: &mut Bencher) { 137 | setup_tracing(); 138 | 139 | let rt = rt(); 140 | 141 | let uri = "amqp://user:bitnami@localhost:5672"; 142 | let options = ConnectionProperties::default() 143 | // Use tokio executor and reactor. 144 | // At the moment the reactor is only available for unix. 145 | .with_executor(Tokio::default().with_handle(rt.handle().clone())) 146 | .with_reactor(tokio_reactor_trait::Tokio); 147 | 148 | let (connection, channel) = rt.block_on(async { 149 | let connection = Connection::connect(uri, options).await.unwrap(); 150 | let channel = connection.create_channel().await.unwrap(); 151 | 152 | (connection, channel) 153 | }); 154 | 155 | let rounting_key = "bench.lapin.pub"; 156 | let exchange_name = "amq.topic"; 157 | let queue_name = "bench-lapin-q"; 158 | 159 | rt.block_on(async { 160 | channel 161 | .queue_declare( 162 | queue_name, 163 | QueueDeclareOptions::default(), 164 | FieldTable::default(), 165 | ) 166 | .await 167 | .unwrap(); 168 | channel 169 | .queue_bind( 170 | queue_name, 171 | exchange_name, 172 | rounting_key, 173 | QueueBindOptions::default(), 174 | FieldTable::default(), 175 | ) 176 | .await 177 | .unwrap(); 178 | }); 179 | 180 | let pubopts = BasicPublishOptions::default(); 181 | let declopts = QueueDeclareOptions { 182 | passive: true, 183 | ..Default::default() 184 | }; 185 | 186 | let msg_size_list = get_size_list(connection.configuration().frame_max() as usize); 187 | 188 | let task = || async { 189 | let count = msg_size_list.len(); 190 | // purge queue 191 | channel 192 | .queue_purge(queue_name, QueuePurgeOptions::default()) 193 | .await 194 | .unwrap(); 195 | let q_state = channel 196 | .queue_declare(queue_name, declopts, FieldTable::default()) 197 | .await 198 | .unwrap(); 199 | 200 | assert_eq!(0, q_state.message_count()); 201 | // publish messages of variable sizes 202 | for &i in msg_size_list.iter().take(count) { 203 | let _confirm = channel 204 | .basic_publish( 205 | exchange_name, 206 | rounting_key, 207 | pubopts, 208 | &vec![0xc5; i], 209 | BasicProperties::default(), 210 | ) 211 | .await 212 | .unwrap() 213 | .await 214 | .unwrap(); 215 | } 216 | // check all messages arrived at queue 217 | loop { 218 | let q_state = channel 219 | .queue_declare(queue_name, declopts, FieldTable::default()) 220 | .await 221 | .unwrap(); 222 | if count == q_state.message_count() as usize { 223 | break; 224 | } 225 | } 226 | }; 227 | // start benchmark 228 | bencher.iter(|| { 229 | rt.block_on(task()); 230 | }); 231 | 232 | rt.block_on(async { 233 | channel.close(0, "").await.unwrap(); 234 | connection.close(0, "").await.unwrap(); 235 | }); 236 | } 237 | } 238 | 239 | benchmark_group!(amqprs, client_amqprs::amqprs_basic_pub,); 240 | benchmark_group!(lapin, client_lapin::lapin_basic_pub,); 241 | 242 | benchmark_main!(amqprs, lapin); 243 | -------------------------------------------------------------------------------- /benchmarks/src/basic_pub_criterion.rs: -------------------------------------------------------------------------------- 1 | use criterion::{criterion_group, criterion_main, Criterion}; 2 | mod common; 3 | use common::*; 4 | 5 | /// benchmark functions for `amqprs` client 6 | mod client_amqprs { 7 | use super::{get_size_list, rt, setup_tracing, Criterion}; 8 | use amqprs::{ 9 | callbacks::{DefaultChannelCallback, DefaultConnectionCallback}, 10 | channel::{ 11 | BasicPublishArguments, QueueBindArguments, QueueDeclareArguments, QueuePurgeArguments, 12 | }, 13 | connection::{Connection, OpenConnectionArguments}, 14 | BasicProperties, 15 | }; 16 | 17 | pub fn amqprs_basic_pub(c: &mut Criterion) { 18 | setup_tracing(); 19 | 20 | let rt = rt(); 21 | 22 | // open a connection to RabbitMQ server 23 | let connection = rt.block_on(async { 24 | let connection = Connection::open(&OpenConnectionArguments::new( 25 | "localhost", 26 | 5672, 27 | "user", 28 | "bitnami", 29 | )) 30 | .await 31 | .unwrap(); 32 | connection 33 | .register_callback(DefaultConnectionCallback) 34 | .await 35 | .unwrap(); 36 | connection 37 | }); 38 | 39 | // open a channel on the connection 40 | let channel = rt.block_on(async { 41 | let channel = connection.open_channel(None).await.unwrap(); 42 | channel 43 | .register_callback(DefaultChannelCallback) 44 | .await 45 | .unwrap(); 46 | channel 47 | }); 48 | 49 | ////////////////////////////////////////////////////////////////////////////// 50 | // publish message 51 | let rounting_key = "bench.amqprs.pub"; 52 | let exchange_name = "amq.topic"; 53 | let queue_name = "bench-amqprs-q"; 54 | rt.block_on(async { 55 | // declare a queue 56 | let (_, _, _) = channel 57 | .queue_declare(QueueDeclareArguments::new(queue_name)) 58 | .await 59 | .unwrap() 60 | .unwrap(); 61 | // bind queue to exchange 62 | channel 63 | .queue_bind(QueueBindArguments::new( 64 | queue_name, 65 | exchange_name, 66 | rounting_key, 67 | )) 68 | .await 69 | .unwrap(); 70 | }); 71 | 72 | let pubargs = BasicPublishArguments::new(exchange_name, rounting_key); 73 | let declargs = QueueDeclareArguments::new(queue_name) 74 | .passive(true) 75 | .finish(); 76 | 77 | let msg_size_list = get_size_list(connection.frame_max() as usize); 78 | // task to be benchmarked 79 | let task = || async { 80 | let count = msg_size_list.len(); 81 | // purge queue 82 | channel 83 | .queue_purge(QueuePurgeArguments::new(queue_name)) 84 | .await 85 | .unwrap(); 86 | let (_, msg_cnt, _) = channel 87 | .queue_declare( 88 | QueueDeclareArguments::new(queue_name) 89 | .passive(true) 90 | .finish(), 91 | ) 92 | .await 93 | .unwrap() 94 | .unwrap(); 95 | assert_eq!(0, msg_cnt); 96 | // publish messages of variable sizes 97 | for &i in msg_size_list.iter().take(count) { 98 | channel 99 | .basic_publish(BasicProperties::default(), vec![0xc5; i], pubargs.clone()) 100 | .await 101 | .unwrap(); 102 | } 103 | // check all messages arrived at queue 104 | loop { 105 | let (_, msg_cnt, _) = channel 106 | .queue_declare(declargs.clone()) 107 | .await 108 | .unwrap() 109 | .unwrap(); 110 | if count == msg_cnt as usize { 111 | break; 112 | } 113 | } 114 | }; 115 | // start benchmark 116 | c.bench_function("amqprs-basic-pub", |b| { 117 | b.iter(|| { 118 | rt.block_on(task()); 119 | }) 120 | }); 121 | // explicitly close 122 | rt.block_on(async { 123 | channel.close().await.unwrap(); 124 | connection.close().await.unwrap(); 125 | }); 126 | } 127 | } 128 | 129 | /// benchmark functions for `lapin` client 130 | mod client_lapin { 131 | 132 | use super::{get_size_list, rt, setup_tracing, Criterion}; 133 | use lapin::{ 134 | options::{BasicPublishOptions, QueueBindOptions, QueueDeclareOptions, QueuePurgeOptions}, 135 | types::FieldTable, 136 | BasicProperties, Connection, ConnectionProperties, 137 | }; 138 | use tokio_executor_trait::Tokio; 139 | 140 | pub fn lapin_basic_pub(c: &mut Criterion) { 141 | setup_tracing(); 142 | 143 | let rt = rt(); 144 | 145 | let uri = "amqp://user:bitnami@localhost:5672"; 146 | let options = ConnectionProperties::default() 147 | // Use tokio executor and reactor. 148 | // At the moment the reactor is only available for unix. 149 | .with_executor(Tokio::default().with_handle(rt.handle().clone())) 150 | .with_reactor(tokio_reactor_trait::Tokio); 151 | 152 | let (connection, channel) = rt.block_on(async { 153 | let connection = Connection::connect(uri, options).await.unwrap(); 154 | let channel = connection.create_channel().await.unwrap(); 155 | 156 | (connection, channel) 157 | }); 158 | 159 | let rounting_key = "bench.lapin.pub"; 160 | let exchange_name = "amq.topic"; 161 | let queue_name = "bench-lapin-q"; 162 | 163 | rt.block_on(async { 164 | channel 165 | .queue_declare( 166 | queue_name, 167 | QueueDeclareOptions::default(), 168 | FieldTable::default(), 169 | ) 170 | .await 171 | .unwrap(); 172 | channel 173 | .queue_bind( 174 | queue_name, 175 | exchange_name, 176 | rounting_key, 177 | QueueBindOptions::default(), 178 | FieldTable::default(), 179 | ) 180 | .await 181 | .unwrap(); 182 | }); 183 | 184 | let pubopts = BasicPublishOptions::default(); 185 | let declopts = QueueDeclareOptions { 186 | passive: true, 187 | ..Default::default() 188 | }; 189 | 190 | let msg_size_list = get_size_list(connection.configuration().frame_max() as usize); 191 | 192 | let task = || async { 193 | let count = msg_size_list.len(); 194 | // purge queue 195 | channel 196 | .queue_purge(queue_name, QueuePurgeOptions::default()) 197 | .await 198 | .unwrap(); 199 | let q_state = channel 200 | .queue_declare(queue_name, declopts, FieldTable::default()) 201 | .await 202 | .unwrap(); 203 | 204 | assert_eq!(0, q_state.message_count()); 205 | // publish messages of variable sizes 206 | for &i in msg_size_list.iter().take(count) { 207 | let _confirm = channel 208 | .basic_publish( 209 | exchange_name, 210 | rounting_key, 211 | pubopts, 212 | &vec![0xc5; i], 213 | BasicProperties::default(), 214 | ) 215 | .await 216 | .unwrap() 217 | .await 218 | .unwrap(); 219 | } 220 | // check all messages arrived at queue 221 | loop { 222 | let q_state = channel 223 | .queue_declare(queue_name, declopts, FieldTable::default()) 224 | .await 225 | .unwrap(); 226 | if count == q_state.message_count() as usize { 227 | break; 228 | } 229 | } 230 | }; 231 | 232 | // start benchmark 233 | c.bench_function("lapin-basic-pub", |b| { 234 | b.iter(|| { 235 | rt.block_on(task()); 236 | }) 237 | }); 238 | 239 | rt.block_on(async { 240 | channel.close(0, "").await.unwrap(); 241 | connection.close(0, "").await.unwrap(); 242 | }); 243 | } 244 | } 245 | 246 | criterion_group! { 247 | name = basic_pub; 248 | config = Criterion::default(); 249 | targets = client_amqprs::amqprs_basic_pub, client_lapin::lapin_basic_pub 250 | } 251 | 252 | criterion_main!(basic_pub); 253 | -------------------------------------------------------------------------------- /amqprs/src/api/callbacks.rs: -------------------------------------------------------------------------------- 1 | //! Callback interfaces of asynchronous message for [`Connection`] and [`Channel`]. 2 | //! 3 | //! In AMQP_0-9-1 protocol, some messages (`methods` in AMQP's term) can be initiated by server. 4 | //! These messages are handled asynchronously by client via callbacks. 5 | //! 6 | //! User should define its own callback types and implement the traits [`ConnectionCallback`] 7 | //! and [`ChannelCallback`]. 8 | //! 9 | //! After open a connection, immediately register the callbacks by [`Connection::register_callback`]. 10 | //! After open a channel, immediately register the callbacks by [`Channel::register_callback`]. 11 | //! 12 | //! # Examples 13 | //! See [`DefaultConnectionCallback`] and [`DefaultChannelCallback`] for simple example. 14 | //! 15 | //! The default callback implementations are only for demo and debugging purposes. 16 | //! User is expected to implement its own callbacks. 17 | //! 18 | //! [`Connection`]: ../connection/struct.Connection.html 19 | //! [`Connection::register_callback`]: ../connection/struct.Connection.html#method.register_callback 20 | //! [`Channel`]: ../channel/struct.Channel.html 21 | //! [`Channel::register_callback`]: ../channel/struct.Channel.html#method.register_callback 22 | 23 | use super::{channel::Channel, connection::Connection}; 24 | use crate::api::Result; 25 | use crate::frame::Cancel; 26 | use crate::{ 27 | frame::{Ack, Close, CloseChannel, Nack, Return}, 28 | BasicProperties, 29 | }; 30 | use async_trait::async_trait; 31 | #[cfg(feature = "traces")] 32 | use tracing::{error, info, warn}; 33 | 34 | ///////////////////////////////////////////////////////////////////////////// 35 | /// Callback interfaces for asynchronous `Connection` class message. 36 | /// 37 | /// See [module][`self`] documentation for general guidelines. 38 | #[async_trait] 39 | pub trait ConnectionCallback { 40 | /// Callback to handle `close` connection request from server. 41 | /// 42 | /// Returns [`Ok`] to reply server that the request is received and 43 | /// handled properly. 44 | /// 45 | /// # Errors 46 | /// 47 | /// If returns [`Err`], no reply to server, which means server won't know 48 | /// whether the request has been received by client, and may consider 49 | /// the connection isn't shutdown. 50 | async fn close(&mut self, connection: &Connection, close: Close) -> Result<()>; 51 | 52 | /// Callback to handle connection `blocked` indication from server 53 | async fn blocked(&mut self, connection: &Connection, reason: String); 54 | 55 | /// Callback to handle connection `unblocked` indication from server 56 | async fn unblocked(&mut self, connection: &Connection); 57 | 58 | /// Callback to handle secret updated indication from server 59 | async fn secret_updated(&mut self, connection: &Connection); 60 | } 61 | 62 | /// Default type that implements `ConnectionCallback`. 63 | /// 64 | /// For demo and debugging purpose only. 65 | pub struct DefaultConnectionCallback; 66 | 67 | #[async_trait] 68 | impl ConnectionCallback for DefaultConnectionCallback { 69 | async fn close(&mut self, connection: &Connection, close: Close) -> Result<()> { 70 | #[cfg(feature = "traces")] 71 | error!( 72 | "handle close request for connection {}, cause: {}", 73 | connection, close 74 | ); 75 | Ok(()) 76 | } 77 | 78 | async fn blocked(&mut self, connection: &Connection, reason: String) { 79 | #[cfg(feature = "traces")] 80 | info!( 81 | "handle blocked notification for connection {}, reason: {}", 82 | connection, reason 83 | ); 84 | } 85 | 86 | async fn unblocked(&mut self, connection: &Connection) { 87 | #[cfg(feature = "traces")] 88 | info!( 89 | "handle unblocked notification for connection {}", 90 | connection 91 | ); 92 | } 93 | 94 | async fn secret_updated(&mut self, connection: &Connection) { 95 | #[cfg(feature = "traces")] 96 | info!( 97 | "handle secret updated notification for connection {}", 98 | connection 99 | ); 100 | } 101 | } 102 | 103 | ///////////////////////////////////////////////////////////////////////////// 104 | /// Callback interfaces for asynchronous `Channel` class message. 105 | /// 106 | /// See [module][`self`] documentation for general guidelines. 107 | #[async_trait] 108 | pub trait ChannelCallback { 109 | /// Callback to handle `close` channel request from server. 110 | /// 111 | /// Returns [`Ok`] to reply server that the request is received and 112 | /// handled properly. 113 | /// 114 | /// # Errors 115 | /// 116 | /// If returns [`Err`], no reply to server, which means server won't know 117 | /// whether the request has been received by client, and may consider 118 | /// the channel isn't closed. 119 | async fn close(&mut self, channel: &Channel, close: CloseChannel) -> Result<()>; 120 | 121 | /// Callback to handle server's request to `cancel` the consumer of current channel. 122 | /// 123 | /// Returns [`Ok`] to reply server that request has been received and 124 | /// the consumer will be cancelled. 125 | /// 126 | /// # Errors 127 | /// 128 | /// If returns [`Err`], no reply to server and no consumer will be cancelled. 129 | async fn cancel(&mut self, channel: &Channel, cancel: Cancel) -> Result<()>; 130 | 131 | /// Callback to handle server's `flow` request to pause or restart 132 | /// the flow of sending content data. 133 | /// 134 | /// if `active` = [`true`], request to start, otherwise to pause. 135 | /// 136 | /// Returns [`true`] to indicate to server that client starts sending data. 137 | /// Returns [`false`] to indicate to server that client stops sending data. 138 | async fn flow(&mut self, channel: &Channel, active: bool) -> Result; 139 | 140 | /// Callback to handle `ack` indication from server. 141 | /// 142 | /// Only occurs in `publish confirm` mode, sent by server to acknowledges 143 | /// one or more messages published. 144 | async fn publish_ack(&mut self, channel: &Channel, ack: Ack); 145 | 146 | /// Callback to handle `nack` indication from server. 147 | /// 148 | /// Only occurs in `publish confirm` mode, sent by server to inform publisher 149 | /// of unhandled messages. 150 | async fn publish_nack(&mut self, channel: &Channel, nack: Nack); 151 | 152 | /// Callback to handle `return` indication with undeliverable message from server. 153 | /// 154 | /// The [ret][`Return`] contains the reason why the message is returned. 155 | /// 156 | /// The [basic_properties][`BasicProperties`] contains the propertities 157 | /// of the returned message. 158 | /// 159 | /// The [content][`Vec`] contains the body of the returned message. 160 | /// 161 | /// [`Return`]: ../struct.Return.html 162 | /// [`BasicProperties`]: ../struct.BasicProperties.html 163 | async fn publish_return( 164 | &mut self, 165 | channel: &Channel, 166 | ret: Return, 167 | basic_properties: BasicProperties, 168 | content: Vec, 169 | ); 170 | } 171 | 172 | /// Default type that implements `ChannelCallback`. 173 | /// 174 | /// For demo and debugging purpose only. 175 | pub struct DefaultChannelCallback; 176 | 177 | #[async_trait] 178 | impl ChannelCallback for DefaultChannelCallback { 179 | async fn close(&mut self, channel: &Channel, close: CloseChannel) -> Result<()> { 180 | #[cfg(feature = "traces")] 181 | error!( 182 | "handle close request for channel {}, cause: {}", 183 | channel, close 184 | ); 185 | Ok(()) 186 | } 187 | async fn cancel(&mut self, channel: &Channel, cancel: Cancel) -> Result<()> { 188 | #[cfg(feature = "traces")] 189 | warn!( 190 | "handle cancel request for consumer {} on channel {}", 191 | cancel.consumer_tag(), 192 | channel 193 | ); 194 | Ok(()) 195 | } 196 | async fn flow(&mut self, channel: &Channel, active: bool) -> Result { 197 | #[cfg(feature = "traces")] 198 | info!( 199 | "handle flow request active={} for channel {}", 200 | active, channel 201 | ); 202 | Ok(true) 203 | } 204 | async fn publish_ack(&mut self, channel: &Channel, ack: Ack) { 205 | #[cfg(feature = "traces")] 206 | info!( 207 | "handle publish ack delivery_tag={} on channel {}", 208 | ack.delivery_tag(), 209 | channel 210 | ); 211 | } 212 | async fn publish_nack(&mut self, channel: &Channel, nack: Nack) { 213 | #[cfg(feature = "traces")] 214 | warn!( 215 | "handle publish nack delivery_tag={} on channel {}", 216 | nack.delivery_tag(), 217 | channel 218 | ); 219 | } 220 | async fn publish_return( 221 | &mut self, 222 | channel: &Channel, 223 | ret: Return, 224 | _basic_properties: BasicProperties, 225 | content: Vec, 226 | ) { 227 | #[cfg(feature = "traces")] 228 | warn!( 229 | "handle publish return {} on channel {}, content size: {}", 230 | ret, 231 | channel, 232 | content.len() 233 | ); 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /amqprs/src/frame/mod.rs: -------------------------------------------------------------------------------- 1 | use amqp_serde::{ 2 | from_bytes, 3 | types::{AmqpChannelId, LongUint, Octect, ShortUint}, 4 | }; 5 | 6 | use serde::{Deserialize, Serialize}; 7 | use std::fmt; 8 | 9 | //////////////////////////////////////////////////////////////////////// 10 | // macros should appear before module declaration 11 | #[macro_use] 12 | mod helpers { 13 | // common interfaces of each method type 14 | macro_rules! impl_method_frame { 15 | ($name:ident, $class_id:literal, $method_id:literal) => { 16 | impl $name { 17 | pub(crate) fn header() -> &'static MethodHeader { 18 | static __METHOD_HEADER: MethodHeader = MethodHeader::new($class_id, $method_id); 19 | &__METHOD_HEADER 20 | } 21 | pub(crate) fn into_frame(self) -> Frame { 22 | Frame::$name(Self::header(), self) 23 | } 24 | } 25 | }; 26 | } 27 | 28 | macro_rules! impl_frame { 29 | ($($class_id:literal => $($method_id:literal : $method:ident),+);+) => { 30 | /// function to decode method frame 31 | fn decode_method_frame(header: MethodHeader, content: &[u8]) -> Result { 32 | match header.class_id() { 33 | $($class_id => { 34 | match header.method_id() { 35 | $($method_id => Ok(from_bytes::<$method>(content)?.into_frame()),)+ 36 | _ => unimplemented!("unknown method id"), 37 | } 38 | })+ 39 | _ => unimplemented!("unknown class id"), 40 | } 41 | } 42 | 43 | // common interfaces of each method type 44 | $($(impl_method_frame!{$method, $class_id, $method_id})+)+ 45 | 46 | /// `Frame` enum to generailize various frames. 47 | /// To avoid generic type parameter for new type depends on `Frame`. 48 | /// Only wrap the frame payload in enum variant, excluding the `FrameHeader` and FRAME_END byte 49 | /// The `Frame` type only need to implement Serialize, because when decoding a `Frame`, 50 | /// `FrameHeader`, its payload, and `FRAME_END` bytes are desrialized separately 51 | #[derive(Debug, Serialize)] 52 | #[serde(untagged)] 53 | pub enum Frame { 54 | /// method frame payload = method header + method 55 | $($($method(&'static MethodHeader, $method),)+)+ 56 | 57 | HeartBeat(HeartBeat), 58 | ContentHeader(Box), 59 | ContentBody(ContentBody), 60 | // speical frame combination for publish 61 | PublishCombo(Publish, Box, ContentBody), 62 | } 63 | }; 64 | } 65 | } 66 | /////////////////////////////////////////////////////////// 67 | mod constants; 68 | mod content_body; 69 | mod content_header; 70 | mod error; 71 | mod heartbeat; 72 | mod method; 73 | mod protocol_header; 74 | 75 | pub use constants::*; 76 | pub use content_body::*; 77 | pub use content_header::*; 78 | pub use error::*; 79 | pub use heartbeat::*; 80 | pub use method::*; 81 | pub use protocol_header::*; 82 | 83 | ///////////////////////////////////////////////////////////////// 84 | impl_frame! { 85 | // == Connection == 86 | 10 => 10: Start, 87 | 11: StartOk, 88 | 20: Secure, 89 | 21: SecureOk, 90 | 30: Tune, 91 | 31: TuneOk, 92 | 40: Open, 93 | 41: OpenOk, 94 | 50: Close, 95 | 51: CloseOk, 96 | 60: Blocked, 97 | 61: Unblocked, 98 | 70: UpdateSecret, 99 | 71: UpdateSecretOk; 100 | // == Channel == 101 | 20 => 10: OpenChannel, 102 | 11: OpenChannelOk, 103 | 20: Flow, 104 | 21: FlowOk, 105 | 40: CloseChannel, 106 | 41: CloseChannelOk; 107 | // == Access == Deprecated: https://www.rabbitmq.com/spec-differences.html 108 | // 30 => 10: Request, 109 | // 11: RequestOk; 110 | // == Exchange == 111 | 40 => 10: Declare, 112 | 11: DeclareOk, 113 | 20: Delete, 114 | 21: DeleteOk, 115 | 30: Bind, 116 | 31: BindOk, 117 | 40: Unbind, 118 | 51: UnbindOk; 119 | // == Queue == 120 | 50 => 10: DeclareQueue, 121 | 11: DeclareQueueOk, 122 | 20: BindQueue, 123 | 21: BindQueueOk, 124 | 30: PurgeQueue, 125 | 31: PurgeQueueOk, 126 | 40: DeleteQueue, 127 | 41: DeleteQueueOk, 128 | 50: UnbindQueue, 129 | 51: UnbindQueueOk; 130 | // == Basic == 131 | 60 => 10: Qos, 132 | 11: QosOk, 133 | 20: Consume, 134 | 21: ConsumeOk, 135 | 30: Cancel, 136 | 31: CancelOk, 137 | 40: Publish, 138 | 50: Return, 139 | 60: Deliver, 140 | 70: Get, 141 | 71: GetOk, 142 | 72: GetEmpty, 143 | 80: Ack, 144 | 90: Reject, 145 | // 100: RecoverAsync, // Deprecated 146 | 110: Recover, 147 | 111: RecoverOk, 148 | 120: Nack; 149 | // == Confirm == 150 | 85 => 10: Select, 151 | 11: SelectOk; 152 | // == Transaction == 153 | 90 => 10: TxSelect, 154 | 11: TxSelectOk, 155 | 20: TxCommit, 156 | 21: TxCommitOk, 157 | 30: TxRollback, 158 | 31: TxRollbackOk 159 | } 160 | 161 | ////////////////////////////////////////////////////////////////////// 162 | 163 | #[derive(Debug, Serialize, Deserialize, Default)] 164 | pub struct FrameHeader { 165 | pub frame_type: Octect, // 1: method, 2: content-header, 3: content-body, 8: heartbeat 166 | pub channel: ShortUint, 167 | pub payload_size: LongUint, 168 | } 169 | 170 | impl fmt::Display for Frame { 171 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 172 | write!(f, "{:?}", self) 173 | } 174 | } 175 | 176 | impl Frame { 177 | pub fn get_frame_type(&self) -> Octect { 178 | match self { 179 | Frame::HeartBeat(_) => FRAME_HEARTBEAT, 180 | Frame::ContentHeader(_) => FRAME_CONTENT_HEADER, 181 | Frame::ContentBody(_) => FRAME_CONTENT_BODY, 182 | _ => FRAME_METHOD, 183 | } 184 | } 185 | 186 | /// To support channels multiplex on one connection, need to populate the channel id 187 | /// to support update of read buffer cursor, and the number of bytes are read 188 | /// Returns: 189 | /// (num of bytes read, channel id, decoded frame) 190 | pub fn decode(buf: &[u8]) -> Result, Error> { 191 | // check frame header, 7 octects 192 | if buf.len() < FRAME_HEADER_SIZE { 193 | return Ok(None); 194 | } 195 | 196 | let FrameHeader { 197 | frame_type, 198 | channel, 199 | payload_size, 200 | } = from_bytes(match buf.get(0..FRAME_HEADER_SIZE) { 201 | Some(s) => s, 202 | None => unreachable!("out of bound"), 203 | })?; 204 | 205 | // check full frame is received payload_size + 8 octects 206 | let total_size = payload_size as usize + FRAME_HEADER_SIZE + 1; 207 | if total_size > buf.len() { 208 | return Ok(None); 209 | } 210 | // check frame end 211 | match buf.get(total_size - 1) { 212 | Some(v) => { 213 | // expect frame_end 214 | if &FRAME_END != v { 215 | return Err(Error::Corrupted); 216 | } 217 | } 218 | None => unreachable!("out of bound"), 219 | }; 220 | 221 | // parse frame payload 222 | match frame_type { 223 | FRAME_METHOD => { 224 | let header: MethodHeader = 225 | from_bytes(match buf.get(FRAME_HEADER_SIZE..FRAME_HEADER_SIZE + 4) { 226 | Some(s) => s, 227 | None => unreachable!("out of bound"), 228 | })?; 229 | let method_raw = match buf.get(FRAME_HEADER_SIZE + 4..total_size - 1) { 230 | Some(s) => s, 231 | None => unreachable!("out of bound"), 232 | }; 233 | 234 | let frame = decode_method_frame(header, method_raw)?; 235 | 236 | Ok(Some((total_size, channel, frame))) 237 | } 238 | FRAME_HEARTBEAT => Ok(Some((total_size, channel, Frame::HeartBeat(HeartBeat)))), 239 | FRAME_CONTENT_HEADER => { 240 | let mut start = FRAME_HEADER_SIZE; 241 | let mut end = start + 12; 242 | let header_common: ContentHeaderCommon = from_bytes(match buf.get(start..end) { 243 | Some(s) => s, 244 | None => unreachable!("out of bound"), 245 | })?; 246 | 247 | start = end; 248 | end = total_size - 1; 249 | let basic_properties: BasicProperties = from_bytes(match buf.get(start..end) { 250 | Some(s) => s, 251 | None => unreachable!("out of bound"), 252 | })?; 253 | 254 | Ok(Some(( 255 | total_size, 256 | channel, 257 | Frame::ContentHeader(Box::new(ContentHeader::new( 258 | header_common, 259 | basic_properties, 260 | ))), 261 | ))) 262 | } 263 | FRAME_CONTENT_BODY => { 264 | let start = FRAME_HEADER_SIZE; 265 | let end = total_size - 1; 266 | let body = buf.get(start..end).expect("should never fail"); 267 | Ok(Some(( 268 | total_size, 269 | channel, 270 | Frame::ContentBody(ContentBody::new(body.to_vec())), 271 | ))) 272 | } 273 | _ => Err(Error::Corrupted), 274 | } 275 | } 276 | } 277 | 278 | ///////////////////////////////////////////////////////////////////////////// 279 | --------------------------------------------------------------------------------