mq-bridge-py-basic 0.2.17__tar.gz → 0.2.18__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (186) hide show
  1. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/Cargo.lock +2 -2
  2. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/Cargo.toml +1 -1
  3. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/PKG-INFO +1 -1
  4. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/scripts/httparena/README.md +5 -4
  5. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/scripts/httparena/frameworks/mq-bridge-py/Dockerfile +1 -1
  6. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/scripts/techempower/Python/mq-bridge-py/mq-bridge-py.dockerfile +1 -1
  7. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/scripts/the-benchmarker/README.md +1 -1
  8. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/scripts/the-benchmarker/python/mq-bridge-py/config.yaml +1 -1
  9. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/src/endpoints/amqp.rs +189 -82
  10. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/src/endpoints/http.rs +324 -32
  11. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/src/models.rs +24 -0
  12. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  13. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  14. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  15. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/.github/workflows/benchmark.yml +0 -0
  16. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/.github/workflows/ci.yml +0 -0
  17. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/.github/workflows/ibm-mq.yml +0 -0
  18. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/.github/workflows/publish-python.yml +0 -0
  19. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/.github/workflows/python.yml +0 -0
  20. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/.github/workflows/release.yml +0 -0
  21. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/.github/workflows/test-matrix.yml +0 -0
  22. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/.gitignore +0 -0
  23. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/ARCHITECTURE.md +0 -0
  24. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/CONFIGURATION.md +0 -0
  25. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/CONTRIBUTING.md +0 -0
  26. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/LICENSE +0 -0
  27. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/LICENSE.python +0 -0
  28. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/README.md +0 -0
  29. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/benches/performance_bench.rs +0 -0
  30. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/build.rs +0 -0
  31. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/claude.md +0 -0
  32. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/examples/ipc_worker_queue.yaml +0 -0
  33. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/mq-bridge.schema.json +0 -0
  34. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/mq_bridge/__init__.py +0 -0
  35. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/mq_bridge/__init__.pyi +0 -0
  36. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/mq_bridge/py.typed +0 -0
  37. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/pyproject.toml +0 -0
  38. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/python/mq-bridge-py/Cargo.toml +0 -0
  39. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/python/mq-bridge-py/LICENSE.python +0 -0
  40. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/python/mq-bridge-py/README.md +0 -0
  41. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/python/mq-bridge-py/analysis/bench_http_native.py +0 -0
  42. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/python/mq-bridge-py/example.py +0 -0
  43. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/python/mq-bridge-py/examples/bench_memory.py +0 -0
  44. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/python/mq-bridge-py/examples/bench_memory.yaml +0 -0
  45. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/python/mq-bridge-py/examples/json_route.py +0 -0
  46. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/python/mq-bridge-py/examples/memory.yaml +0 -0
  47. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/python/mq-bridge-py/examples/raw_route.py +0 -0
  48. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/python/mq-bridge-py/pyproject-basic.toml +0 -0
  49. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/python/mq-bridge-py/src/lib.rs +0 -0
  50. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/python/mq-bridge-py/tests/test_performance_smoke.py +0 -0
  51. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/python/mq-bridge-py/tests/test_public_api.py +0 -0
  52. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/python/mq-bridge-py/uv.lock +0 -0
  53. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/scripts/analysis/HTTP_STREAM_ENDPOINT_PLAN.md +0 -0
  54. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/scripts/analysis/benches/metadata_bench.rs +0 -0
  55. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/scripts/analysis/debug_ibm_mq_tls.rs +0 -0
  56. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/scripts/analysis/http/analyze_xctrace_time_profile.py +0 -0
  57. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/scripts/analysis/http/http_flamegraph.py +0 -0
  58. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/scripts/analysis/http/mq_bridge_http_profile.rs +0 -0
  59. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/scripts/httparena/frameworks/mq-bridge-py/meta.json +0 -0
  60. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/scripts/httparena/frameworks/mq-bridge-py/server.py +0 -0
  61. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/scripts/techempower/Python/mq-bridge-py/README.md +0 -0
  62. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/scripts/techempower/Python/mq-bridge-py/benchmark_config.json +0 -0
  63. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/scripts/techempower/Python/mq-bridge-py/server.py +0 -0
  64. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/scripts/techempower/README.md +0 -0
  65. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/scripts/techempower/postgres.yml +0 -0
  66. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/scripts/techempower/seed.sql +0 -0
  67. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/scripts/techempower/verify.sh +0 -0
  68. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/scripts/the-benchmarker/python/mq-bridge-py/pyproject.toml +0 -0
  69. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/scripts/the-benchmarker/python/mq-bridge-py/server.py +0 -0
  70. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/src/canonical_message.rs +0 -0
  71. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/src/command_handler.rs +0 -0
  72. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/src/endpoints/aws.rs +0 -0
  73. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/src/endpoints/fanout.rs +0 -0
  74. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/src/endpoints/file.rs +0 -0
  75. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/src/endpoints/grpc.proto +0 -0
  76. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/src/endpoints/grpc.rs +0 -0
  77. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/src/endpoints/http_stream.rs +0 -0
  78. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/src/endpoints/ibm_mq.rs +0 -0
  79. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/src/endpoints/kafka.rs +0 -0
  80. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/src/endpoints/memory/endpoint.rs +0 -0
  81. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/src/endpoints/memory/ipc_unix.rs +0 -0
  82. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/src/endpoints/memory/ipc_windows.rs +0 -0
  83. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/src/endpoints/memory/memory_transport.rs +0 -0
  84. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/src/endpoints/memory/mod.rs +0 -0
  85. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/src/endpoints/memory/transport.rs +0 -0
  86. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/src/endpoints/mod.rs +0 -0
  87. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/src/endpoints/mongodb.rs +0 -0
  88. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/src/endpoints/mqtt.rs +0 -0
  89. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/src/endpoints/nats.rs +0 -0
  90. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/src/endpoints/null.rs +0 -0
  91. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/src/endpoints/reader.rs +0 -0
  92. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/src/endpoints/response.rs +0 -0
  93. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/src/endpoints/sled.rs +0 -0
  94. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/src/endpoints/sqlx.rs +0 -0
  95. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/src/endpoints/static_endpoint.rs +0 -0
  96. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/src/endpoints/stream_buffer.rs +0 -0
  97. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/src/endpoints/switch.rs +0 -0
  98. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/src/endpoints/websocket.rs +0 -0
  99. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/src/endpoints/zeromq.rs +0 -0
  100. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/src/errors.rs +0 -0
  101. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/src/event_handler.rs +0 -0
  102. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/src/event_store.rs +0 -0
  103. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/src/extensions.rs +0 -0
  104. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/src/lib.rs +0 -0
  105. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/src/middleware/buffer.rs +0 -0
  106. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/src/middleware/cookie_jar.rs +0 -0
  107. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/src/middleware/deduplication.rs +0 -0
  108. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/src/middleware/delay.rs +0 -0
  109. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/src/middleware/dlq.rs +0 -0
  110. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/src/middleware/limiter.rs +0 -0
  111. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/src/middleware/metrics.rs +0 -0
  112. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/src/middleware/mod.rs +0 -0
  113. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/src/middleware/random_panic.rs +0 -0
  114. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/src/middleware/retry.rs +0 -0
  115. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/src/middleware/weak_join.rs +0 -0
  116. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/src/outcomes.rs +0 -0
  117. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/src/publisher.rs +0 -0
  118. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/src/response.rs +0 -0
  119. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/src/route.rs +0 -0
  120. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/src/test_utils.rs +0 -0
  121. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/src/traits.rs +0 -0
  122. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/src/type_handler.rs +0 -0
  123. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/README.md +0 -0
  124. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/armature_integration.rs +0 -0
  125. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/integration/amqp.rs +0 -0
  126. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/integration/aws.rs +0 -0
  127. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/integration/config.toml +0 -0
  128. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/integration/docker-compose/amqp.yml +0 -0
  129. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/integration/docker-compose/aws.yml +0 -0
  130. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/integration/docker-compose/ibm-mq-certs/client.crl +0 -0
  131. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/integration/docker-compose/ibm-mq-certs/client.kdb +0 -0
  132. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/integration/docker-compose/ibm-mq-certs/client.p12 +0 -0
  133. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/integration/docker-compose/ibm-mq-certs/client.rdb +0 -0
  134. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/integration/docker-compose/ibm-mq-certs/client.sth +0 -0
  135. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/integration/docker-compose/ibm_mq.yml +0 -0
  136. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/integration/docker-compose/ibm_mq_tls.yml +0 -0
  137. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/integration/docker-compose/kafka-tls.yml +0 -0
  138. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/integration/docker-compose/kafka.yml +0 -0
  139. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/integration/docker-compose/mariadb.yml +0 -0
  140. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/integration/docker-compose/mongodb-replica.yml +0 -0
  141. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/integration/docker-compose/mongodb-tls.yml +0 -0
  142. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/integration/docker-compose/mongodb.yml +0 -0
  143. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/integration/docker-compose/mosquitto.conf +0 -0
  144. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/integration/docker-compose/mosquitto_performance.conf +0 -0
  145. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/integration/docker-compose/mosquitto_persistence.conf +0 -0
  146. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/integration/docker-compose/mq_init.mqsc +0 -0
  147. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/integration/docker-compose/mq_init_tls.mqsc +0 -0
  148. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/integration/docker-compose/mqtt.yml +0 -0
  149. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/integration/docker-compose/mqtt_performance.yml +0 -0
  150. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/integration/docker-compose/mysql.yml +0 -0
  151. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/integration/docker-compose/nats.yml +0 -0
  152. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/integration/docker-compose/postgres.yml +0 -0
  153. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/integration/file.rs +0 -0
  154. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/integration/grpc.rs +0 -0
  155. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/integration/grpc_tls.rs +0 -0
  156. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/integration/http.rs +0 -0
  157. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/integration/http_tls.rs +0 -0
  158. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/integration/ibm_mq.rs +0 -0
  159. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/integration/ibm_mq_tls.rs +0 -0
  160. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/integration/ipc.rs +0 -0
  161. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/integration/kafka.rs +0 -0
  162. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/integration/kafka_tls.rs +0 -0
  163. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/integration/logic_test.rs +0 -0
  164. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/integration/mariadb.rs +0 -0
  165. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/integration/memory.rs +0 -0
  166. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/integration/mod.rs +0 -0
  167. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/integration/mongodb.rs +0 -0
  168. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/integration/mongodb_raw.rs +0 -0
  169. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/integration/mongodb_tls.rs +0 -0
  170. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/integration/mqtt.rs +0 -0
  171. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/integration/mysql.rs +0 -0
  172. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/integration/nats.rs +0 -0
  173. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/integration/performance_static.rs +0 -0
  174. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/integration/postgres.rs +0 -0
  175. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/integration/route.rs +0 -0
  176. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/integration/scripts/gen_certs.sh +0 -0
  177. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/integration/sqlite.rs +0 -0
  178. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/integration/tls_helpers.rs +0 -0
  179. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/integration/websocket.rs +0 -0
  180. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/integration/zeromq.rs +0 -0
  181. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/integration_test.rs +0 -0
  182. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/ref_test.rs +0 -0
  183. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/sqlite_test.rs +0 -0
  184. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/tls_example.rs +0 -0
  185. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/unit_tests.md +0 -0
  186. {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/websocket_test.rs +0 -0
@@ -3057,7 +3057,7 @@ dependencies = [
3057
3057
 
3058
3058
  [[package]]
3059
3059
  name = "mq-bridge"
3060
- version = "0.2.17"
3060
+ version = "0.2.18"
3061
3061
  dependencies = [
3062
3062
  "anyhow",
3063
3063
  "async-channel",
@@ -3123,7 +3123,7 @@ dependencies = [
3123
3123
 
3124
3124
  [[package]]
3125
3125
  name = "mq-bridge-py"
3126
- version = "0.2.17"
3126
+ version = "0.2.18"
3127
3127
  dependencies = [
3128
3128
  "anyhow",
3129
3129
  "async-trait",
@@ -16,7 +16,7 @@ members = ["python/mq-bridge-py"]
16
16
  resolver = "2"
17
17
 
18
18
  [workspace.package]
19
- version = "0.2.17"
19
+ version = "0.2.18"
20
20
 
21
21
  [workspace.dependencies]
22
22
  anyhow = "1.0"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mq-bridge-py-basic
3
- Version: 0.2.17
3
+ Version: 0.2.18
4
4
  Classifier: Development Status :: 3 - Alpha
5
5
  Classifier: License :: OSI Approved :: MIT License
6
6
  Classifier: Programming Language :: Python :: 3
@@ -38,12 +38,13 @@ profiles still run.
38
38
 
39
39
  ## How it works
40
40
 
41
- - **One AutoBuilder, every HTTP flavour.** mq-bridge's HTTP server uses
41
+ - **Auto for core, explicit H2 for h2c.** mq-bridge's default HTTP server uses
42
42
  hyper-util's `AutoBuilder`, which negotiates HTTP/1.1 **and** HTTP/2
43
43
  prior-knowledge (h2c) on the same plaintext port. The core `mq-bridge` entry
44
44
  binds **8080** (HTTP/1.1 + h2c) and **8443** (HTTP/2 over TLS) in one process,
45
- sharing the same handler; `mq-bridge-h2c` is the same dispatch on **8082** for
46
- the explicit h2c profiles.
45
+ sharing the same handler; `mq-bridge-h2c` uses the library's
46
+ `server_protocol = http2_only` setting on **8082** so HttpArena's explicit
47
+ h2c profiles cannot fall back to HTTP/1.1.
47
48
  - **HTTP/2 over TLS (`baseline-h2`, `static-h2`).** The library advertises ALPN
48
49
  `h2` on the TLS listener, so conformant clients negotiate HTTP/2; the TLS route
49
50
  reads its cert/key from `/certs/server.crt` + `/certs/server.key` (overridable
@@ -97,7 +98,7 @@ scripts/httparena/frameworks/
97
98
  ## Submitting upstream
98
99
 
99
100
  1. Pin the version: each Rust `Cargo.toml` and the Python `Dockerfile` reference
100
- this repo at tag `v0.2.16` — bump to the release you want to benchmark.
101
+ this repo at tag `v0.2.18` — bump to the release you want to benchmark.
101
102
  2. Fork `MDA2AV/HttpArena` and copy each `scripts/httparena/frameworks/<name>/`
102
103
  into the fork's `frameworks/<name>/`.
103
104
  3. On the PR, validate and benchmark per framework:
@@ -5,7 +5,7 @@
5
5
  # the compiled extension ABI matches. Pin MQB_REF to a released tag.
6
6
  FROM python:3.12-slim AS build
7
7
 
8
- ARG MQB_REF=v0.2.16
8
+ ARG MQB_REF=v0.2.18
9
9
  RUN apt-get update \
10
10
  && apt-get install -y --no-install-recommends curl build-essential git ca-certificates \
11
11
  && rm -rf /var/lib/apt/lists/*
@@ -5,7 +5,7 @@
5
5
  # matches. For the upstream PR, pin MQB_REF to a released tag.
6
6
  FROM python:3.12-slim AS build
7
7
 
8
- ARG MQB_REF=v0.2.16
8
+ ARG MQB_REF=v0.2.18
9
9
  RUN apt-get update \
10
10
  && apt-get install -y --no-install-recommends curl build-essential git ca-certificates \
11
11
  && rm -rf /var/lib/apt/lists/*
@@ -45,7 +45,7 @@ installs Rust + maturin and builds `mq_bridge_py` from this repo at the pinned
45
45
  ## Submitting upstream
46
46
 
47
47
  1. Pin the version: the Rust `Cargo.toml` and Python `config.yaml` reference this
48
- repo at tag `v0.2.16` — bump to the release you want to benchmark.
48
+ repo at tag `v0.2.18` — bump to the release you want to benchmark.
49
49
  2. Fork `the-benchmarker/web-frameworks` and copy:
50
50
  - `scripts/the-benchmarker/rust/mq-bridge/` → `rust/mq-bridge/`
51
51
  - `scripts/the-benchmarker/python/mq-bridge-py/` → `python/mq-bridge-py/`
@@ -24,7 +24,7 @@ framework:
24
24
  bootstrap:
25
25
  - curl https://sh.rustup.rs -sSf | sh -s -- -y --profile minimal
26
26
  - . "$HOME/.cargo/env" && pip install maturin
27
- - git clone --depth 1 -b v0.2.16 https://github.com/marcomq/mq-bridge /tmp/mqb
27
+ - git clone --depth 1 -b v0.2.18 https://github.com/marcomq/mq-bridge /tmp/mqb
28
28
  - . "$HOME/.cargo/env" && cd /tmp/mqb/python/mq-bridge-py && maturin build --release --no-default-features -F http -F pyo3/extension-module -o /tmp/wheels
29
29
  - pip install /tmp/wheels/*.whl
30
30
 
@@ -31,9 +31,44 @@ use uuid::Uuid;
31
31
  /// connection drops and the confirm future never resolves.
32
32
  const CONFIRM_TIMEOUT: Duration = Duration::from_secs(5);
33
33
 
34
+ /// How often the consumer wakes up while waiting for a message to re-check that
35
+ /// the broker connection is still alive. In lapin 4 a dropped connection does
36
+ /// not reliably terminate the consumer stream, so without this periodic health
37
+ /// check `Consumer::next()` can block forever after the broker goes away and the
38
+ /// route would never get the error it needs to reconnect.
39
+ const CONSUMER_HEALTH_POLL: Duration = Duration::from_secs(1);
40
+
41
+ /// Maximum time any single connection-setup step (connect, create_channel,
42
+ /// confirm_select, declares, qos, consume) may take. In lapin 4 a
43
+ /// `Connection::connect` can return `Ok` on a socket the broker immediately
44
+ /// force-closes; the io_loop then aborts recovery and any subsequent channel
45
+ /// operation on that connection never resolves. Without a bound, the
46
+ /// publisher's `reconnect` parks inside `connect` holding the state write lock
47
+ /// forever and the whole producer wedges. Bounding each step lets the attempt
48
+ /// fail and be retried against a fresh connection.
49
+ const SETUP_TIMEOUT: Duration = Duration::from_secs(5);
50
+
51
+ /// Bounds a single lapin setup operation, turning a hang or error into a
52
+ /// retryable failure. See [`SETUP_TIMEOUT`].
53
+ async fn with_setup_timeout<T>(
54
+ op: &str,
55
+ fut: impl std::future::Future<Output = Result<T, lapin::Error>>,
56
+ ) -> anyhow::Result<T> {
57
+ match tokio::time::timeout(SETUP_TIMEOUT, fut).await {
58
+ Ok(Ok(v)) => Ok(v),
59
+ Ok(Err(e)) => Err(anyhow!("AMQP {op} failed: {e}")),
60
+ Err(_) => Err(anyhow!("AMQP {op} timed out after {SETUP_TIMEOUT:?}")),
61
+ }
62
+ }
63
+
34
64
  struct AmqpState {
35
65
  connection: Connection,
36
66
  channel: Channel,
67
+ /// Bumped on every successful reconnect. Lets a worker that observed a
68
+ /// failure ask for a reconnect while letting concurrent workers that share
69
+ /// the same dead channel deduplicate, without trusting lapin's connection
70
+ /// status (which in lapin 4 can keep reporting `connected` on a dead link).
71
+ generation: u64,
37
72
  }
38
73
 
39
74
  pub struct AmqpPublisher {
@@ -84,18 +119,21 @@ impl AmqpPublisher {
84
119
  .as_deref()
85
120
  .ok_or_else(|| anyhow!("Queue name is required for AMQP publisher"))?;
86
121
  let conn = create_amqp_connection(config).await?;
87
- let channel = conn.create_channel().await?;
122
+ let channel = with_setup_timeout("create_channel", conn.create_channel()).await?;
88
123
  // Enable publisher confirms on this channel to allow waiting for acks.
89
- channel
90
- .confirm_select(lapin::options::ConfirmSelectOptions::default())
91
- .await?;
124
+ with_setup_timeout(
125
+ "confirm_select",
126
+ channel.confirm_select(lapin::options::ConfirmSelectOptions::default()),
127
+ )
128
+ .await?;
92
129
 
93
130
  if !config.no_declare_queue {
94
131
  if config.subscribe_mode {
95
132
  let exchange_name = config.exchange.as_deref().unwrap_or(queue_or_exchange);
96
133
  info!(exchange = %exchange_name, "Declaring AMQP Fanout exchange in sink");
97
- channel
98
- .exchange_declare(
134
+ with_setup_timeout(
135
+ "exchange_declare",
136
+ channel.exchange_declare(
99
137
  exchange_name.into(),
100
138
  ExchangeKind::Fanout,
101
139
  ExchangeDeclareOptions {
@@ -103,43 +141,60 @@ impl AmqpPublisher {
103
141
  ..Default::default()
104
142
  },
105
143
  FieldTable::default(),
106
- )
107
- .await?;
144
+ ),
145
+ )
146
+ .await?;
108
147
  } else {
109
148
  // Ensure the queue exists before we try to publish to it. This is idempotent.
110
149
  info!(queue = %queue_or_exchange, "Declaring AMQP queue in sink");
111
- channel
112
- .queue_declare(
150
+ with_setup_timeout(
151
+ "queue_declare",
152
+ channel.queue_declare(
113
153
  queue_or_exchange.into(),
114
154
  QueueDeclareOptions {
115
155
  durable: !config.no_persistence,
116
156
  ..Default::default()
117
157
  },
118
158
  FieldTable::default(),
119
- )
120
- .await?;
159
+ ),
160
+ )
161
+ .await?;
121
162
  }
122
163
  }
123
164
 
124
165
  Ok(AmqpState {
125
166
  connection: conn,
126
167
  channel,
168
+ generation: 0,
127
169
  })
128
170
  }
129
171
 
130
- async fn get_channel(&self) -> Channel {
131
- self.state.read().await.channel.clone()
172
+ /// Returns the current channel together with the generation it belongs to.
173
+ /// Callers pass the generation back to `reconnect` so a reconnect only fires
174
+ /// for the channel that actually failed.
175
+ async fn get_channel(&self) -> (Channel, u64) {
176
+ let state = self.state.read().await;
177
+ (state.channel.clone(), state.generation)
132
178
  }
133
179
 
134
- async fn reconnect(&self) {
180
+ /// Reconnect after a worker observed a failure on the channel of generation
181
+ /// `observed_generation`. If another worker has already reconnected since
182
+ /// (generation moved on), this is a no-op. We deliberately do NOT consult
183
+ /// `connection.status()` here: in lapin 4 a dead connection can keep
184
+ /// reporting `connected`, which previously made this early-return and left
185
+ /// the publisher wedged on a dead channel forever.
186
+ async fn reconnect(&self, observed_generation: u64) {
135
187
  let mut state = self.state.write().await;
136
- if state.connection.status().connected() && state.channel.status().connected() {
188
+ if state.generation != observed_generation {
189
+ // A concurrent worker already rebuilt the connection.
137
190
  return;
138
191
  }
139
192
  info!("Reconnecting AMQP publisher...");
140
193
  match Self::connect(&self.config).await {
141
194
  Ok(new_state) => {
195
+ let next_generation = state.generation.wrapping_add(1);
142
196
  *state = new_state;
197
+ state.generation = next_generation;
143
198
  info!("AMQP publisher reconnected.");
144
199
  }
145
200
  Err(e) => {
@@ -185,21 +240,31 @@ impl MessagePublisher for AmqpPublisher {
185
240
  properties = properties.with_headers(table);
186
241
  }
187
242
 
188
- let channel = self.get_channel().await;
189
- let confirmation_result = channel
190
- .basic_publish(
191
- self.exchange.clone().into(),
192
- self.queue.clone().into(),
193
- BasicPublishOptions::default(),
194
- &message.payload,
195
- properties,
196
- )
197
- .await;
243
+ let (channel, generation) = self.get_channel().await;
244
+ let publish_fut = channel.basic_publish(
245
+ self.exchange.clone().into(),
246
+ self.queue.clone().into(),
247
+ BasicPublishOptions::default(),
248
+ &message.payload,
249
+ properties,
250
+ );
251
+ // Bound the publish submit. In lapin 4 a publish to a silently-dropped
252
+ // connection can hang without surfacing an error, so cap the wait and
253
+ // treat it as a retryable failure that triggers a reconnect.
254
+ let confirmation_result = match tokio::time::timeout(CONFIRM_TIMEOUT, publish_fut).await {
255
+ Ok(res) => res,
256
+ Err(_) => {
257
+ self.reconnect(generation).await;
258
+ return Err(PublisherError::Retryable(anyhow!(
259
+ "Timed out submitting AMQP publish"
260
+ )));
261
+ }
262
+ };
198
263
 
199
264
  let confirmation = match confirmation_result {
200
265
  Ok(c) => c,
201
266
  Err(e) => {
202
- self.reconnect().await;
267
+ self.reconnect(generation).await;
203
268
  return Err(PublisherError::Retryable(anyhow!(
204
269
  "Failed to publish AMQP message: {}",
205
270
  e
@@ -214,14 +279,14 @@ impl MessagePublisher for AmqpPublisher {
214
279
  let confirm = match tokio::time::timeout(CONFIRM_TIMEOUT, confirmation).await {
215
280
  Ok(Ok(c)) => c,
216
281
  Ok(Err(e)) => {
217
- self.reconnect().await;
282
+ self.reconnect(generation).await;
218
283
  return Err(PublisherError::Retryable(anyhow!(
219
284
  "Failed to get AMQP publisher confirmation: {}",
220
285
  e
221
286
  )));
222
287
  }
223
288
  Err(_) => {
224
- self.reconnect().await;
289
+ self.reconnect(generation).await;
225
290
  return Err(PublisherError::Retryable(anyhow!(
226
291
  "Timed out waiting for AMQP publisher confirmation"
227
292
  )));
@@ -248,7 +313,7 @@ impl MessagePublisher for AmqpPublisher {
248
313
  .await;
249
314
  }
250
315
 
251
- let channel = self.get_channel().await;
316
+ let (channel, generation) = self.get_channel().await;
252
317
  let mut pending_messages = Vec::with_capacity(messages.len());
253
318
  let mut pending_confirms = Vec::with_capacity(messages.len());
254
319
  let mut failed_messages = Vec::new();
@@ -281,21 +346,19 @@ impl MessagePublisher for AmqpPublisher {
281
346
  properties = properties.with_headers(table);
282
347
  }
283
348
 
284
- match channel
285
- .basic_publish(
286
- self.exchange.clone().into(),
287
- self.queue.clone().into(),
288
- BasicPublishOptions::default(),
289
- &message.payload,
290
- properties,
291
- )
292
- .await
293
- {
294
- Ok(confirmation) => {
349
+ let publish_fut = channel.basic_publish(
350
+ self.exchange.clone().into(),
351
+ self.queue.clone().into(),
352
+ BasicPublishOptions::default(),
353
+ &message.payload,
354
+ properties,
355
+ );
356
+ match tokio::time::timeout(CONFIRM_TIMEOUT, publish_fut).await {
357
+ Ok(Ok(confirmation)) => {
295
358
  pending_messages.push(message);
296
359
  pending_confirms.push(confirmation);
297
360
  }
298
- Err(e) => {
361
+ Ok(Err(e)) => {
299
362
  failed_messages.push((
300
363
  message,
301
364
  PublisherError::Retryable(
@@ -303,6 +366,17 @@ impl MessagePublisher for AmqpPublisher {
303
366
  ),
304
367
  ));
305
368
  }
369
+ Err(_) => {
370
+ // In lapin 4 a publish submitted to a silently-dropped
371
+ // connection can hang without ever surfacing an error. Bound
372
+ // the submit so the batch fails fast, reconnects, and retries.
373
+ failed_messages.push((
374
+ message,
375
+ PublisherError::Retryable(anyhow!(
376
+ "Timed out submitting AMQP publish in batch"
377
+ )),
378
+ ));
379
+ }
306
380
  }
307
381
  }
308
382
 
@@ -350,7 +424,7 @@ impl MessagePublisher for AmqpPublisher {
350
424
  }
351
425
 
352
426
  if !failed_messages.is_empty() {
353
- self.reconnect().await;
427
+ self.reconnect(generation).await;
354
428
  }
355
429
 
356
430
  if failed_messages.is_empty() {
@@ -410,7 +484,7 @@ impl AmqpConsumer {
410
484
  .as_deref()
411
485
  .ok_or_else(|| anyhow!("Queue name is required for AMQP consumer"))?;
412
486
  let conn = create_amqp_connection(config).await?;
413
- let channel = conn.create_channel().await?;
487
+ let channel = with_setup_timeout("create_channel", conn.create_channel()).await?;
414
488
 
415
489
  let is_subscriber = config.subscribe_mode;
416
490
 
@@ -418,8 +492,9 @@ impl AmqpConsumer {
418
492
  // Subscriber mode: Declare Fanout exchange and temporary queue
419
493
  let exchange_name = config.exchange.as_deref().unwrap_or(queue_or_exchange);
420
494
  info!(exchange = %exchange_name, "Declaring AMQP Fanout exchange for subscriber");
421
- channel
422
- .exchange_declare(
495
+ with_setup_timeout(
496
+ "exchange_declare",
497
+ channel.exchange_declare(
423
498
  exchange_name.into(),
424
499
  ExchangeKind::Fanout,
425
500
  ExchangeDeclareOptions {
@@ -427,13 +502,15 @@ impl AmqpConsumer {
427
502
  ..Default::default()
428
503
  },
429
504
  FieldTable::default(),
430
- )
431
- .await?;
505
+ ),
506
+ )
507
+ .await?;
432
508
 
433
509
  let id = fast_uuid_v7::gen_id_string();
434
510
  let queue_name_str = format!("{}-{}-{}", APP_NAME, queue_or_exchange, id);
435
- let queue = channel
436
- .queue_declare(
511
+ let queue = with_setup_timeout(
512
+ "queue_declare",
513
+ channel.queue_declare(
437
514
  queue_name_str.clone().into(),
438
515
  QueueDeclareOptions {
439
516
  exclusive: true,
@@ -441,34 +518,39 @@ impl AmqpConsumer {
441
518
  ..Default::default()
442
519
  },
443
520
  FieldTable::default(),
444
- )
445
- .await?;
521
+ ),
522
+ )
523
+ .await?;
446
524
  let q_name = queue.name().as_str().to_string();
447
525
 
448
526
  info!(queue = %q_name, exchange = %exchange_name, "Binding temporary queue to exchange");
449
- channel
450
- .queue_bind(
527
+ with_setup_timeout(
528
+ "queue_bind",
529
+ channel.queue_bind(
451
530
  q_name.clone().into(),
452
531
  exchange_name.into(),
453
532
  "".into(),
454
533
  QueueBindOptions::default(),
455
534
  FieldTable::default(),
456
- )
457
- .await?;
535
+ ),
536
+ )
537
+ .await?;
458
538
  q_name
459
539
  } else {
460
540
  // Consumer mode: Declare durable queue
461
541
  info!(queue = %queue_or_exchange, "Declaring AMQP queue");
462
- channel
463
- .queue_declare(
542
+ with_setup_timeout(
543
+ "queue_declare",
544
+ channel.queue_declare(
464
545
  queue_or_exchange.into(),
465
546
  QueueDeclareOptions {
466
547
  durable: !config.no_persistence,
467
548
  ..Default::default()
468
549
  },
469
550
  FieldTable::default(),
470
- )
471
- .await?;
551
+ ),
552
+ )
553
+ .await?;
472
554
  queue_or_exchange.to_string()
473
555
  };
474
556
 
@@ -476,9 +558,11 @@ impl AmqpConsumer {
476
558
  // We'll get the concurrency from the route config, but for now, let's use a reasonable default
477
559
  // that can be overridden by a new method.
478
560
  let prefetch_count = config.prefetch_count.unwrap_or(100);
479
- channel
480
- .basic_qos(prefetch_count, BasicQosOptions::default())
481
- .await?;
561
+ with_setup_timeout(
562
+ "basic_qos",
563
+ channel.basic_qos(prefetch_count, BasicQosOptions::default()),
564
+ )
565
+ .await?;
482
566
 
483
567
  let consumer_tag = if is_subscriber {
484
568
  format!("{}_sub_{}", APP_NAME, fast_uuid_v7::gen_id_str())
@@ -487,14 +571,16 @@ impl AmqpConsumer {
487
571
  };
488
572
  info!(queue = %queue_name, consumer_tag = %consumer_tag, "Starting AMQP consumer");
489
573
 
490
- let consumer = channel
491
- .basic_consume(
574
+ let consumer = with_setup_timeout(
575
+ "basic_consume",
576
+ channel.basic_consume(
492
577
  queue_name.clone().into(),
493
578
  consumer_tag.into(),
494
579
  BasicConsumeOptions::default(),
495
580
  FieldTable::default(),
496
- )
497
- .await?;
581
+ ),
582
+ )
583
+ .await?;
498
584
 
499
585
  Ok(Self {
500
586
  _conn: conn,
@@ -523,28 +609,32 @@ async fn create_amqp_connection(config: &AmqpConfig) -> anyhow::Result<Connectio
523
609
  }
524
610
  let conn_uri = url.to_string();
525
611
 
526
- let mut last_error = None;
612
+ let mut last_error: Option<anyhow::Error> = None;
527
613
  for attempt in 1..=5 {
528
614
  // Avoid logging credentials embedded in URLs.
529
615
  info!(attempt = attempt, "Attempting to connect to AMQP broker");
530
616
  let conn_props = ConnectionProperties::default();
617
+ // Bound the connect itself: a hung TLS/AMQP handshake against a broker
618
+ // that is restarting must not stall the reconnect indefinitely.
531
619
  let result = if config.tls.required {
532
620
  let tls_config = build_tls_config(config).await?;
533
- DefaultConnectionBuilder::new()?
621
+ let builder = DefaultConnectionBuilder::new()?
534
622
  .with_uri_str(conn_uri.clone())
535
623
  .with_properties(conn_props)
536
- .with_tls_config(tls_config)
537
- .connect()
538
- .await
624
+ .with_tls_config(tls_config);
625
+ with_setup_timeout("connect", builder.connect()).await
539
626
  } else {
540
- Connection::connect(&conn_uri, conn_props).await
627
+ with_setup_timeout("connect", Connection::connect(&conn_uri, conn_props)).await
541
628
  };
542
629
 
543
630
  match result {
544
631
  Ok(conn) => return Ok(conn),
545
632
  Err(e) => {
546
633
  last_error = Some(e);
547
- tokio::time::sleep(Duration::from_secs(attempt * 2)).await; // Exponential backoff
634
+ // No need to back off after the final attempt.
635
+ if attempt < 5 {
636
+ tokio::time::sleep(Duration::from_secs(attempt * 2)).await; // Exponential backoff
637
+ }
548
638
  }
549
639
  }
550
640
  }
@@ -635,14 +725,31 @@ impl MessageConsumer for AmqpConsumer {
635
725
  });
636
726
  }
637
727
 
638
- // 1. Wait for the first message. This will block until a message is available.
639
- let first_delivery = match self.consumer.next().await {
640
- Some(Ok(delivery)) => delivery,
641
- Some(Err(e)) => return Err(ConsumerError::Connection(anyhow::anyhow!(e))),
642
- None => {
643
- return Err(ConsumerError::Connection(anyhow::anyhow!(
644
- "AMQP consumer stream ended unexpectedly"
645
- )))
728
+ // 1. Wait for the first message. This blocks until a message is available,
729
+ // but each wait is bounded so we can periodically verify the broker
730
+ // connection is still alive. In lapin 4 a dropped connection does not
731
+ // reliably terminate the consumer stream, so without this health check
732
+ // `next()` would block forever after the broker goes away and the route
733
+ // would never see the Connection error it needs to recreate the consumer.
734
+ let first_delivery = loop {
735
+ match tokio::time::timeout(CONSUMER_HEALTH_POLL, self.consumer.next()).await {
736
+ Ok(Some(Ok(delivery))) => break delivery,
737
+ Ok(Some(Err(e))) => return Err(ConsumerError::Connection(anyhow::anyhow!(e))),
738
+ Ok(None) => {
739
+ return Err(ConsumerError::Connection(anyhow::anyhow!(
740
+ "AMQP consumer stream ended unexpectedly"
741
+ )))
742
+ }
743
+ Err(_) => {
744
+ // No message arrived within the poll window. If the connection
745
+ // or channel has dropped, surface a Connection error so the route
746
+ // recreates the consumer; otherwise keep waiting for a message.
747
+ if !self._conn.status().connected() || !self.channel.status().connected() {
748
+ return Err(ConsumerError::Connection(anyhow::anyhow!(
749
+ "AMQP connection lost while waiting for messages"
750
+ )));
751
+ }
752
+ }
646
753
  }
647
754
  };
648
755