mq-bridge-py-basic 0.2.17__tar.gz → 0.2.19__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.19}/Cargo.lock +3 -2
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/Cargo.toml +12 -3
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/PKG-INFO +1 -1
- mq_bridge_py_basic-0.2.19/benches/router_bench.rs +275 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/scripts/analysis/http/mq_bridge_http_profile.rs +24 -4
- mq_bridge_py_basic-0.2.19/scripts/analysis/http/run_http_ladder.sh +87 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/scripts/httparena/README.md +5 -4
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/scripts/httparena/frameworks/mq-bridge-py/Dockerfile +1 -1
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/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.19}/scripts/the-benchmarker/README.md +1 -1
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/scripts/the-benchmarker/python/mq-bridge-py/config.yaml +1 -1
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/src/endpoints/amqp.rs +189 -82
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/src/endpoints/http.rs +650 -84
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/src/models.rs +24 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/.github/workflows/benchmark.yml +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/.github/workflows/ci.yml +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/.github/workflows/ibm-mq.yml +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/.github/workflows/publish-python.yml +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/.github/workflows/python.yml +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/.github/workflows/release.yml +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/.github/workflows/test-matrix.yml +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/.gitignore +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/ARCHITECTURE.md +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/CONFIGURATION.md +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/CONTRIBUTING.md +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/LICENSE +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/LICENSE.python +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/README.md +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/benches/performance_bench.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/build.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/claude.md +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/examples/ipc_worker_queue.yaml +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/mq-bridge.schema.json +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/mq_bridge/__init__.py +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/mq_bridge/__init__.pyi +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/mq_bridge/py.typed +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/pyproject.toml +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/python/mq-bridge-py/Cargo.toml +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/python/mq-bridge-py/LICENSE.python +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/python/mq-bridge-py/README.md +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/python/mq-bridge-py/analysis/bench_http_native.py +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/python/mq-bridge-py/example.py +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/python/mq-bridge-py/examples/bench_memory.py +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/python/mq-bridge-py/examples/bench_memory.yaml +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/python/mq-bridge-py/examples/json_route.py +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/python/mq-bridge-py/examples/memory.yaml +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/python/mq-bridge-py/examples/raw_route.py +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/python/mq-bridge-py/pyproject-basic.toml +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/python/mq-bridge-py/src/lib.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/python/mq-bridge-py/tests/test_performance_smoke.py +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/python/mq-bridge-py/tests/test_public_api.py +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/python/mq-bridge-py/uv.lock +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/scripts/analysis/HTTP_STREAM_ENDPOINT_PLAN.md +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/scripts/analysis/benches/metadata_bench.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/scripts/analysis/debug_ibm_mq_tls.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/scripts/analysis/http/analyze_xctrace_time_profile.py +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/scripts/analysis/http/http_flamegraph.py +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/scripts/httparena/frameworks/mq-bridge-py/meta.json +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/scripts/httparena/frameworks/mq-bridge-py/server.py +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/scripts/techempower/Python/mq-bridge-py/README.md +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/scripts/techempower/Python/mq-bridge-py/benchmark_config.json +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/scripts/techempower/Python/mq-bridge-py/server.py +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/scripts/techempower/README.md +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/scripts/techempower/postgres.yml +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/scripts/techempower/seed.sql +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/scripts/techempower/verify.sh +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/scripts/the-benchmarker/python/mq-bridge-py/pyproject.toml +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/scripts/the-benchmarker/python/mq-bridge-py/server.py +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/src/canonical_message.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/src/command_handler.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/src/endpoints/aws.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/src/endpoints/fanout.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/src/endpoints/file.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/src/endpoints/grpc.proto +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/src/endpoints/grpc.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/src/endpoints/http_stream.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/src/endpoints/ibm_mq.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/src/endpoints/kafka.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/src/endpoints/memory/endpoint.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/src/endpoints/memory/ipc_unix.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/src/endpoints/memory/ipc_windows.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/src/endpoints/memory/memory_transport.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/src/endpoints/memory/mod.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/src/endpoints/memory/transport.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/src/endpoints/mod.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/src/endpoints/mongodb.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/src/endpoints/mqtt.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/src/endpoints/nats.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/src/endpoints/null.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/src/endpoints/reader.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/src/endpoints/response.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/src/endpoints/sled.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/src/endpoints/sqlx.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/src/endpoints/static_endpoint.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/src/endpoints/stream_buffer.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/src/endpoints/switch.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/src/endpoints/websocket.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/src/endpoints/zeromq.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/src/errors.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/src/event_handler.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/src/event_store.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/src/extensions.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/src/lib.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/src/middleware/buffer.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/src/middleware/cookie_jar.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/src/middleware/deduplication.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/src/middleware/delay.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/src/middleware/dlq.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/src/middleware/limiter.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/src/middleware/metrics.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/src/middleware/mod.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/src/middleware/random_panic.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/src/middleware/retry.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/src/middleware/weak_join.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/src/outcomes.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/src/publisher.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/src/response.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/src/route.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/src/test_utils.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/src/traits.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/src/type_handler.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/tests/README.md +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/tests/armature_integration.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/tests/integration/amqp.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/tests/integration/aws.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/tests/integration/config.toml +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/tests/integration/docker-compose/amqp.yml +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/tests/integration/docker-compose/aws.yml +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/tests/integration/docker-compose/ibm-mq-certs/client.crl +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/tests/integration/docker-compose/ibm-mq-certs/client.kdb +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/tests/integration/docker-compose/ibm-mq-certs/client.p12 +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/tests/integration/docker-compose/ibm-mq-certs/client.rdb +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/tests/integration/docker-compose/ibm-mq-certs/client.sth +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/tests/integration/docker-compose/ibm_mq.yml +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/tests/integration/docker-compose/ibm_mq_tls.yml +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/tests/integration/docker-compose/kafka-tls.yml +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/tests/integration/docker-compose/kafka.yml +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/tests/integration/docker-compose/mariadb.yml +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/tests/integration/docker-compose/mongodb-replica.yml +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/tests/integration/docker-compose/mongodb-tls.yml +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/tests/integration/docker-compose/mongodb.yml +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/tests/integration/docker-compose/mosquitto.conf +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/tests/integration/docker-compose/mosquitto_performance.conf +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/tests/integration/docker-compose/mosquitto_persistence.conf +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/tests/integration/docker-compose/mq_init.mqsc +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/tests/integration/docker-compose/mq_init_tls.mqsc +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/tests/integration/docker-compose/mqtt.yml +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/tests/integration/docker-compose/mqtt_performance.yml +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/tests/integration/docker-compose/mysql.yml +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/tests/integration/docker-compose/nats.yml +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/tests/integration/docker-compose/postgres.yml +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/tests/integration/file.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/tests/integration/grpc.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/tests/integration/grpc_tls.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/tests/integration/http.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/tests/integration/http_tls.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/tests/integration/ibm_mq.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/tests/integration/ibm_mq_tls.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/tests/integration/ipc.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/tests/integration/kafka.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/tests/integration/kafka_tls.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/tests/integration/logic_test.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/tests/integration/mariadb.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/tests/integration/memory.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/tests/integration/mod.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/tests/integration/mongodb.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/tests/integration/mongodb_raw.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/tests/integration/mongodb_tls.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/tests/integration/mqtt.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/tests/integration/mysql.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/tests/integration/nats.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/tests/integration/performance_static.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/tests/integration/postgres.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/tests/integration/route.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/tests/integration/scripts/gen_certs.sh +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/tests/integration/sqlite.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/tests/integration/tls_helpers.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/tests/integration/websocket.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/tests/integration/zeromq.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/tests/integration_test.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/tests/ref_test.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/tests/sqlite_test.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/tests/tls_example.rs +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/tests/unit_tests.md +0 -0
- {mq_bridge_py_basic-0.2.17 → mq_bridge_py_basic-0.2.19}/tests/websocket_test.rs +0 -0
|
@@ -3057,9 +3057,10 @@ dependencies = [
|
|
|
3057
3057
|
|
|
3058
3058
|
[[package]]
|
|
3059
3059
|
name = "mq-bridge"
|
|
3060
|
-
version = "0.2.
|
|
3060
|
+
version = "0.2.19"
|
|
3061
3061
|
dependencies = [
|
|
3062
3062
|
"anyhow",
|
|
3063
|
+
"arc-swap",
|
|
3063
3064
|
"async-channel",
|
|
3064
3065
|
"async-nats",
|
|
3065
3066
|
"async-trait",
|
|
@@ -3123,7 +3124,7 @@ dependencies = [
|
|
|
3123
3124
|
|
|
3124
3125
|
[[package]]
|
|
3125
3126
|
name = "mq-bridge-py"
|
|
3126
|
-
version = "0.2.
|
|
3127
|
+
version = "0.2.19"
|
|
3127
3128
|
dependencies = [
|
|
3128
3129
|
"anyhow",
|
|
3129
3130
|
"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.
|
|
19
|
+
version = "0.2.19"
|
|
20
20
|
|
|
21
21
|
[workspace.dependencies]
|
|
22
22
|
anyhow = "1.0"
|
|
@@ -26,7 +26,7 @@ fast-uuid-v7 = "0.1.5"
|
|
|
26
26
|
serde = "1.0"
|
|
27
27
|
serde_json = "1.0"
|
|
28
28
|
serde_yaml_ng = "0.10"
|
|
29
|
-
tokio = "1"
|
|
29
|
+
tokio = "1.34" # `Receiver::recv_many` (HTTP source batch drain) needs 1.34+.
|
|
30
30
|
tracing = "0.1"
|
|
31
31
|
|
|
32
32
|
[profile.release]
|
|
@@ -109,6 +109,7 @@ rumqttc = { version = "0.25", default-features = false, features = ["use-rustls-
|
|
|
109
109
|
mqi = { version = "0.3", optional = true }
|
|
110
110
|
|
|
111
111
|
# HTTP
|
|
112
|
+
arc-swap = { version = "1", optional = true }
|
|
112
113
|
hyper = { version = "1.8", optional = true, features = ["client", "server", "http1", "http2"] }
|
|
113
114
|
hyper-util = { version = "0.1", optional = true, features = ["tokio", "client", "client-legacy", "server", "http1", "http2"] }
|
|
114
115
|
hyper-rustls = { version = "0.27", optional = true, default-features = false, features = ["http1", "http2", "tls12", "logging", "native-tokio"] }
|
|
@@ -178,7 +179,7 @@ amqp = ["lapin", "rustls", "rustls-pemfile", "url"]
|
|
|
178
179
|
nats = ["async-nats", "rustls", "rustls-pemfile"]
|
|
179
180
|
mongodb = ["dep:mongodb"]
|
|
180
181
|
mqtt = ["rumqttc", "rustls", "tokio-rustls", "rustls-pemfile", "url"]
|
|
181
|
-
http = ["hyper", "hyper-util", "hyper-rustls", "http-body-util", "dep:http-body", "rustls", "tokio-rustls", "rustls-pemfile", "webpki-roots", "h2", "dep:flate2", "dep:base64"]
|
|
182
|
+
http = ["dep:arc-swap", "hyper", "hyper-util", "hyper-rustls", "http-body-util", "dep:http-body", "rustls", "tokio-rustls", "rustls-pemfile", "webpki-roots", "h2", "dep:flate2", "dep:base64"]
|
|
182
183
|
websocket = ["dep:tokio-tungstenite"]
|
|
183
184
|
aws = ["aws-config", "aws-sdk-sqs", "aws-sdk-sns", "dep:base64"]
|
|
184
185
|
ibm-mq = ["mqi"]
|
|
@@ -194,6 +195,8 @@ tempfile = "3.10"
|
|
|
194
195
|
chrono = "0.4"
|
|
195
196
|
metrics-util = "0.20"
|
|
196
197
|
criterion = { version = "0.8", features = ["async", "async_tokio"] }
|
|
198
|
+
# Used by the router_bench microbench to model the proposed lock-free route table.
|
|
199
|
+
arc-swap = "1"
|
|
197
200
|
mq-bridge = { path = ".", features = ["test-utils", "rustls-ring"] }
|
|
198
201
|
reqwest = { version = "0.13", features = ["stream"] }
|
|
199
202
|
|
|
@@ -207,6 +210,12 @@ name = "performance_bench"
|
|
|
207
210
|
harness = false
|
|
208
211
|
required-features = ["test-utils"]
|
|
209
212
|
|
|
213
|
+
# Isolates the per-request HTTP route lookup (Mutex<HashMap> vs ArcSwap).
|
|
214
|
+
# No external services or features required.
|
|
215
|
+
[[bench]]
|
|
216
|
+
name = "router_bench"
|
|
217
|
+
harness = false
|
|
218
|
+
|
|
210
219
|
[[test]]
|
|
211
220
|
name = "integration_test"
|
|
212
221
|
required-features = ["test-utils"]
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
//! Microbenchmark isolating HTTP route lookup — bottleneck #1 from the
|
|
2
|
+
//! throughput analysis.
|
|
3
|
+
//!
|
|
4
|
+
//! `SharedHttpRouter::match_route` (src/endpoints/http.rs) takes a blocking
|
|
5
|
+
//! `std::sync::Mutex` over a `HashMap` and linearly scans it on EVERY request.
|
|
6
|
+
//! This bench reproduces that exact access pattern in isolation and compares it
|
|
7
|
+
//! with two lock-free alternatives, so we can see — before touching production
|
|
8
|
+
//! code — whether the per-request router lock is what caps multicore scaling:
|
|
9
|
+
//!
|
|
10
|
+
//! * `mutex_hashmap` — current design: `Mutex<HashMap>` + scan + `Arc` clone
|
|
11
|
+
//! * `arcswap_vec` — proposed: `ArcSwap<Vec<..>>` snapshot + scan + `Arc` clone
|
|
12
|
+
//! * `arcswap_single` — proposed fast path for the common single-route server
|
|
13
|
+
//!
|
|
14
|
+
//! Each is measured single-threaded (latency) and under N-thread contention
|
|
15
|
+
//! (aggregate throughput). The hypothesis is confirmed if the mutex variant
|
|
16
|
+
//! collapses as threads are added while the ArcSwap variants stay flat.
|
|
17
|
+
//!
|
|
18
|
+
//! Run with: `cargo bench --bench router_bench`
|
|
19
|
+
|
|
20
|
+
use arc_swap::{ArcSwap, ArcSwapOption};
|
|
21
|
+
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
|
|
22
|
+
use std::collections::HashMap;
|
|
23
|
+
use std::hint::black_box;
|
|
24
|
+
use std::sync::{Arc, Barrier, Mutex};
|
|
25
|
+
use std::thread;
|
|
26
|
+
use std::time::{Duration, Instant};
|
|
27
|
+
|
|
28
|
+
/// Stand-in for `HttpConsumerState`: the two fields `match_route` actually reads,
|
|
29
|
+
/// plus padding so the `Arc` payload is a realistic size (the real struct holds a
|
|
30
|
+
/// channel sender, a semaphore, several strings, etc.). The padding keeps the
|
|
31
|
+
/// comparison honest — a trivially tiny `Arc` would understate clone/cache costs.
|
|
32
|
+
struct RouteState {
|
|
33
|
+
path: Option<String>,
|
|
34
|
+
method: Option<&'static str>,
|
|
35
|
+
id: u64,
|
|
36
|
+
_padding: [u64; 8],
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
impl RouteState {
|
|
40
|
+
fn new(id: u64, path: Option<&str>, method: Option<&'static str>) -> Arc<Self> {
|
|
41
|
+
Arc::new(Self {
|
|
42
|
+
path: path.map(String::from),
|
|
43
|
+
method,
|
|
44
|
+
id,
|
|
45
|
+
_padding: [0; 8],
|
|
46
|
+
})
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
fn route_matches_path(state: &RouteState, path: &str) -> bool {
|
|
51
|
+
match &state.path {
|
|
52
|
+
Some(route_path) => route_path == path,
|
|
53
|
+
None => true,
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
fn route_matches_method(state: &RouteState, method: &str) -> bool {
|
|
58
|
+
match state.method {
|
|
59
|
+
Some(route_method) => route_method == method,
|
|
60
|
+
None => true,
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
fn route_specificity(state: &RouteState) -> (u8, u8) {
|
|
65
|
+
(
|
|
66
|
+
u8::from(state.path.is_some()),
|
|
67
|
+
u8::from(state.method.is_some()),
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/// Faithful copy of `match_route`'s best-specificity scan.
|
|
72
|
+
fn scan<'a, I>(routes: I, path: &str, method: &str) -> Option<Arc<RouteState>>
|
|
73
|
+
where
|
|
74
|
+
I: Iterator<Item = &'a Arc<RouteState>>,
|
|
75
|
+
{
|
|
76
|
+
let mut best: Option<Arc<RouteState>> = None;
|
|
77
|
+
let mut best_specificity = (0u8, 0u8);
|
|
78
|
+
for state in routes {
|
|
79
|
+
if !route_matches_path(state, path) {
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if route_matches_method(state, method) {
|
|
83
|
+
let specificity = route_specificity(state);
|
|
84
|
+
if best.is_none() || specificity > best_specificity {
|
|
85
|
+
best_specificity = specificity;
|
|
86
|
+
best = Some(Arc::clone(state));
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
best
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/// Current production design: blocking lock + `HashMap` scan per lookup.
|
|
94
|
+
struct MutexRouter {
|
|
95
|
+
routes: Mutex<HashMap<u64, Arc<RouteState>>>,
|
|
96
|
+
}
|
|
97
|
+
impl MutexRouter {
|
|
98
|
+
fn lookup(&self, path: &str, method: &str) -> Option<Arc<RouteState>> {
|
|
99
|
+
let routes = self.routes.lock().unwrap();
|
|
100
|
+
scan(routes.values(), path, method)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/// Proposed design: lock-free snapshot + scan (handles multi-route servers).
|
|
105
|
+
struct ArcSwapRouter {
|
|
106
|
+
routes: ArcSwap<Vec<Arc<RouteState>>>,
|
|
107
|
+
}
|
|
108
|
+
impl ArcSwapRouter {
|
|
109
|
+
fn lookup(&self, path: &str, method: &str) -> Option<Arc<RouteState>> {
|
|
110
|
+
let routes = self.routes.load();
|
|
111
|
+
scan(routes.iter(), path, method)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/// Proposed fast path for the overwhelmingly common single-route server: a
|
|
116
|
+
/// lock-free pointer load with no scan at all.
|
|
117
|
+
struct ArcSwapSingleRouter {
|
|
118
|
+
only: ArcSwapOption<RouteState>,
|
|
119
|
+
}
|
|
120
|
+
impl ArcSwapSingleRouter {
|
|
121
|
+
fn lookup(&self, path: &str, method: &str) -> Option<Arc<RouteState>> {
|
|
122
|
+
let only = self.only.load_full()?;
|
|
123
|
+
if route_matches_path(&only, path) && route_matches_method(&only, method) {
|
|
124
|
+
Some(only)
|
|
125
|
+
} else {
|
|
126
|
+
None
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
fn build_routes(n: usize) -> Vec<Arc<RouteState>> {
|
|
132
|
+
(0..n)
|
|
133
|
+
.map(|i| RouteState::new(i as u64, Some(&format!("/r{i}")), Some("POST")))
|
|
134
|
+
.collect()
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/// Run `work` on `threads` OS threads, each performing `iters` lookups, started
|
|
138
|
+
/// together via a barrier. Returns wall-clock elapsed for the whole batch.
|
|
139
|
+
///
|
|
140
|
+
/// criterion divides this by `iters`, so with `Throughput::Elements(threads)`
|
|
141
|
+
/// the reported rate is aggregate lookups/sec across all threads — exactly the
|
|
142
|
+
/// number that should plateau if a shared lock is the bottleneck.
|
|
143
|
+
fn contended(threads: usize, iters: u64, work: Arc<dyn Fn() + Send + Sync>) -> Duration {
|
|
144
|
+
let barrier = Arc::new(Barrier::new(threads + 1));
|
|
145
|
+
let mut handles = Vec::with_capacity(threads);
|
|
146
|
+
for _ in 0..threads {
|
|
147
|
+
let work = Arc::clone(&work);
|
|
148
|
+
let barrier = Arc::clone(&barrier);
|
|
149
|
+
handles.push(thread::spawn(move || {
|
|
150
|
+
barrier.wait();
|
|
151
|
+
for _ in 0..iters {
|
|
152
|
+
work();
|
|
153
|
+
}
|
|
154
|
+
}));
|
|
155
|
+
}
|
|
156
|
+
let start = Instant::now();
|
|
157
|
+
barrier.wait();
|
|
158
|
+
for handle in handles {
|
|
159
|
+
handle.join().unwrap();
|
|
160
|
+
}
|
|
161
|
+
start.elapsed()
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
fn bench_router(c: &mut Criterion) {
|
|
165
|
+
// A handful of routes so the scan has real work; lookups target the last one
|
|
166
|
+
// to force a full scan (worst case for the linear-scan designs).
|
|
167
|
+
let n_routes = 4usize;
|
|
168
|
+
let target_path = format!("/r{}", n_routes - 1);
|
|
169
|
+
let single_path = "/only";
|
|
170
|
+
|
|
171
|
+
let mutex = Arc::new(MutexRouter {
|
|
172
|
+
routes: Mutex::new(
|
|
173
|
+
build_routes(n_routes)
|
|
174
|
+
.into_iter()
|
|
175
|
+
.map(|state| (state.id, state))
|
|
176
|
+
.collect(),
|
|
177
|
+
),
|
|
178
|
+
});
|
|
179
|
+
let arcswap = Arc::new(ArcSwapRouter {
|
|
180
|
+
routes: ArcSwap::from_pointee(build_routes(n_routes)),
|
|
181
|
+
});
|
|
182
|
+
let single = Arc::new(ArcSwapSingleRouter {
|
|
183
|
+
only: ArcSwapOption::new(Some(RouteState::new(0, Some(single_path), Some("POST")))),
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// ---- single-threaded latency ----
|
|
187
|
+
{
|
|
188
|
+
let mut group = c.benchmark_group("router_lookup/single_thread");
|
|
189
|
+
group.throughput(Throughput::Elements(1));
|
|
190
|
+
group.bench_function("mutex_hashmap", |b| {
|
|
191
|
+
b.iter(|| black_box(mutex.lookup(black_box(&target_path), black_box("POST"))))
|
|
192
|
+
});
|
|
193
|
+
group.bench_function("arcswap_vec", |b| {
|
|
194
|
+
b.iter(|| black_box(arcswap.lookup(black_box(&target_path), black_box("POST"))))
|
|
195
|
+
});
|
|
196
|
+
group.bench_function("arcswap_single", |b| {
|
|
197
|
+
b.iter(|| black_box(single.lookup(black_box(single_path), black_box("POST"))))
|
|
198
|
+
});
|
|
199
|
+
group.finish();
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// ---- contended aggregate throughput ----
|
|
203
|
+
let max_threads = thread::available_parallelism()
|
|
204
|
+
.map(|n| n.get())
|
|
205
|
+
.unwrap_or(4);
|
|
206
|
+
let thread_counts: Vec<usize> = {
|
|
207
|
+
let mut counts = vec![1usize, max_threads / 2, max_threads];
|
|
208
|
+
counts.retain(|&t| t >= 1);
|
|
209
|
+
counts.sort_unstable();
|
|
210
|
+
counts.dedup();
|
|
211
|
+
counts
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
let mut group = c.benchmark_group("router_lookup/contended");
|
|
215
|
+
for &threads in &thread_counts {
|
|
216
|
+
group.throughput(Throughput::Elements(threads as u64));
|
|
217
|
+
|
|
218
|
+
group.bench_with_input(
|
|
219
|
+
BenchmarkId::new("mutex_hashmap", threads),
|
|
220
|
+
&threads,
|
|
221
|
+
|b, &t| {
|
|
222
|
+
b.iter_custom(|iters| {
|
|
223
|
+
let router = Arc::clone(&mutex);
|
|
224
|
+
let path = target_path.clone();
|
|
225
|
+
contended(
|
|
226
|
+
t,
|
|
227
|
+
iters,
|
|
228
|
+
Arc::new(move || {
|
|
229
|
+
black_box(router.lookup(&path, "POST"));
|
|
230
|
+
}),
|
|
231
|
+
)
|
|
232
|
+
})
|
|
233
|
+
},
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
group.bench_with_input(
|
|
237
|
+
BenchmarkId::new("arcswap_vec", threads),
|
|
238
|
+
&threads,
|
|
239
|
+
|b, &t| {
|
|
240
|
+
b.iter_custom(|iters| {
|
|
241
|
+
let router = Arc::clone(&arcswap);
|
|
242
|
+
let path = target_path.clone();
|
|
243
|
+
contended(
|
|
244
|
+
t,
|
|
245
|
+
iters,
|
|
246
|
+
Arc::new(move || {
|
|
247
|
+
black_box(router.lookup(&path, "POST"));
|
|
248
|
+
}),
|
|
249
|
+
)
|
|
250
|
+
})
|
|
251
|
+
},
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
group.bench_with_input(
|
|
255
|
+
BenchmarkId::new("arcswap_single", threads),
|
|
256
|
+
&threads,
|
|
257
|
+
|b, &t| {
|
|
258
|
+
b.iter_custom(|iters| {
|
|
259
|
+
let router = Arc::clone(&single);
|
|
260
|
+
contended(
|
|
261
|
+
t,
|
|
262
|
+
iters,
|
|
263
|
+
Arc::new(move || {
|
|
264
|
+
black_box(router.lookup(single_path, "POST"));
|
|
265
|
+
}),
|
|
266
|
+
)
|
|
267
|
+
})
|
|
268
|
+
},
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
group.finish();
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
criterion_group!(benches, bench_router);
|
|
275
|
+
criterion_main!(benches);
|
|
@@ -136,6 +136,7 @@ enum ProfileMode {
|
|
|
136
136
|
Immediate,
|
|
137
137
|
Metadata,
|
|
138
138
|
ChannelAck,
|
|
139
|
+
WorkerLocalAck,
|
|
139
140
|
DirectConsumer,
|
|
140
141
|
DirectConsumerAck,
|
|
141
142
|
DirectConsumerFireForget,
|
|
@@ -242,6 +243,7 @@ fn parse_mode(value: &str) -> ProfileMode {
|
|
|
242
243
|
"immediate" => ProfileMode::Immediate,
|
|
243
244
|
"metadata" => ProfileMode::Metadata,
|
|
244
245
|
"channel-ack" | "channel_ack" => ProfileMode::ChannelAck,
|
|
246
|
+
"worker-local-ack" | "worker_local_ack" => ProfileMode::WorkerLocalAck,
|
|
245
247
|
"direct-consumer" | "direct_consumer" => ProfileMode::DirectConsumer,
|
|
246
248
|
"direct-consumer-ack" | "direct_consumer_ack" => ProfileMode::DirectConsumerAck,
|
|
247
249
|
"direct-consumer-fire-forget" | "direct_consumer_fire_forget" => {
|
|
@@ -283,7 +285,7 @@ fn print_help() {
|
|
|
283
285
|
eprintln!(
|
|
284
286
|
"Usage: mq_bridge_http_profile [--mode MODE] [--port PORT] [--duration-s SECONDS] [--path PATH]\n\
|
|
285
287
|
Options:\n\
|
|
286
|
-
--mode MODE route | route-fire-forget | route-handler | immediate | metadata | channel-ack | direct-consumer | direct-consumer-ack | direct-consumer-fire-forget | inline-response | inline-body-only (default route)\n\
|
|
288
|
+
--mode MODE route | route-fire-forget | route-handler | immediate | metadata | channel-ack | worker-local-ack | direct-consumer | direct-consumer-ack | direct-consumer-fire-forget | inline-response | inline-body-only (default route)\n\
|
|
287
289
|
--client-url URL run Rust load client instead of server\n\
|
|
288
290
|
--clients N Rust load client concurrency (default 8)\n\
|
|
289
291
|
--header-count N Add N synthetic request headers in Rust load client (default 0)\n\
|
|
@@ -335,6 +337,7 @@ async fn run_standalone_server(args: ProfileArgs) -> anyhow::Result<()> {
|
|
|
335
337
|
let mode = args.mode;
|
|
336
338
|
let path = Arc::new(args.path.clone());
|
|
337
339
|
let duration_s = args.duration_s;
|
|
340
|
+
let buffer_size = args.internal_buffer_size;
|
|
338
341
|
|
|
339
342
|
let shutdown = tokio::time::sleep(std::time::Duration::from_secs(duration_s));
|
|
340
343
|
tokio::pin!(shutdown);
|
|
@@ -350,7 +353,21 @@ async fn run_standalone_server(args: ProfileArgs) -> anyhow::Result<()> {
|
|
|
350
353
|
accepted = listener.accept() => {
|
|
351
354
|
let (stream, _) = accepted?;
|
|
352
355
|
let io = TokioIo::new(stream);
|
|
353
|
-
|
|
356
|
+
// WorkerLocalAck gives every connection its own channel + drain task, so
|
|
357
|
+
// there is no shared single-receiver contention (variant B in the ladder).
|
|
358
|
+
// ChannelAck reuses the one process-wide channel (variant C). The
|
|
359
|
+
// per-request work is otherwise identical, isolating the global funnel.
|
|
360
|
+
let tx = if mode == ProfileMode::WorkerLocalAck {
|
|
361
|
+
let (tx, mut rx) = tokio::sync::mpsc::channel::<AckMessage>(buffer_size);
|
|
362
|
+
tokio::spawn(async move {
|
|
363
|
+
while let Some((_payload, _metadata, ack)) = rx.recv().await {
|
|
364
|
+
let _ = ack.send(());
|
|
365
|
+
}
|
|
366
|
+
});
|
|
367
|
+
Some(tx)
|
|
368
|
+
} else {
|
|
369
|
+
channel_tx.clone()
|
|
370
|
+
};
|
|
354
371
|
let path = path.clone();
|
|
355
372
|
tokio::spawn(async move {
|
|
356
373
|
let service = service_fn(move |req| {
|
|
@@ -479,7 +496,10 @@ async fn profile_request(
|
|
|
479
496
|
.unwrap());
|
|
480
497
|
}
|
|
481
498
|
|
|
482
|
-
let metadata = if matches!(
|
|
499
|
+
let metadata = if matches!(
|
|
500
|
+
mode,
|
|
501
|
+
ProfileMode::Metadata | ProfileMode::ChannelAck | ProfileMode::WorkerLocalAck
|
|
502
|
+
) {
|
|
483
503
|
build_metadata(&req)
|
|
484
504
|
} else {
|
|
485
505
|
HashMap::new()
|
|
@@ -495,7 +515,7 @@ async fn profile_request(
|
|
|
495
515
|
}
|
|
496
516
|
};
|
|
497
517
|
|
|
498
|
-
if mode
|
|
518
|
+
if matches!(mode, ProfileMode::ChannelAck | ProfileMode::WorkerLocalAck) {
|
|
499
519
|
let Some(tx) = tx else {
|
|
500
520
|
return Ok(Response::builder()
|
|
501
521
|
.status(500)
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# HTTP throughput ladder — runs the four benchmark targets from the throughput
|
|
4
|
+
# analysis back-to-back with the same in-tree Rust load client, so each rung adds
|
|
5
|
+
# exactly one layer and the RPS deltas localize the bottleneck.
|
|
6
|
+
#
|
|
7
|
+
# A raw-hyper (immediate) : Hyper + body read + fixed "ok". Ceiling.
|
|
8
|
+
# B worker-local-chan (worker-local-ack) : + per-connection mpsc + oneshot ack (no shared receiver)
|
|
9
|
+
# C global-channel (channel-ack) : + ONE process-wide mpsc + single drain task
|
|
10
|
+
# Dr router+consumer (direct-consumer-ack): real HttpConsumer — router Mutex + per-route mpsc
|
|
11
|
+
# D full-route (route-handler) : full mq-bridge route executor
|
|
12
|
+
#
|
|
13
|
+
# Read the deltas:
|
|
14
|
+
# A -> B : cost of the per-request channel+ack machinery itself (should be small)
|
|
15
|
+
# B -> C : cost of funneling every connection through ONE shared receiver (bottleneck #2)
|
|
16
|
+
# C -> Dr: cost of the router Mutex + real consumer path (bottleneck #1)
|
|
17
|
+
# Dr -> D: cost of the route executor / worker pool / commit sequencer
|
|
18
|
+
#
|
|
19
|
+
# Caveat: A/B/C use a single-acceptor standalone server; Dr/D use the production
|
|
20
|
+
# SO_REUSEPORT multi-worker accept. Compare within {A,B,C} and within {Dr,D}; the
|
|
21
|
+
# router lock itself is isolated cleanly (no accept-model confound) by
|
|
22
|
+
# `cargo bench --bench router_bench`.
|
|
23
|
+
#
|
|
24
|
+
# Usage: scripts/analysis/http/run_http_ladder.sh
|
|
25
|
+
# Env: WORKERS, CLIENTS, DURATION (s), PORT, FEATURES
|
|
26
|
+
set -euo pipefail
|
|
27
|
+
|
|
28
|
+
cd "$(dirname "$0")/../../.."
|
|
29
|
+
|
|
30
|
+
FEATURES="${FEATURES:-http,rustls-ring}"
|
|
31
|
+
if command -v nproc >/dev/null 2>&1; then
|
|
32
|
+
CORES="$(nproc)"
|
|
33
|
+
else
|
|
34
|
+
CORES="$(sysctl -n hw.ncpu 2>/dev/null || echo 4)"
|
|
35
|
+
fi
|
|
36
|
+
WORKERS="${WORKERS:-$CORES}"
|
|
37
|
+
CLIENTS="${CLIENTS:-$CORES}"
|
|
38
|
+
DURATION="${DURATION:-15}"
|
|
39
|
+
PORT="${PORT:-18080}"
|
|
40
|
+
REQ_PATH="/bench"
|
|
41
|
+
BIN=target/release/mq_bridge_http_profile
|
|
42
|
+
|
|
43
|
+
echo ">> building release binary (features: $FEATURES)"
|
|
44
|
+
cargo build --release --bin mq_bridge_http_profile --features "$FEATURES" >/dev/null
|
|
45
|
+
|
|
46
|
+
run_rung() {
|
|
47
|
+
local label="$1" mode="$2" expect="$3"
|
|
48
|
+
local log; log="$(mktemp)"
|
|
49
|
+
|
|
50
|
+
"$BIN" --mode "$mode" --port "$PORT" --path "$REQ_PATH" \
|
|
51
|
+
--workers "$WORKERS" --duration-s "$((DURATION + 8))" >"$log" 2>&1 &
|
|
52
|
+
local server_pid=$!
|
|
53
|
+
|
|
54
|
+
# Wait for the server to announce READY (up to ~10s).
|
|
55
|
+
local ready=0
|
|
56
|
+
for _ in $(seq 1 100); do
|
|
57
|
+
if grep -q READY "$log"; then ready=1; break; fi
|
|
58
|
+
if ! kill -0 "$server_pid" 2>/dev/null; then break; fi
|
|
59
|
+
sleep 0.1
|
|
60
|
+
done
|
|
61
|
+
if [ "$ready" -ne 1 ]; then
|
|
62
|
+
echo " [$label] server failed to start:"; sed 's/^/ /' "$log"
|
|
63
|
+
kill "$server_pid" 2>/dev/null || true; wait "$server_pid" 2>/dev/null || true
|
|
64
|
+
rm -f "$log"; return 1
|
|
65
|
+
fi
|
|
66
|
+
|
|
67
|
+
local out
|
|
68
|
+
out="$("$BIN" --client-url "http://127.0.0.1:${PORT}${REQ_PATH}" \
|
|
69
|
+
--clients "$CLIENTS" --duration-s "$DURATION" --expected-body "$expect" 2>&1 || true)"
|
|
70
|
+
local rps; rps="$(printf '%s' "$out" | sed -n 's/.*(\([0-9]*\) req\/s).*/\1/p')"
|
|
71
|
+
printf ' %-22s %12s req/s\n' "$label" "${rps:-FAILED}"
|
|
72
|
+
if [ -z "$rps" ]; then printf '%s\n' "$out" | sed 's/^/ /'; fi
|
|
73
|
+
|
|
74
|
+
kill "$server_pid" 2>/dev/null || true
|
|
75
|
+
wait "$server_pid" 2>/dev/null || true
|
|
76
|
+
rm -f "$log"
|
|
77
|
+
|
|
78
|
+
if [ -z "$rps" ]; then return 1; fi
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
echo ">> HTTP throughput ladder (workers=$WORKERS clients=$CLIENTS duration=${DURATION}s)"
|
|
82
|
+
run_rung "A raw-hyper" immediate ok
|
|
83
|
+
run_rung "B worker-local-chan" worker-local-ack ok
|
|
84
|
+
run_rung "C global-channel" channel-ack ok
|
|
85
|
+
run_rung "Dr router+consumer" direct-consumer-ack message-processed
|
|
86
|
+
run_rung "D full-route" route-handler payload
|
|
87
|
+
echo ">> done"
|
|
@@ -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
|
|