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.
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/Cargo.lock +2 -2
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/Cargo.toml +1 -1
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/PKG-INFO +1 -1
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/scripts/httparena/README.md +5 -4
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/scripts/httparena/frameworks/mq-bridge-py/Dockerfile +1 -1
- {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
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/scripts/the-benchmarker/README.md +1 -1
- {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
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/src/endpoints/amqp.rs +189 -82
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/src/endpoints/http.rs +324 -32
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/src/models.rs +24 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/.github/workflows/benchmark.yml +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/.github/workflows/ci.yml +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/.github/workflows/ibm-mq.yml +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/.github/workflows/publish-python.yml +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/.github/workflows/python.yml +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/.github/workflows/release.yml +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/.github/workflows/test-matrix.yml +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/.gitignore +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/ARCHITECTURE.md +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/CONFIGURATION.md +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/CONTRIBUTING.md +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/LICENSE +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/LICENSE.python +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/README.md +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/benches/performance_bench.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/build.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/claude.md +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/examples/ipc_worker_queue.yaml +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/mq-bridge.schema.json +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/mq_bridge/__init__.py +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/mq_bridge/__init__.pyi +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/mq_bridge/py.typed +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/pyproject.toml +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/python/mq-bridge-py/Cargo.toml +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/python/mq-bridge-py/LICENSE.python +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/python/mq-bridge-py/README.md +0 -0
- {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
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/python/mq-bridge-py/example.py +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/python/mq-bridge-py/examples/bench_memory.py +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/python/mq-bridge-py/examples/bench_memory.yaml +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/python/mq-bridge-py/examples/json_route.py +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/python/mq-bridge-py/examples/memory.yaml +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/python/mq-bridge-py/examples/raw_route.py +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/python/mq-bridge-py/pyproject-basic.toml +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/python/mq-bridge-py/src/lib.rs +0 -0
- {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
- {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
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/python/mq-bridge-py/uv.lock +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/scripts/analysis/HTTP_STREAM_ENDPOINT_PLAN.md +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/scripts/analysis/benches/metadata_bench.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/scripts/analysis/debug_ibm_mq_tls.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/scripts/analysis/http/analyze_xctrace_time_profile.py +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/scripts/analysis/http/http_flamegraph.py +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/scripts/analysis/http/mq_bridge_http_profile.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/scripts/httparena/frameworks/mq-bridge-py/meta.json +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/scripts/httparena/frameworks/mq-bridge-py/server.py +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/scripts/techempower/Python/mq-bridge-py/README.md +0 -0
- {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
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/scripts/techempower/Python/mq-bridge-py/server.py +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/scripts/techempower/README.md +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/scripts/techempower/postgres.yml +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/scripts/techempower/seed.sql +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/scripts/techempower/verify.sh +0 -0
- {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
- {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
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/src/canonical_message.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/src/command_handler.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/src/endpoints/aws.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/src/endpoints/fanout.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/src/endpoints/file.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/src/endpoints/grpc.proto +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/src/endpoints/grpc.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/src/endpoints/http_stream.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/src/endpoints/ibm_mq.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/src/endpoints/kafka.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/src/endpoints/memory/endpoint.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/src/endpoints/memory/ipc_unix.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/src/endpoints/memory/ipc_windows.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/src/endpoints/memory/memory_transport.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/src/endpoints/memory/mod.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/src/endpoints/memory/transport.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/src/endpoints/mod.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/src/endpoints/mongodb.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/src/endpoints/mqtt.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/src/endpoints/nats.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/src/endpoints/null.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/src/endpoints/reader.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/src/endpoints/response.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/src/endpoints/sled.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/src/endpoints/sqlx.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/src/endpoints/static_endpoint.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/src/endpoints/stream_buffer.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/src/endpoints/switch.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/src/endpoints/websocket.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/src/endpoints/zeromq.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/src/errors.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/src/event_handler.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/src/event_store.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/src/extensions.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/src/lib.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/src/middleware/buffer.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/src/middleware/cookie_jar.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/src/middleware/deduplication.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/src/middleware/delay.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/src/middleware/dlq.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/src/middleware/limiter.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/src/middleware/metrics.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/src/middleware/mod.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/src/middleware/random_panic.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/src/middleware/retry.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/src/middleware/weak_join.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/src/outcomes.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/src/publisher.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/src/response.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/src/route.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/src/test_utils.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/src/traits.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/src/type_handler.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/README.md +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/armature_integration.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/integration/amqp.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/integration/aws.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/integration/config.toml +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/integration/docker-compose/amqp.yml +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/integration/docker-compose/aws.yml +0 -0
- {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
- {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
- {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
- {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
- {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
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/integration/docker-compose/ibm_mq.yml +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/integration/docker-compose/ibm_mq_tls.yml +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/integration/docker-compose/kafka-tls.yml +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/integration/docker-compose/kafka.yml +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/integration/docker-compose/mariadb.yml +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/integration/docker-compose/mongodb-replica.yml +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/integration/docker-compose/mongodb-tls.yml +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/integration/docker-compose/mongodb.yml +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/integration/docker-compose/mosquitto.conf +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/integration/docker-compose/mosquitto_performance.conf +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/integration/docker-compose/mosquitto_persistence.conf +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/integration/docker-compose/mq_init.mqsc +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/integration/docker-compose/mq_init_tls.mqsc +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/integration/docker-compose/mqtt.yml +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/integration/docker-compose/mqtt_performance.yml +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/integration/docker-compose/mysql.yml +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/integration/docker-compose/nats.yml +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/integration/docker-compose/postgres.yml +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/integration/file.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/integration/grpc.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/integration/grpc_tls.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/integration/http.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/integration/http_tls.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/integration/ibm_mq.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/integration/ibm_mq_tls.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/integration/ipc.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/integration/kafka.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/integration/kafka_tls.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/integration/logic_test.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/integration/mariadb.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/integration/memory.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/integration/mod.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/integration/mongodb.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/integration/mongodb_raw.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/integration/mongodb_tls.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/integration/mqtt.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/integration/mysql.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/integration/nats.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/integration/performance_static.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/integration/postgres.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/integration/route.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/integration/scripts/gen_certs.sh +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/integration/sqlite.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/integration/tls_helpers.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/integration/websocket.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/integration/zeromq.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/integration_test.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/ref_test.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/sqlite_test.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/tls_example.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.18}/tests/unit_tests.md +0 -0
- {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.
|
|
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.
|
|
3126
|
+
version = "0.2.18"
|
|
3127
3127
|
dependencies = [
|
|
3128
3128
|
"anyhow",
|
|
3129
3129
|
"async-trait",
|
|
@@ -38,12 +38,13 @@ profiles still run.
|
|
|
38
38
|
|
|
39
39
|
## How it works
|
|
40
40
|
|
|
41
|
-
- **
|
|
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`
|
|
46
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
.
|
|
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
|
-
|
|
98
|
-
|
|
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
|
-
|
|
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
|
-
|
|
112
|
-
|
|
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
|
-
|
|
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
|
-
|
|
131
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
190
|
-
.
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
-
|
|
285
|
-
.
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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
|
-
|
|
422
|
-
|
|
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
|
-
|
|
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 =
|
|
436
|
-
|
|
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
|
-
|
|
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
|
-
|
|
450
|
-
|
|
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
|
-
|
|
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
|
-
|
|
463
|
-
|
|
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
|
-
|
|
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
|
-
|
|
480
|
-
|
|
481
|
-
.
|
|
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 =
|
|
491
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
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
|
|