mq-bridge-py-basic 0.3.9__tar.gz → 0.3.10__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.3.10/.github/dependabot.yml +11 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/.github/workflows/ci.yml +27 -0
- mq_bridge_py_basic-0.3.10/CHANGELOG.md +76 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/Cargo.lock +4 -4
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/Cargo.toml +15 -6
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/PKG-INFO +3 -3
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/README.md +71 -2
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/REFERENCE.md +61 -8
- mq_bridge_py_basic-0.3.10/SECURITY.md +104 -0
- mq_bridge_py_basic-0.3.10/deny.toml +97 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/mq-bridge.schema.json +50 -3
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/mq_bridge/__init__.pyi +8 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/mq_bridge/config.pyi +9 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/python/mq-bridge-py/README.md +2 -2
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/python/mq-bridge-py/examples/dlt_source.py +4 -8
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/python/mq-bridge-py/src/lib.rs +6 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/python/mq-bridge-py/tests/test_public_api.py +10 -7
- mq_bridge_py_basic-0.3.10/src/canonical_message.rs +854 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/src/command_handler.rs +9 -1
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/src/endpoints/amqp.rs +20 -5
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/src/endpoints/file/mod.rs +493 -146
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/src/endpoints/file/tests.rs +268 -3
- mq_bridge_py_basic-0.3.10/src/endpoints/kafka.rs +1927 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/src/endpoints/mod.rs +229 -27
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/src/endpoints/mongodb/readers.rs +81 -3
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/src/endpoints/mongodb/tests.rs +67 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/src/endpoints/nats.rs +44 -17
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/src/endpoints/object_store.rs +233 -23
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/src/endpoints/postgres/mod.rs +56 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/src/endpoints/sqlx/mod.rs +252 -116
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/src/endpoints/sqlx/tests.rs +262 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/src/errors.rs +66 -6
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/src/middleware/cookie_jar.rs +73 -5
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/src/middleware/deduplication.rs +518 -145
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/src/middleware/limiter.rs +31 -1
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/src/middleware/mod.rs +43 -3
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/src/middleware/transform/compiled.rs +133 -15
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/src/middleware/transform/mod.rs +7 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/src/middleware/transform/path.rs +26 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/src/middleware/transform/schema.rs +15 -1
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/src/middleware/transform/tests.rs +333 -7
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/src/models.rs +46 -2
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/src/route.rs +729 -5
- mq_bridge_py_basic-0.3.10/src/support/base64_engine.rs +101 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/src/support/interpolation.rs +260 -17
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/src/support/mod.rs +2 -0
- mq_bridge_py_basic-0.3.10/src/support/source_ranges.rs +448 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/src/test_utils.rs +30 -1
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/tests/armature_integration.rs +16 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/tests/integration/mqtt.rs +9 -3
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/tests/integration/postgres_cdc.rs +1 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/tests/integration/route.rs +25 -23
- mq_bridge_py_basic-0.3.9/src/canonical_message.rs +0 -471
- mq_bridge_py_basic-0.3.9/src/endpoints/kafka.rs +0 -986
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/.github/workflows/benchmark.yml +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/.github/workflows/ibm-mq.yml +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/.github/workflows/node.yml +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/.github/workflows/publish-node.yml +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/.github/workflows/publish-python.yml +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/.github/workflows/python.yml +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/.github/workflows/release.yml +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/.github/workflows/test-matrix.yml +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/.gitignore +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/ARCHITECTURE.md +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/CLAUDE.md +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/CONFIGURATION.md +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/CONTRIBUTING.md +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/LICENSE +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/LICENSE.python +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/benches/ETL_BENCHMARKS.md +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/benches/performance_bench.rs +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/benches/router_bench.rs +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/benches/zeromq_backends.rs +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/bindings-common/Cargo.toml +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/bindings-common/src/lib.rs +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/bindings-common/src/logging.rs +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/build.rs +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/examples/ipc_worker_queue.yaml +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/mq_bridge/__init__.py +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/mq_bridge/config.py +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/mq_bridge/py.typed +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/package-lock.json +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/pyproject.toml +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/python/mq-bridge-py/Cargo.toml +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/python/mq-bridge-py/LICENSE.python +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/python/mq-bridge-py/analysis/bench_http_native.py +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/python/mq-bridge-py/example.py +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/python/mq-bridge-py/examples/bench_memory.py +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/python/mq-bridge-py/examples/bench_memory.yaml +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/python/mq-bridge-py/examples/consume_pull.py +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/python/mq-bridge-py/examples/json_route.py +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/python/mq-bridge-py/examples/memory.yaml +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/python/mq-bridge-py/examples/raw_route.py +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/python/mq-bridge-py/pyproject-basic.toml +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/python/mq-bridge-py/pyproject-reduced.toml +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/python/mq-bridge-py/scripts/gen_config_types.py +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/python/mq-bridge-py/tests/test_config_types.py +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/python/mq-bridge-py/tests/test_logging.py +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/python/mq-bridge-py/tests/test_packaging.py +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/python/mq-bridge-py/tests/test_performance_smoke.py +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/python/mq-bridge-py/tests/test_routes.py +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/python/mq-bridge-py/uv.lock +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/scripts/analysis/benches/metadata_bench.rs +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/scripts/analysis/debug_ibm_mq_tls.rs +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/scripts/analysis/http/analyze_xctrace_time_profile.py +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/scripts/analysis/http/http_flamegraph.py +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/scripts/analysis/http/mq_bridge_http_profile.rs +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/scripts/analysis/http/run_http_ladder.sh +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/scripts/ci/format_bench_output.sh +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/scripts/httparena/README.md +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/scripts/httparena/frameworks/mq-bridge-py/Dockerfile +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/scripts/httparena/frameworks/mq-bridge-py/meta.json +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/scripts/httparena/frameworks/mq-bridge-py/server.py +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/scripts/techempower/Python/mq-bridge-py/README.md +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/scripts/techempower/Python/mq-bridge-py/benchmark_config.json +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/scripts/techempower/Python/mq-bridge-py/mq-bridge-py.dockerfile +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/scripts/techempower/Python/mq-bridge-py/server.py +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/scripts/techempower/README.md +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/scripts/techempower/postgres.yml +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/scripts/techempower/seed.sql +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/scripts/techempower/verify.sh +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/scripts/the-benchmarker/README.md +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/scripts/the-benchmarker/python/mq-bridge-py/config.yaml +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/scripts/the-benchmarker/python/mq-bridge-py/pyproject.toml +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/scripts/the-benchmarker/python/mq-bridge-py/server.py +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/src/checkpoint.rs +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/src/endpoints/aws.rs +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/src/endpoints/clickhouse.rs +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/src/endpoints/grpc.proto +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/src/endpoints/grpc.rs +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/src/endpoints/http/mod.rs +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/src/endpoints/http/publisher.rs +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/src/endpoints/http/stream.rs +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/src/endpoints/http/tests.rs +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/src/endpoints/ibm_mq.rs +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/src/endpoints/memory/endpoint.rs +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/src/endpoints/memory/framed.rs +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/src/endpoints/memory/ipc_unix.rs +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/src/endpoints/memory/ipc_windows.rs +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/src/endpoints/memory/memory_transport.rs +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/src/endpoints/memory/mod.rs +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/src/endpoints/memory/transport.rs +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/src/endpoints/mongodb/consumer.rs +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/src/endpoints/mongodb/dedup.rs +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/src/endpoints/mongodb/mod.rs +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/src/endpoints/mongodb/publisher.rs +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/src/endpoints/mqtt.rs +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/src/endpoints/poll.rs +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/src/endpoints/postgres/VENDORED.md +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/src/endpoints/postgres/pgoutput/decoder.rs +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/src/endpoints/postgres/pgoutput/messages.rs +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/src/endpoints/postgres/pgoutput/mod.rs +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/src/endpoints/postgres/pgoutput/registry.rs +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/src/endpoints/postgres/pgoutput/values.rs +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/src/endpoints/postgres/replication.rs +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/src/endpoints/postgres/state.rs +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/src/endpoints/redis_streams.rs +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/src/endpoints/sled.rs +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/src/endpoints/sqlx/dedup.rs +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/src/endpoints/structural/fanout.rs +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/src/endpoints/structural/mod.rs +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/src/endpoints/structural/null.rs +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/src/endpoints/structural/reader.rs +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/src/endpoints/structural/request.rs +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/src/endpoints/structural/response.rs +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/src/endpoints/structural/static_endpoint.rs +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/src/endpoints/structural/stream_buffer.rs +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/src/endpoints/structural/switch.rs +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/src/endpoints/websocket.rs +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/src/endpoints/zeromq/codec.rs +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/src/endpoints/zeromq/mod.rs +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/src/endpoints/zeromq/omq.rs +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/src/endpoints/zeromq/zmq.rs +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/src/event_handler.rs +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/src/event_store.rs +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/src/extensions.rs +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/src/lib.rs +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/src/middleware/buffer.rs +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/src/middleware/compression.rs +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/src/middleware/delay.rs +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/src/middleware/dlq.rs +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/src/middleware/encryption.rs +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/src/middleware/metrics.rs +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/src/middleware/random_panic.rs +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/src/middleware/retry.rs +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/src/middleware/transform/coerce.rs +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/src/middleware/transform/error.rs +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/src/middleware/weak_join.rs +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/src/outcomes.rs +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/src/publisher.rs +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/src/response.rs +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/src/support/compression.rs +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/src/support/connection_registry.rs +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/src/support/crypto.rs +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/src/support/crypto_envelope.rs +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/src/traits.rs +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/src/type_handler.rs +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/tests/README.md +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/tests/integration/amqp.rs +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/tests/integration/aws.rs +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/tests/integration/clickhouse.rs +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/tests/integration/config.toml +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/tests/integration/docker-compose/amqp.yml +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/tests/integration/docker-compose/aws.yml +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/tests/integration/docker-compose/clickhouse.yml +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/tests/integration/docker-compose/ibm-mq-certs/client.crl +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/tests/integration/docker-compose/ibm-mq-certs/client.kdb +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/tests/integration/docker-compose/ibm-mq-certs/client.p12 +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/tests/integration/docker-compose/ibm-mq-certs/client.rdb +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/tests/integration/docker-compose/ibm-mq-certs/client.sth +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/tests/integration/docker-compose/ibm_mq.yml +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/tests/integration/docker-compose/ibm_mq_tls.yml +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/tests/integration/docker-compose/kafka-tls.yml +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/tests/integration/docker-compose/kafka.yml +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/tests/integration/docker-compose/mariadb.yml +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/tests/integration/docker-compose/mongodb-replica.yml +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/tests/integration/docker-compose/mongodb-tls.yml +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/tests/integration/docker-compose/mongodb.yml +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/tests/integration/docker-compose/mosquitto.conf +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/tests/integration/docker-compose/mosquitto_performance.conf +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/tests/integration/docker-compose/mosquitto_persistence.conf +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/tests/integration/docker-compose/mq_init.mqsc +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/tests/integration/docker-compose/mq_init_tls.mqsc +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/tests/integration/docker-compose/mqtt.yml +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/tests/integration/docker-compose/mqtt_performance.yml +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/tests/integration/docker-compose/mysql.yml +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/tests/integration/docker-compose/nats.yml +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/tests/integration/docker-compose/object_store.yml +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/tests/integration/docker-compose/postgres.yml +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/tests/integration/docker-compose/postgres_cdc.yml +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/tests/integration/docker-compose/redis.yml +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/tests/integration/file.rs +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/tests/integration/grpc.rs +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/tests/integration/grpc_tls.rs +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/tests/integration/http.rs +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/tests/integration/http_tls.rs +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/tests/integration/ibm_mq.rs +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/tests/integration/ibm_mq_tls.rs +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/tests/integration/ipc.rs +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/tests/integration/kafka.rs +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/tests/integration/kafka_tls.rs +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/tests/integration/logic_test.rs +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/tests/integration/mariadb.rs +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/tests/integration/memory.rs +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/tests/integration/mod.rs +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/tests/integration/mongodb.rs +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/tests/integration/mongodb_raw.rs +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/tests/integration/mongodb_tls.rs +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/tests/integration/mysql.rs +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/tests/integration/nats.rs +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/tests/integration/object_store.rs +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/tests/integration/performance_static.rs +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/tests/integration/postgres.rs +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/tests/integration/redis_streams.rs +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/tests/integration/scripts/gen_certs.sh +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/tests/integration/sqlite.rs +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/tests/integration/tls_helpers.rs +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/tests/integration/websocket.rs +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/tests/integration/zeromq.rs +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/tests/integration_test.rs +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/tests/memory_leak_test.rs +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/tests/ref_test.rs +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/tests/reference_docs_test.rs +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/tests/sqlite_test.rs +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/tests/tls_example.rs +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/tests/unit_tests.md +0 -0
- {mq_bridge_py_basic-0.3.9 → mq_bridge_py_basic-0.3.10}/tests/websocket_test.rs +0 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# To get started with Dependabot version updates, you'll need to specify which
|
|
2
|
+
# package ecosystems to update and where the package manifests are located.
|
|
3
|
+
# Please see the documentation for all configuration options:
|
|
4
|
+
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
|
|
5
|
+
|
|
6
|
+
version: 2
|
|
7
|
+
updates:
|
|
8
|
+
- package-ecosystem: "cargo" # See documentation for possible values
|
|
9
|
+
directory: "/" # Location of package manifests
|
|
10
|
+
schedule:
|
|
11
|
+
interval: "weekly"
|
|
@@ -305,3 +305,30 @@ jobs:
|
|
|
305
305
|
|
|
306
306
|
- name: Build documentation (deny warnings)
|
|
307
307
|
run: RUSTDOCFLAGS="-D warnings" cargo doc --all-features --no-deps
|
|
308
|
+
|
|
309
|
+
# Supply chain: licenses, banned crates, source registries, RustSec advisories
|
|
310
|
+
supply-chain:
|
|
311
|
+
name: Supply Chain
|
|
312
|
+
runs-on: ubuntu-latest
|
|
313
|
+
permissions:
|
|
314
|
+
contents: read
|
|
315
|
+
steps:
|
|
316
|
+
- uses: actions/checkout@v4
|
|
317
|
+
with:
|
|
318
|
+
persist-credentials: false
|
|
319
|
+
|
|
320
|
+
# Hard gate: an unapproved license, a banned crate or a non-crates.io
|
|
321
|
+
# source is always a real, actionable problem in this repo.
|
|
322
|
+
- name: Licenses / bans / sources
|
|
323
|
+
uses: EmbarkStudios/cargo-deny-action@v2
|
|
324
|
+
with:
|
|
325
|
+
command: check bans licenses sources
|
|
326
|
+
|
|
327
|
+
# Informational: every current advisory is in a transitive dependency we
|
|
328
|
+
# cannot upgrade ourselves, and RustSec updates daily. SECURITY.md records
|
|
329
|
+
# the per-feature reachability analysis; deny.toml carries the reasoning.
|
|
330
|
+
- name: Advisories (informational)
|
|
331
|
+
continue-on-error: true
|
|
332
|
+
uses: EmbarkStudios/cargo-deny-action@v2
|
|
333
|
+
with:
|
|
334
|
+
command: check advisories
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `mq-bridge`. Newest first.
|
|
4
|
+
|
|
5
|
+
## 0.3.10
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Changed
|
|
9
|
+
|
|
10
|
+
- DLQ and output middleware now run around the publish step, not inside the handler, so a
|
|
11
|
+
handler failure is no longer retried by the output chain. The tradeoff: a handler failure
|
|
12
|
+
now skips the output middleware entirely, so `dlq` on the output endpoint cannot capture
|
|
13
|
+
it — only publish failures reach the DLQ. Put a `dlq` on the input endpoint to catch
|
|
14
|
+
handler failures.
|
|
15
|
+
- Errors carry their full cause chain instead of only the outermost context.
|
|
16
|
+
- `deduplication` on an **output** endpoint is now a startup error instead of a warning and a
|
|
17
|
+
silent no-op. Move it to the route's input endpoint.
|
|
18
|
+
- `limiter` paces a single `send_batch` by its own message count, so one large batch is no
|
|
19
|
+
longer a free burst. Sustained throughput is unchanged.
|
|
20
|
+
- A `message_id` that is neither a UUID nor a `u128` is hashed to a stable id rather than
|
|
21
|
+
making the whole JSON envelope unparseable.
|
|
22
|
+
|
|
23
|
+
### Performance
|
|
24
|
+
|
|
25
|
+
- **Kafka consumer**: a long-lived prefetch task reads librdkafka continuously into a bounded
|
|
26
|
+
channel, instead of rebuilding the stream inside every `receive_batch`. librdkafka only keeps
|
|
27
|
+
requesting records while its queue is drained, so every pause the pipeline took — a transform,
|
|
28
|
+
a slow sink — was also a pause in fetching, and the fetch rate collapsed to well under what the
|
|
29
|
+
broker could serve. Batch offsets are also recorded once per partition rather than once per
|
|
30
|
+
message, which was O(n²) in the batch and allocated two `CString`s per record.
|
|
31
|
+
Kafka → transform → file: **192,854 → 824,983 rows/s** on the 1M-row, four-partition
|
|
32
|
+
benchmark, from 0.35x to 1.51x Arroyo on identical output. A 16,384-message passthrough
|
|
33
|
+
batch went from 11.5s to 2.2s, and from 10.4s to 0.9s of CPU.
|
|
34
|
+
- **Postgres / sqlx**: `test_before_acquire(false)` on the pool, zero-copy row encoding via
|
|
35
|
+
a prebuilt `JsonRowSchema`, and prebuilt first/next page queries on the cursor path.
|
|
36
|
+
- **Command handler**: `send_batch` no longer loops per-message `send()` (≈8x on batched routes).
|
|
37
|
+
- **File**: single-pass byte-array decode, faster CSV writes, compression sniffing.
|
|
38
|
+
- **Deduplication**: the two-phase reserve/commit no longer writes to the store twice per
|
|
39
|
+
message. Reservations are held in memory — sled takes an exclusive file lock on its directory,
|
|
40
|
+
so a claim only ever has to be visible to this process — leaving one write per message, on
|
|
41
|
+
commit.
|
|
42
|
+
|
|
43
|
+
### Fixed
|
|
44
|
+
|
|
45
|
+
- `insert_query` batch inserts dropped anything but a bare token from the `VALUES` tuple, so
|
|
46
|
+
`decode(${payload:x}, 'base64')` and casts were silently discarded — a binary (`bytea`)
|
|
47
|
+
column could not be written. The batch path now keeps the user's SQL and falls back to
|
|
48
|
+
iterative inserts when the tuple contains an expression.
|
|
49
|
+
- A payload string containing an embedded NUL byte was dropped by the driver and stored as
|
|
50
|
+
SQL `NULL` while the route reported success. It is now rejected as non-retryable.
|
|
51
|
+
- Database errors were rendered twice, because `sqlx` errors already display their own
|
|
52
|
+
source. Cause chains now skip a link an earlier one already contains.
|
|
53
|
+
- `cookie_jar`'s `inject_metadata` resolved only a bare stored name, so the namespaced
|
|
54
|
+
`cookie.<name>` / `value.<name>` spelling that `export_metadata_prefix` reports back
|
|
55
|
+
injected nothing. Both spellings now work.
|
|
56
|
+
- MQTT publishes are confirmed by PUBACK/PUBCOMP and guarded against session resets, which
|
|
57
|
+
took chaos-test message loss to zero.
|
|
58
|
+
- Postgres CDC advances its replication slot durably on shutdown instead of leaving the
|
|
59
|
+
feedback unflushed.
|
|
60
|
+
- `deduplication` could silently drop a message after a crash. The reservation was written to
|
|
61
|
+
the store *before* the message was processed, so a redelivery arriving within the 5s pending
|
|
62
|
+
TTL was classified as a duplicate and acked — without ever having been written to the sink.
|
|
63
|
+
Reservations are now in-memory and die with the process, so the redelivery is reprocessed.
|
|
64
|
+
- A Kafka record with no value — a tombstone, which is ordinary traffic on a compacted topic —
|
|
65
|
+
failed the whole batch. Its offset was never committed, so the route reconnected onto the same
|
|
66
|
+
record forever: one tombstone wedged the consumer permanently, and a compacted topic could not
|
|
67
|
+
be consumed at all. Tombstones now arrive as an empty payload flagged `mqb.kafka.tombstone`.
|
|
68
|
+
- `--drain` on a Kafka source could report success having copied only part of a topic, or none
|
|
69
|
+
of it. An idle wait was taken to mean "source exhausted", but an idle channel means nothing on
|
|
70
|
+
its own: before the first fetch lands, or in an ordinary gap between fetches, it looks exactly
|
|
71
|
+
the same as the end of the data. The shorter the idle timeout the more went missing — at 1ms a
|
|
72
|
+
1,000,000-row topic landed 0 rows, and the copy still exited successfully. A drain now
|
|
73
|
+
completes only once every assigned partition has reached the offset it held when the drain
|
|
74
|
+
began, and lands all 1,000,000 rows at every idle timeout including 0.
|
|
75
|
+
- Drain no longer hangs on an empty source, and reconnect attempts are bounded.
|
|
76
|
+
- CSV reader handles quoted newlines; file endpoints fail fast on an unopenable path.
|
|
@@ -3348,7 +3348,7 @@ dependencies = [
|
|
|
3348
3348
|
|
|
3349
3349
|
[[package]]
|
|
3350
3350
|
name = "mq-bridge"
|
|
3351
|
-
version = "0.3.
|
|
3351
|
+
version = "0.3.10"
|
|
3352
3352
|
dependencies = [
|
|
3353
3353
|
"aes-gcm",
|
|
3354
3354
|
"anyhow",
|
|
@@ -3429,7 +3429,7 @@ dependencies = [
|
|
|
3429
3429
|
|
|
3430
3430
|
[[package]]
|
|
3431
3431
|
name = "mq-bridge-bindings-common"
|
|
3432
|
-
version = "0.3.
|
|
3432
|
+
version = "0.3.10"
|
|
3433
3433
|
dependencies = [
|
|
3434
3434
|
"anyhow",
|
|
3435
3435
|
"fast-uuid-v7",
|
|
@@ -3443,7 +3443,7 @@ dependencies = [
|
|
|
3443
3443
|
|
|
3444
3444
|
[[package]]
|
|
3445
3445
|
name = "mq-bridge-node"
|
|
3446
|
-
version = "0.3.
|
|
3446
|
+
version = "0.3.10"
|
|
3447
3447
|
dependencies = [
|
|
3448
3448
|
"anyhow",
|
|
3449
3449
|
"async-trait",
|
|
@@ -3465,7 +3465,7 @@ dependencies = [
|
|
|
3465
3465
|
|
|
3466
3466
|
[[package]]
|
|
3467
3467
|
name = "mq-bridge-py"
|
|
3468
|
-
version = "0.3.
|
|
3468
|
+
version = "0.3.10"
|
|
3469
3469
|
dependencies = [
|
|
3470
3470
|
"anyhow",
|
|
3471
3471
|
"async-trait",
|
|
@@ -16,7 +16,7 @@ members = ["bindings-common", "python/mq-bridge-py"]
|
|
|
16
16
|
resolver = "2"
|
|
17
17
|
|
|
18
18
|
[workspace.package]
|
|
19
|
-
version = "0.3.
|
|
19
|
+
version = "0.3.10"
|
|
20
20
|
|
|
21
21
|
[workspace.dependencies]
|
|
22
22
|
anyhow = "1.0"
|
|
@@ -29,6 +29,12 @@ serde_yaml_ng = "0.10"
|
|
|
29
29
|
tokio = "1.34" # `Receiver::recv_many` (HTTP source batch drain) needs 1.34+.
|
|
30
30
|
tracing = "0.1"
|
|
31
31
|
|
|
32
|
+
[profile.dev.package."*"]
|
|
33
|
+
debug = false
|
|
34
|
+
|
|
35
|
+
[profile.dev.build-override]
|
|
36
|
+
debug = false
|
|
37
|
+
|
|
32
38
|
[profile.release]
|
|
33
39
|
lto = "thin"
|
|
34
40
|
strip = "symbols"
|
|
@@ -138,7 +144,8 @@ flate2 = { version = "1.1", optional = true }
|
|
|
138
144
|
lz4_flex = { version = "0.14", optional = true, default-features = false, features = ["frame"] }
|
|
139
145
|
# zstd (bundled libzstd via zstd-sys); Decoder handles concatenated frames like gzip/lz4.
|
|
140
146
|
zstd = { version = "0.13", optional = true }
|
|
141
|
-
|
|
147
|
+
# Non-optional: `CanonicalMessage`'s JSON form encodes binary payloads as base64.
|
|
148
|
+
base64 = { version = "0.23" }
|
|
142
149
|
|
|
143
150
|
# AEAD ciphers for the `encryption` feature (payload middleware + at-rest file/object).
|
|
144
151
|
chacha20poly1305 = { version = "0.11", optional = true }
|
|
@@ -229,7 +236,7 @@ dedup = ["sled"]
|
|
|
229
236
|
compression = ["dep:flate2", "dep:lz4_flex", "dep:zstd"]
|
|
230
237
|
# AEAD payload encryption: the `encryption` middleware and the at-rest
|
|
231
238
|
# `encryption` field on the file/object_store endpoints.
|
|
232
|
-
encryption = ["dep:chacha20poly1305", "dep:aes-gcm"
|
|
239
|
+
encryption = ["dep:chacha20poly1305", "dep:aes-gcm"]
|
|
233
240
|
metrics = ["dep:metrics"]
|
|
234
241
|
test-utils = ["dep:tracing-appender", "dep:tracing-subscriber", "dep:serde_yaml_ng"]
|
|
235
242
|
# Opt-in produce/consume-only isolation benchmarks (throwaway diagnostics, off by default)
|
|
@@ -254,9 +261,9 @@ amqp = ["lapin", "rustls", "rustls-pemfile", "url"]
|
|
|
254
261
|
nats = ["async-nats", "rustls", "rustls-pemfile"]
|
|
255
262
|
mongodb = ["dep:mongodb", "url"]
|
|
256
263
|
mqtt = ["rumqttc", "rustls", "tokio-rustls", "rustls-pemfile", "url"]
|
|
257
|
-
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:lz4_flex", "dep:zstd"
|
|
264
|
+
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:lz4_flex", "dep:zstd"]
|
|
258
265
|
websocket = ["dep:tokio-websockets"]
|
|
259
|
-
aws = ["aws-config", "aws-sdk-sqs", "aws-sdk-sns"
|
|
266
|
+
aws = ["aws-config", "aws-sdk-sqs", "aws-sdk-sns"]
|
|
260
267
|
# Runtime-loaded IBM MQ client via dlopen: builds with no SDK present; the IBM
|
|
261
268
|
# redistributable client is only needed at runtime, and only if an ibm-mq
|
|
262
269
|
# endpoint is actually used.
|
|
@@ -283,10 +290,12 @@ object-store = ["dep:object_store", "url"]
|
|
|
283
290
|
# Postgres logical-replication CDC (pgoutput). Reuses the sqlx dependency for the
|
|
284
291
|
# slot-lifecycle control plane; the streaming wire protocol is pgwire-replication.
|
|
285
292
|
# Needs Rust 1.88 (pgwire-replication MSRV), like the amqp feature gate.
|
|
286
|
-
postgres-cdc = ["dep:pgwire-replication", "dep:byteorder", "sqlx", "
|
|
293
|
+
postgres-cdc = ["dep:pgwire-replication", "dep:byteorder", "sqlx", "url", "dep:percent-encoding"]
|
|
287
294
|
|
|
288
295
|
[dev-dependencies]
|
|
289
296
|
config = { version = "0.15" }
|
|
297
|
+
# `start_paused` — lets a test cross a 30s health window without waiting 30s.
|
|
298
|
+
tokio = { workspace = true, features = ["test-util"] }
|
|
290
299
|
serde_yaml_ng = "0.10"
|
|
291
300
|
tempfile = "3.27"
|
|
292
301
|
chrono = "0.4"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mq-bridge-py-basic
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.10
|
|
4
4
|
Classifier: Development Status :: 3 - Alpha
|
|
5
5
|
Classifier: License :: OSI Approved :: MIT License
|
|
6
6
|
Classifier: Programming Language :: Python :: 3
|
|
@@ -232,8 +232,8 @@ that ack each batch individually (NATS JetStream, AMQP, MQTT) accept any order.
|
|
|
232
232
|
> per source position (Kafka `partition:offset`, NATS `stream_sequence`, AMQP
|
|
233
233
|
> delivery tag) and makes a natural primary key. Source cursor fields are also
|
|
234
234
|
> available in `message.metadata` (`mqb.src.kafka_topic`/`mqb.src.kafka_offset`, `mqb.src.nats_subject`/`mqb.src.nats_stream_sequence`,
|
|
235
|
-
> `mqb.src.amqp_routing_key`/`mqb.src.amqp_delivery_tag`) when
|
|
236
|
-
>
|
|
235
|
+
> `mqb.src.amqp_routing_key`/`mqb.src.amqp_delivery_tag`) when its Kafka, NATS, or
|
|
236
|
+
> AMQP source config sets `source_metadata: true` (off by default).
|
|
237
237
|
>
|
|
238
238
|
> **Ack deadlines vs slow loads.** JetStream `AckWait` (default 30s), AMQP
|
|
239
239
|
> prefetch/consumer-timeout and MQTT inflight windows each bound how long a batch
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|

|
|
7
7
|

|
|
8
8
|

|
|
9
|
+
[](deny.toml)
|
|
10
|
+
[](SECURITY.md)
|
|
9
11
|
[](LICENSE)
|
|
10
12
|
|
|
11
13
|
|
|
@@ -21,7 +23,7 @@ It is not only a forwarder. A route can transform, filter, fan out, retry, rate-
|
|
|
21
23
|
|
|
22
24
|
## Move data from A to B — inside your own service
|
|
23
25
|
|
|
24
|
-
If you need to move data reliably between systems and you write code (Rust, Python, or Node), `mq-bridge` is a strong default. It is a **library you embed**, not a daemon or control plane you operate.
|
|
26
|
+
If you need to move data or events reliably between systems and you write code (Rust, Python, or Node), `mq-bridge` is a strong default. It is a **library you embed**, not a daemon or control plane you operate.
|
|
25
27
|
|
|
26
28
|
**Prefer not to write code?** [`mq-bridge-app`](https://github.com/marcomq/mq-bridge-app) runs the exact same engine as a **standalone, zero-code ETL service** configured entirely by **YAML or environment variables** — move data from A to B without writing a line. It ships a **Postman-style UI** to build, send, and inspect messages against a route, and can **import Postman collections and AsyncAPI documents** to scaffold routes and endpoints for you.
|
|
27
29
|
|
|
@@ -35,6 +37,8 @@ If you need to move data reliably between systems and you write code (Rust, Pyth
|
|
|
35
37
|
|
|
36
38
|
> **Throughput & footprint.** In our own benchmarks, the same engine — driven the zero-code way through [`mq-bridge-app`](https://github.com/marcomq/mq-bridge-app) — on a CSV→JSONL file conversion (1,000,000 mixed-type rows, ~116 MiB) sustained **1,133,786 rows/s** at ~22 MiB, about **~58x faster** and **~20x leaner in memory** than Meltano (`tap-csv` → `target-jsonl`, ~19,500 rows/s / ~444 MiB). Full setup, methodology, and the exact parameters are in [`benches/ETL_BENCHMARKS.md`](benches/ETL_BENCHMARKS.md).
|
|
37
39
|
|
|
40
|
+
> **Kafka → file.** In a 1,000,000-row relay with the default file format and no transform, the engine was up to **80% faster than Sea Streamer**. The [`mq-bridge-app` benchmark](https://github.com/marcomq/mq-bridge-app/tree/dev/benches/etl) contains the reproducible helper and native file-format caveats.
|
|
41
|
+
|
|
38
42
|
|
|
39
43
|
## Language Bindings
|
|
40
44
|
|
|
@@ -364,11 +368,56 @@ orders_upsert_branch:
|
|
|
364
368
|
duplicate to detect. Left unwrapped by `request`, the tagged message is returned as the route's
|
|
365
369
|
response as usual.
|
|
366
370
|
|
|
371
|
+
**Files & object storage — `idempotency`.** A filesystem has no unique constraint, so the `file` and
|
|
372
|
+
`object_store` sinks get replay safety a different way: **deterministic names plus covered-range
|
|
373
|
+
recovery**. Set `idempotency: true` and the sink stops writing UUID-named objects. Instead it groups
|
|
374
|
+
each batch into runs of consecutive source positions and writes one immutable part per run, named
|
|
375
|
+
for the range it covers:
|
|
376
|
+
|
|
377
|
+
```yaml
|
|
378
|
+
kafka_to_s3:
|
|
379
|
+
input:
|
|
380
|
+
kafka: { topic: "orders", url: "localhost:9092", source_metadata: true }
|
|
381
|
+
output:
|
|
382
|
+
object_store:
|
|
383
|
+
url: "s3://my-bucket/orders"
|
|
384
|
+
idempotency: true # → s3://my-bucket/orders/part-orders-0-0-1023.jsonl
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
On startup the sink lists what is already there, parses the ranges out of the part names, and drops
|
|
388
|
+
any incoming record whose position falls inside one. Filtering is **per record, not per batch**, so
|
|
389
|
+
batch boundaries are free to differ across restarts — that is what makes it work without a
|
|
390
|
+
checkpoint protocol. Local files stage to a temp name, `fsync`, then `rename` (atomic within a
|
|
391
|
+
filesystem); object stores have no atomic rename, so a single PUT under the final name *is* the
|
|
392
|
+
commit.
|
|
393
|
+
|
|
394
|
+
This needs a replayable source position, which today means **Kafka** (topic/partition/offset) or
|
|
395
|
+
**`postgres_cdc`** (commit LSN plus in-transaction ordinal). A route with an idempotent output turns
|
|
396
|
+
`source_metadata` on for its input automatically; set it explicitly only when you want the
|
|
397
|
+
`mqb.src.*` keys for something else. Any other input is rejected when the route starts. NATS, AMQP
|
|
398
|
+
and MongoDB CDC also accept `source_metadata` and emit provenance keys, but a subject or routing key
|
|
399
|
+
is not a replayable offset, so they cannot drive an idempotent sink.
|
|
400
|
+
|
|
401
|
+
For the `file` sink, `idempotency: true` changes what `path` means: it is the directory that receives
|
|
402
|
+
the part files, not the file that is appended to. The sink creates it on startup, so pointing it at an
|
|
403
|
+
existing regular file fails there with "Failed to create idempotent file sink directory".
|
|
404
|
+
|
|
405
|
+
`compression` and `encryption` work as usual.
|
|
406
|
+
|
|
407
|
+
Current limits, all of which the sink rejects or logs rather than silently mishandling:
|
|
408
|
+
|
|
409
|
+
* No `csv` (each part would need its own header row).
|
|
410
|
+
* `date_partition` is ignored — parts are written flat under the prefix, since the name already
|
|
411
|
+
carries the range. Logged at startup.
|
|
412
|
+
* **One part file per contiguous run per batch.** There is no size-based rolling yet, so a large
|
|
413
|
+
backfill produces many small files (~1000 per 1M rows at `batch_size: 1024`). For `postgres_cdc`
|
|
414
|
+
this is per-transaction, so a high-commit-rate stream produces one small file per commit. Rolling
|
|
415
|
+
would require buffering across batches the route has already acked, which is not safe today.
|
|
367
416
|
|
|
368
417
|
### Cloud Object Storage (S3 / GCS / Azure)
|
|
369
418
|
The `object_store` endpoint (alias `s3`) reads and writes cloud object stores — Amazon S3, Google Cloud Storage, Azure Blob, Cloudflare R2, and anything else the [`object_store`](https://crates.io/crates/object_store) crate speaks — behind the same `receive_batch` / `send_batch` API. Enable it with the `object-store` feature. Credentials and backend options are read from the process environment (`AWS_ACCESS_KEY_ID`, `AWS_ENDPOINT`, `AWS_REGION`, `GOOGLE_SERVICE_ACCOUNT`, `AZURE_STORAGE_ACCOUNT`, ...); the URL scheme picks the backend (`s3://`, `gs://`, `az://`).
|
|
370
419
|
|
|
371
|
-
* **As a sink**, each flushed batch is encoded with the same file endpoint formats (`normal` JSONL, `json`, `text`, `raw`) and written as **one immutable object** at `<prefix>/[YYYY/MM/DD/]<uuidv7>.<ext>`. Objects are write-once — nothing is appended or mutated. The uuidv7 name already sorts by write time; the optional `date_partition` prefix (on by default, derived from that same id's timestamp) is a readability / lifecycle-rule convenience.
|
|
420
|
+
* **As a sink**, each flushed batch is encoded with the same file endpoint formats (`normal` JSONL, `json`, `text`, `raw`) and written as **one immutable object** at `<prefix>/[YYYY/MM/DD/]<uuidv7>.<ext>`. Objects are write-once — nothing is appended or mutated. The uuidv7 name already sorts by write time; the optional `date_partition` prefix (on by default, derived from that same id's timestamp) is a readability / lifecycle-rule convenience. This naming applies only while `idempotency` is off — with `idempotency: true` the parts are instead named for the source range they cover and written flat under the prefix, and `date_partition` is ignored (see "Files & object storage — `idempotency`" above).
|
|
372
421
|
* **As a source**, objects under the prefix are listed in key order, fetched whole, split on the delimiter, and emitted. Progress is a durable cursor holding the last fully-acked object key: set `cursor_id` and an external `checkpoint_store` (`file://`, `s3://`, `postgres://`, `mongodb://`) so a restart resumes without re-emitting. Objects are **never deleted or rewritten** — resume is non-destructive and at-least-once at object granularity (a nacked batch is redelivered; the cursor only advances once an object is fully acked). `csv` is supported on the source only.
|
|
373
422
|
|
|
374
423
|
```yaml
|
|
@@ -396,6 +445,26 @@ replay_from_s3:
|
|
|
396
445
|
> always write the `{message_id, payload, metadata}` wrapper, so the message id survives the round
|
|
397
446
|
> trip. Use `format: raw` to write payloads verbatim (bare documents, no wrapper). Applies to both
|
|
398
447
|
> `file` and `object_store`.
|
|
448
|
+
>
|
|
449
|
+
> A **source** on those formats expects that same wrapper, `message_id` included. A line that is
|
|
450
|
+
> valid JSON but not the wrapper — a hand-written fixture with only `payload` and `metadata`, say —
|
|
451
|
+
> is not decomposed: the whole line becomes the payload and the line's own `metadata` is discarded.
|
|
452
|
+
> The reader logs a warning naming the wrapper when this happens. For plain JSON lines, use
|
|
453
|
+
> `format: raw`.
|
|
454
|
+
>
|
|
455
|
+
> **Payload encoding in the wrapper.** A UTF-8 payload is written as a plain JSON string under
|
|
456
|
+
> `payload`. A binary one (compressed, encrypted, Protobuf, …) is base64-encoded under a separate
|
|
457
|
+
> `payload_base64` field; the two are mutually exclusive, as in the
|
|
458
|
+
> [CloudEvents JSON format](https://github.com/cloudevents/spec/blob/main/cloudevents/formats/json-format.md).
|
|
459
|
+
>
|
|
460
|
+
> ```json
|
|
461
|
+
> {"message_id":"019f9b12-d786-7ebe-a7ec-a1aa71bc47ae","payload":"{\"order_id\":7}"}
|
|
462
|
+
> {"message_id":"019f9b12-d78a-7c01-b0f4-2f0f4d6a1c33","payload_base64":"KLUv/SBOAQAA"}
|
|
463
|
+
> ```
|
|
464
|
+
>
|
|
465
|
+
> Sources still read the older byte-array form (`"payload":[123,34,…]`), so existing files keep
|
|
466
|
+
> working. Only the reverse is a break: a **binary** record written by this version is not readable
|
|
467
|
+
> by an older mq-bridge. Text records are compatible in both directions.
|
|
399
468
|
|
|
400
469
|
### Response Endpoint
|
|
401
470
|
The `response` output endpoint sends a reply back to the original requester. This is useful for synchronous request-reply flows, for example HTTP-to-NATS-to-HTTP. Use `response: {}` as the output endpoint configuration.
|
|
@@ -43,7 +43,15 @@ Writing with `[compression, encryption]` produces `compress(encrypt(payload))`;
|
|
|
43
43
|
given that same list would try to decrypt first and fail. The reading route must say
|
|
44
44
|
`[encryption, compression]`. The lists mirror — they are not copied.
|
|
45
45
|
|
|
46
|
-
|
|
46
|
+
**A route handler sits outside every output middleware**, so it runs **once** per message
|
|
47
|
+
and the middlewares act on what it returned. In particular `retry` re-attempts only the
|
|
48
|
+
publish, never the handler — a handler with a side effect fires once however many times the
|
|
49
|
+
sink is retried. The trade: a `dlq` cannot capture a handler failure, only a send failure;
|
|
50
|
+
a handler error propagates to the route and is reported there.
|
|
51
|
+
|
|
52
|
+
> This is asserted by `route::tests::test_retryable_handler_error_is_not_retried_by_output_middleware`
|
|
53
|
+
> (the handler runs once and `retry` does not re-run it),
|
|
54
|
+
> `route::tests::test_dlq_and_retry_batch_integration`,
|
|
47
55
|
> `middleware::transform::tests::test_rejected_message_reaches_the_dlq_through_the_config_wiring`,
|
|
48
56
|
> and `reference_docs_test::publisher_middleware_wraps_last_entry_outermost`, and is
|
|
49
57
|
> documented on `apply_middlewares_to_publisher` in `src/middleware/mod.rs`.
|
|
@@ -88,9 +96,10 @@ middlewares:
|
|
|
88
96
|
**Putting a middleware on the wrong side behaves in two different ways**, so check the table
|
|
89
97
|
above rather than assuming:
|
|
90
98
|
|
|
91
|
-
- `
|
|
92
|
-
|
|
93
|
-
|
|
99
|
+
- `dlq` / `retry` on an input log a warning and are skipped. The route still starts.
|
|
100
|
+
- `deduplication` and `weak_join` on an output are **hard startup errors**. Deduplication
|
|
101
|
+
cannot work on the publish side, and silently starting an un-deduplicated route is worse
|
|
102
|
+
than refusing to start.
|
|
94
103
|
|
|
95
104
|
A middleware whose feature is not compiled in (`deduplication` without `dedup`, `metrics`
|
|
96
105
|
without `metrics`) is likewise a startup error, not a silent no-op.
|
|
@@ -131,16 +140,20 @@ Sends permanently-failed messages to a separate endpoint instead of failing the
|
|
|
131
140
|
```
|
|
132
141
|
|
|
133
142
|
Captures `NonRetryable` failures and `Retryable` ones whose retries are exhausted. Connection
|
|
134
|
-
errors are **not** dead-lettered — they propagate so the route can reconnect.
|
|
143
|
+
errors are **not** dead-lettered — they propagate so the route can reconnect. Nor are handler
|
|
144
|
+
failures: the handler runs outside the middlewares (see [Ordering](#ordering--read-this-before-combining-middleware)),
|
|
145
|
+
so a `dlq` only ever sees what failed on the way to the sink. The DLQ endpoint
|
|
135
146
|
is a full endpoint, so it can itself have middleware. If the DLQ send fails with a connection
|
|
136
147
|
error that error propagates rather than silently dropping the message.
|
|
137
148
|
|
|
138
149
|
**Without a `dlq` middleware**, a message that fails permanently — a data/type
|
|
139
|
-
error the sink rejects
|
|
150
|
+
error the sink rejects — is logged at `error` level and
|
|
140
151
|
**dropped**, and the route keeps processing the rest of the batch. `dlq` is the only retention
|
|
141
152
|
mechanism: `retry` alone does not retain a permanently-failed message nor prevent it from being
|
|
142
|
-
dropped — it only re-attempts
|
|
143
|
-
to
|
|
153
|
+
dropped — it only re-attempts `Retryable` errors (a connection error is passed straight through
|
|
154
|
+
for the route to reconnect on, not retried), then hands a still-failing message on to be dropped
|
|
155
|
+
(or to a following `dlq`). This is why a sink that fails with a *connection* error never reaches
|
|
156
|
+
its `dlq`, whether it is a route's sole output or one leg of a `fanout`. This tolerate-and-continue
|
|
144
157
|
policy keeps one bad message from halting the whole stream, but it means a *systematic* failure
|
|
145
158
|
(e.g. every row hitting a column-type mismatch) drains the input while committing nothing and
|
|
146
159
|
still ends `completed`. Add a `dlq` to capture the failures for inspection/replay, or watch the
|
|
@@ -161,11 +174,17 @@ validation — over a single parse. Input and output.
|
|
|
161
174
|
| `schema_file` | path to a schema file | – |
|
|
162
175
|
| `coerce` | bool | `true` |
|
|
163
176
|
| `apply_defaults` | bool | `true` |
|
|
177
|
+
| `coerce_empty_as_null` | bool | `false` |
|
|
164
178
|
| `on_error` | `reject` \| `pass_through` | `reject` |
|
|
165
179
|
|
|
166
180
|
`schema` and `schema_file` are mutually exclusive. A mapping rule is either a bare path
|
|
167
181
|
string or `{ path, default, required }`.
|
|
168
182
|
|
|
183
|
+
`schema` must be a JSON *object*, not a string containing one. A flat `key=value` middleware
|
|
184
|
+
syntax (such as the `|transform?schema=…` form in a connection URI) can only pass strings, so
|
|
185
|
+
it cannot express `schema` or any mapping rule beyond a bare path — use `schema_file`, or move
|
|
186
|
+
the route into a config file.
|
|
187
|
+
|
|
169
188
|
```yaml middleware
|
|
170
189
|
- transform:
|
|
171
190
|
mapping:
|
|
@@ -185,6 +204,26 @@ else is ignored, so an existing fuller schema can be used as-is. Coercions are l
|
|
|
185
204
|
lossless ones: `string → integer`, `string → number`, `string → boolean` (`true`/`false`/`1`/`0`),
|
|
186
205
|
`number → string`.
|
|
187
206
|
|
|
207
|
+
#### Empty strings
|
|
208
|
+
|
|
209
|
+
CSV and many SQL exports spell "no value" as an empty string. `coerce_empty_as_null: true`
|
|
210
|
+
reads every `""` the schema visits as `null`, which is then handled like any other null —
|
|
211
|
+
a `nullable` field keeps it, a `default` replaces it:
|
|
212
|
+
|
|
213
|
+
```yaml middleware
|
|
214
|
+
- transform:
|
|
215
|
+
coerce_empty_as_null: true
|
|
216
|
+
schema:
|
|
217
|
+
type: object
|
|
218
|
+
properties:
|
|
219
|
+
note: { type: string, nullable: true }
|
|
220
|
+
tier: { type: string, default: standard }
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
`note: ""` arrives as `null` and `tier: ""` as `"standard"`. A field that is neither
|
|
224
|
+
nullable nor defaulted is rejected, naming the coercion. Only fields the schema declares are
|
|
225
|
+
affected; `" "` is not empty.
|
|
226
|
+
|
|
188
227
|
#### Embedded JSON
|
|
189
228
|
|
|
190
229
|
A field carrying a JSON document as a string is decoded by `contentMediaType`, following
|
|
@@ -311,6 +350,11 @@ Correlates messages by a metadata key and emits them as one joined message. Inpu
|
|
|
311
350
|
on_timeout: discard
|
|
312
351
|
```
|
|
313
352
|
|
|
353
|
+
`group_by` reads message **metadata** only — never the payload. A message that lacks the key
|
|
354
|
+
falls into a shared `"default"` group, so a mistyped key or a source that never sets it joins
|
|
355
|
+
unrelated messages instead of failing. If the value lives in the payload, lift it into metadata
|
|
356
|
+
first (a `transform` mapping, or the source's own metadata options).
|
|
357
|
+
|
|
314
358
|
Setting `branch_by` switches to branch mode, where `required` overrides `expected_count`.
|
|
315
359
|
On timeout an incomplete group is either emitted partially (`fire`) or dropped (`discard`).
|
|
316
360
|
Messages are acknowledged on receipt, so a crash before the group completes loses the
|
|
@@ -506,6 +550,15 @@ Deliberate fault injection for testing recovery paths. Input and output.
|
|
|
506
550
|
non-retryable one — useful for exercising a `dlq`. Keep `enabled: false` in committed configs
|
|
507
551
|
rather than deleting the block.
|
|
508
552
|
|
|
553
|
+
On the **input** side, `json_format_error`/`nack` never call the real consumer at all — they
|
|
554
|
+
substitute a synthetic message (or error) on every triggered `receive`. Leaving
|
|
555
|
+
`trigger_on_message` unset means *every* poll is faulted, so the real source is never read and
|
|
556
|
+
`exit_on_empty`/`--drain` never sees the empty batch it waits for — the route runs forever,
|
|
557
|
+
manufacturing synthetic messages. Always set `trigger_on_message` to a specific count when
|
|
558
|
+
testing a drain-mode route with input-side fault injection. And since `dlq`/`retry` on an input
|
|
559
|
+
are no-ops (see above), pair an input-side fault with a real assertion on the *consumer's*
|
|
560
|
+
recovery, not a `dlq`.
|
|
561
|
+
|
|
509
562
|
The middleware block alone is **not** enough: fault injection is gated per route by
|
|
510
563
|
`allow_fault_injection`, which defaults to `false`. Copying only the snippet above leaves the
|
|
511
564
|
middleware inert (the route logs that it is disabled). A complete, working configuration:
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# Security Policy
|
|
2
|
+
|
|
3
|
+
## Reporting a vulnerability
|
|
4
|
+
|
|
5
|
+
Please report suspected vulnerabilities privately via
|
|
6
|
+
[GitHub Security Advisories](https://github.com/marcomq/mq-bridge/security/advisories/new)
|
|
7
|
+
rather than opening a public issue.
|
|
8
|
+
|
|
9
|
+
## Supply-chain checks
|
|
10
|
+
|
|
11
|
+
`cargo deny check` runs in CI ([deny.toml](deny.toml)). Licenses, bans, and
|
|
12
|
+
sources are hard-gated; advisories run as an informational step, for the reasons
|
|
13
|
+
explained below.
|
|
14
|
+
|
|
15
|
+
## Known advisories and why they do not (mostly) affect mq-bridge
|
|
16
|
+
|
|
17
|
+
Running `cargo audit` against this repository reports findings. **Most of them
|
|
18
|
+
are not reachable in mq-bridge**, and the rest are narrower than the raw output
|
|
19
|
+
suggests. This section records the analysis so you do not have to redo it.
|
|
20
|
+
|
|
21
|
+
Two structural points come first, because they explain most of the gap between
|
|
22
|
+
the tool output and the real exposure:
|
|
23
|
+
|
|
24
|
+
1. **`cargo audit` and `cargo deny` read `Cargo.lock`, which is feature-blind.**
|
|
25
|
+
Almost every transport in this crate sits behind an optional Cargo feature.
|
|
26
|
+
A finding attributed to, say, the AWS SDK is not compiled at all in a
|
|
27
|
+
Kafka-only or Postgres-only build. The table below therefore states status
|
|
28
|
+
*per feature*.
|
|
29
|
+
2. **A crate appearing in the tree is not the same as its vulnerable code being
|
|
30
|
+
used.** In one case below, a dependency pulls in an affected crate purely to
|
|
31
|
+
name a type in an error enum, while the actual cryptographic work happens in
|
|
32
|
+
a different, patched version.
|
|
33
|
+
|
|
34
|
+
### Current status
|
|
35
|
+
|
|
36
|
+
| Advisory | Crate / path | Feature | Status |
|
|
37
|
+
| --- | --- | --- | --- |
|
|
38
|
+
| [RUSTSEC-2026-0104](https://rustsec.org/advisories/RUSTSEC-2026-0104) | `rustls-webpki` via `rumqttc`, `aws-sdk-*` | `mqtt`, `aws` | **Not affected** — CRL code path never invoked |
|
|
39
|
+
| [RUSTSEC-2026-0049](https://rustsec.org/advisories/RUSTSEC-2026-0049) | `rustls-webpki 0.102.8` via `rumqttc` | `mqtt` | **Not affected** — CRL code path never invoked |
|
|
40
|
+
| [RUSTSEC-2026-0098](https://rustsec.org/advisories/RUSTSEC-2026-0098) | `rustls-webpki 0.102.8` via `rumqttc` | `mqtt` | **Not affected** — validation runs on patched `0.103.13` |
|
|
41
|
+
| [RUSTSEC-2026-0099](https://rustsec.org/advisories/RUSTSEC-2026-0099) | `rustls-webpki 0.102.8` via `rumqttc` | `mqtt` | **Not affected** — validation runs on patched `0.103.13` |
|
|
42
|
+
| [RUSTSEC-2026-0098](https://rustsec.org/advisories/RUSTSEC-2026-0098) | `rustls-webpki 0.101.7` via `aws-sdk-*` | `aws` | **Reachable**, narrow preconditions — see below |
|
|
43
|
+
| [RUSTSEC-2026-0099](https://rustsec.org/advisories/RUSTSEC-2026-0099) | `rustls-webpki 0.101.7` via `aws-sdk-*` | `aws` | **Reachable**, narrow preconditions — see below |
|
|
44
|
+
| [RUSTSEC-2025-0057](https://rustsec.org/advisories/RUSTSEC-2025-0057) | `fxhash` via `sled 0.34` | `sled`, `dedup` | Unmaintained only, no vulnerability |
|
|
45
|
+
| [RUSTSEC-2024-0384](https://rustsec.org/advisories/RUSTSEC-2024-0384) | `instant` via `sled 0.34` → `parking_lot 0.11` | `sled`, `dedup` | Unmaintained only, no vulnerability |
|
|
46
|
+
| [RUSTSEC-2025-0134](https://rustsec.org/advisories/RUSTSEC-2025-0134) | `rustls-pemfile` (direct + `rumqttc`) | `amqp`, `nats`, `mqtt`, `http` | Unmaintained only, no vulnerability |
|
|
47
|
+
|
|
48
|
+
Each row applies only to the features listed for it: a build that enables none of
|
|
49
|
+
a row's features does not compile that finding's code. `rustls-pemfile` is the
|
|
50
|
+
broadest — it is pulled in by `amqp`, `nats`, `mqtt`, and `http` alike.
|
|
51
|
+
|
|
52
|
+
### Detail: the `rustls-webpki` advisories
|
|
53
|
+
|
|
54
|
+
These reach the dependency tree by two independent paths.
|
|
55
|
+
|
|
56
|
+
**Path A — `rumqttc 0.25` → `rustls-webpki 0.102.8` (feature `mqtt`).**
|
|
57
|
+
`rumqttc` declares this dependency but uses exactly one item from it: the
|
|
58
|
+
`webpki::Error` variant in its own error enum (`src/tls.rs:42` is the only
|
|
59
|
+
reference to `webpki` in the crate). All certificate validation on the MQTT path
|
|
60
|
+
runs through `tokio-rustls 0.26` → `rustls 0.23` → `rustls-webpki 0.103.13`,
|
|
61
|
+
which is patched for all four advisories. **Not affected.**
|
|
62
|
+
|
|
63
|
+
**Path B — `aws-config` / `aws-sdk-sqs` / `aws-sdk-sns` →
|
|
64
|
+
`aws-smithy-http-client` → legacy `rustls 0.21` → `rustls-webpki 0.101.7`
|
|
65
|
+
(feature `aws`).** `aws-smithy-http-client` still selects its
|
|
66
|
+
`legacy-rustls-ring` connector, so this is live code on the SQS/SNS path. It
|
|
67
|
+
splits two ways:
|
|
68
|
+
|
|
69
|
+
- **RUSTSEC-2026-0104 and RUSTSEC-2026-0049 (CRL handling): not affected.** This
|
|
70
|
+
code executes only when the application supplies a certificate revocation list
|
|
71
|
+
to a client-certificate verifier. The AWS SDK is an outbound HTTPS client and
|
|
72
|
+
never configures one. mq-bridge's only `WebPkiClientVerifier` — used for the
|
|
73
|
+
optional mTLS HTTP server in `src/endpoints/http/mod.rs` — runs on
|
|
74
|
+
`rustls 0.23` / `rustls-webpki 0.103.13` and is never given a CRL.
|
|
75
|
+
- **RUSTSEC-2026-0098 and RUSTSEC-2026-0099 (name constraints): reachable.**
|
|
76
|
+
These fire during ordinary server-certificate chain validation, which the SQS
|
|
77
|
+
and SNS clients perform on every call. Exploitation requires a
|
|
78
|
+
name-constrained CA present in the trust store that issues certificates
|
|
79
|
+
outside its constraints — narrow, and not something mq-bridge can trigger or
|
|
80
|
+
mitigate itself. The fix depends on the AWS SDK migrating off its legacy
|
|
81
|
+
`rustls 0.21` connector. We do not claim to be unaffected here.
|
|
82
|
+
|
|
83
|
+
### Detail: the unmaintained crates
|
|
84
|
+
|
|
85
|
+
None of these carry a known vulnerability; RustSec flags them as unmaintained.
|
|
86
|
+
|
|
87
|
+
- **`fxhash`, `instant`** arrive through `sled 0.34`, used by the deduplication
|
|
88
|
+
store. They resolve when `sled` updates its own dependencies.
|
|
89
|
+
- **`rustls-pemfile`** is a direct dependency of mq-bridge as well as of
|
|
90
|
+
`rumqttc`. It is superseded by pemfile support in `rustls-pki-types`.
|
|
91
|
+
Migrating the direct dependency is tracked as a follow-up.
|
|
92
|
+
|
|
93
|
+
### Keeping this current
|
|
94
|
+
|
|
95
|
+
[deny.toml](deny.toml) carries these advisories as `[advisories].ignore`
|
|
96
|
+
entries, each with its reasoning inline, so suppressions and justifications
|
|
97
|
+
cannot drift apart. When an advisory is added, removed, or changes status,
|
|
98
|
+
update both files together.
|
|
99
|
+
|
|
100
|
+
The two tools do not report identically. `cargo deny` is configured with
|
|
101
|
+
`unmaintained = "workspace"`, which surfaces unmaintained crates only when we
|
|
102
|
+
depend on them directly — so the transitive `fxhash` and `instant` advisories
|
|
103
|
+
need no entry there. `cargo audit` applies no such filter and does report them.
|
|
104
|
+
Both are covered in the table above so either tool leads to the same answer.
|