mq-bridge-py-basic 0.4.1__tar.gz → 0.4.3__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.4.1 → mq_bridge_py_basic-0.4.3}/Cargo.lock +6 -6
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/Cargo.toml +2 -2
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/PKG-INFO +1 -1
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/docs/ARCHITECTURE.md +13 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/docs/DELIVERY.md +62 -10
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/mq-bridge.schema.json +12 -2
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/mq_bridge/config.pyi +2 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/src/command_handler.rs +4 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/src/endpoints/file/mod.rs +78 -5
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/src/endpoints/file/tests.rs +257 -6
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/src/endpoints/http/mod.rs +22 -3
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/src/endpoints/http/tests.rs +351 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/src/endpoints/mod.rs +127 -15
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/src/endpoints/mongodb/readers.rs +85 -18
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/src/endpoints/mongodb/tests.rs +46 -6
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/src/endpoints/object_store.rs +115 -11
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/src/endpoints/sqlx/mod.rs +88 -3
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/src/endpoints/sqlx/tests.rs +229 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/src/endpoints/structural/fanout.rs +5 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/src/endpoints/structural/request.rs +4 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/src/endpoints/structural/switch.rs +8 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/src/middleware/buffer.rs +4 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/src/middleware/compression.rs +4 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/src/middleware/cookie_jar.rs +4 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/src/middleware/delay.rs +4 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/src/middleware/dlq.rs +8 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/src/middleware/encryption.rs +4 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/src/middleware/limiter.rs +4 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/src/middleware/metrics.rs +4 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/src/middleware/random_panic.rs +4 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/src/middleware/retry.rs +4 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/src/middleware/transform/mod.rs +4 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/src/models/builders.rs +1 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/src/models.rs +10 -2
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/src/plugin/middleware.rs +4 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/src/route.rs +237 -9
- mq_bridge_py_basic-0.4.3/src/support/source_ranges.rs +801 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/src/traits.rs +38 -0
- mq_bridge_py_basic-0.4.3/tests/integration/docker-compose/mosquitto_persistence.conf +20 -0
- mq_bridge_py_basic-0.4.1/src/support/source_ranges.rs +0 -448
- mq_bridge_py_basic-0.4.1/tests/integration/docker-compose/mosquitto_persistence.conf +0 -18
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/.github/dependabot.yml +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/.github/workflows/benchmark.yml +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/.github/workflows/ci.yml +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/.github/workflows/ibm-mq.yml +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/.github/workflows/node.yml +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/.github/workflows/publish-node.yml +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/.github/workflows/publish-python.yml +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/.github/workflows/python.yml +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/.github/workflows/release.yml +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/.github/workflows/test-matrix.yml +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/.gitignore +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/CHANGELOG.md +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/CLAUDE.md +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/CONTRIBUTING.md +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/LICENSE +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/LICENSE.python +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/README.md +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/SECURITY.md +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/benches/ETL_BENCHMARKS.md +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/benches/performance_bench.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/benches/router_bench.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/benches/zeromq_backends.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/bindings-common/Cargo.toml +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/bindings-common/src/lib.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/bindings-common/src/logging.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/build.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/deny.toml +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/docs/CONFIGURATION.md +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/docs/EXTENDING.md +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/docs/PLUGINS.md +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/docs/REFERENCE.md +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/examples/ipc_worker_queue.yaml +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/mq_bridge/__init__.py +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/mq_bridge/__init__.pyi +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/mq_bridge/config.py +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/mq_bridge/plugin_packaging.py +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/mq_bridge/py.typed +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/package-lock.json +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/pyproject.toml +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/python/mq-bridge-py/Cargo.toml +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/python/mq-bridge-py/LICENSE.python +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/python/mq-bridge-py/README.md +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/python/mq-bridge-py/analysis/bench_http_native.py +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/python/mq-bridge-py/example.py +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/python/mq-bridge-py/examples/bench_memory.py +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/python/mq-bridge-py/examples/bench_memory.yaml +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/python/mq-bridge-py/examples/consume_pull.py +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/python/mq-bridge-py/examples/dlt_source.py +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/python/mq-bridge-py/examples/json_route.py +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/python/mq-bridge-py/examples/memory.yaml +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/python/mq-bridge-py/examples/raw_route.py +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/python/mq-bridge-py/pyproject-basic.toml +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/python/mq-bridge-py/pyproject-reduced.toml +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/python/mq-bridge-py/scripts/gen_config_types.py +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/python/mq-bridge-py/src/lib.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/python/mq-bridge-py/tests/test_config_types.py +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/python/mq-bridge-py/tests/test_custom_endpoint.py +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/python/mq-bridge-py/tests/test_logging.py +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/python/mq-bridge-py/tests/test_packaging.py +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/python/mq-bridge-py/tests/test_performance_smoke.py +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/python/mq-bridge-py/tests/test_plugin_package.py +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/python/mq-bridge-py/tests/test_public_api.py +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/python/mq-bridge-py/tests/test_routes.py +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/python/mq-bridge-py/uv.lock +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/scripts/analysis/benches/metadata_bench.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/scripts/analysis/debug_ibm_mq_tls.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/scripts/analysis/http/analyze_xctrace_time_profile.py +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/scripts/analysis/http/http_flamegraph.py +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/scripts/analysis/http/mq_bridge_http_profile.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/scripts/analysis/http/run_http_ladder.sh +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/scripts/ci/format_bench_output.sh +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/scripts/httparena/README.md +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/scripts/httparena/frameworks/mq-bridge-py/Dockerfile +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/scripts/httparena/frameworks/mq-bridge-py/meta.json +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/scripts/httparena/frameworks/mq-bridge-py/server.py +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/scripts/techempower/Python/mq-bridge-py/README.md +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/scripts/techempower/Python/mq-bridge-py/benchmark_config.json +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/scripts/techempower/Python/mq-bridge-py/mq-bridge-py.dockerfile +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/scripts/techempower/Python/mq-bridge-py/server.py +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/scripts/techempower/README.md +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/scripts/techempower/postgres.yml +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/scripts/techempower/seed.sql +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/scripts/techempower/verify.sh +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/scripts/the-benchmarker/README.md +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/scripts/the-benchmarker/python/mq-bridge-py/config.yaml +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/scripts/the-benchmarker/python/mq-bridge-py/pyproject.toml +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/scripts/the-benchmarker/python/mq-bridge-py/server.py +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/src/canonical_message.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/src/checkpoint.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/src/endpoints/amqp.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/src/endpoints/aws.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/src/endpoints/clickhouse.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/src/endpoints/grpc.proto +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/src/endpoints/grpc.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/src/endpoints/http/publisher.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/src/endpoints/http/stream.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/src/endpoints/ibm_mq.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/src/endpoints/kafka.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/src/endpoints/memory/endpoint.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/src/endpoints/memory/framed.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/src/endpoints/memory/ipc_unix.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/src/endpoints/memory/ipc_windows.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/src/endpoints/memory/memory_transport.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/src/endpoints/memory/mod.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/src/endpoints/memory/transport.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/src/endpoints/mongodb/consumer.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/src/endpoints/mongodb/dedup.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/src/endpoints/mongodb/mod.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/src/endpoints/mongodb/publisher.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/src/endpoints/mqtt.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/src/endpoints/nats.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/src/endpoints/poll.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/src/endpoints/postgres/VENDORED.md +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/src/endpoints/postgres/mod.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/src/endpoints/postgres/pgoutput/decoder.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/src/endpoints/postgres/pgoutput/messages.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/src/endpoints/postgres/pgoutput/mod.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/src/endpoints/postgres/pgoutput/registry.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/src/endpoints/postgres/pgoutput/values.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/src/endpoints/postgres/replication.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/src/endpoints/postgres/state.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/src/endpoints/redis_streams.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/src/endpoints/sled.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/src/endpoints/sqlx/dedup.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/src/endpoints/structural/mod.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/src/endpoints/structural/null.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/src/endpoints/structural/reader.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/src/endpoints/structural/response.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/src/endpoints/structural/static_endpoint.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/src/endpoints/structural/stream_buffer.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/src/endpoints/websocket.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/src/endpoints/zeromq/codec.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/src/endpoints/zeromq/mod.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/src/endpoints/zeromq/omq.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/src/endpoints/zeromq/zmq.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/src/errors.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/src/event_handler.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/src/event_store.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/src/extensions.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/src/lib.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/src/middleware/deduplication.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/src/middleware/id.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/src/middleware/mod.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/src/middleware/transform/coerce.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/src/middleware/transform/compiled.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/src/middleware/transform/error.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/src/middleware/transform/path.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/src/middleware/transform/schema.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/src/middleware/transform/tests.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/src/middleware/weak_join.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/src/models/defaults.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/src/models/secrets.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/src/models/serde_support.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/src/models/tests.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/src/outcomes.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/src/plugin/conformance.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/src/plugin/endpoint.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/src/plugin/message.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/src/plugin/mod.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/src/plugin/sdk.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/src/plugin/test_support.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/src/publisher.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/src/response.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/src/support/base64_engine.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/src/support/compression.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/src/support/connection_registry.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/src/support/crypto.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/src/support/crypto_envelope.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/src/support/interpolation.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/src/support/mod.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/src/support/plugin_abi.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/src/test_utils.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/src/type_handler.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/tests/README.md +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/tests/armature_integration.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/tests/integration/amqp.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/tests/integration/aws.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/tests/integration/clickhouse.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/tests/integration/config.toml +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/tests/integration/docker-compose/amqp.yml +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/tests/integration/docker-compose/aws.yml +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/tests/integration/docker-compose/clickhouse.yml +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/tests/integration/docker-compose/ibm-mq-certs/client.crl +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/tests/integration/docker-compose/ibm-mq-certs/client.kdb +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/tests/integration/docker-compose/ibm-mq-certs/client.p12 +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/tests/integration/docker-compose/ibm-mq-certs/client.rdb +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/tests/integration/docker-compose/ibm-mq-certs/client.sth +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/tests/integration/docker-compose/ibm_mq.yml +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/tests/integration/docker-compose/ibm_mq_tls.yml +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/tests/integration/docker-compose/kafka-tls.yml +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/tests/integration/docker-compose/kafka.yml +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/tests/integration/docker-compose/mariadb.yml +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/tests/integration/docker-compose/mongodb-replica.yml +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/tests/integration/docker-compose/mongodb-tls.yml +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/tests/integration/docker-compose/mongodb.yml +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/tests/integration/docker-compose/mosquitto.conf +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/tests/integration/docker-compose/mosquitto_performance.conf +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/tests/integration/docker-compose/mq_init.mqsc +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/tests/integration/docker-compose/mq_init_tls.mqsc +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/tests/integration/docker-compose/mqtt.yml +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/tests/integration/docker-compose/mqtt_performance.yml +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/tests/integration/docker-compose/mysql.yml +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/tests/integration/docker-compose/nats.yml +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/tests/integration/docker-compose/object_store.yml +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/tests/integration/docker-compose/postgres.yml +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/tests/integration/docker-compose/postgres_cdc.yml +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/tests/integration/docker-compose/redis.yml +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/tests/integration/file.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/tests/integration/grpc.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/tests/integration/grpc_tls.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/tests/integration/http.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/tests/integration/http_tls.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/tests/integration/ibm_mq.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/tests/integration/ibm_mq_tls.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/tests/integration/ipc.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/tests/integration/kafka.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/tests/integration/kafka_tls.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/tests/integration/logic_test.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/tests/integration/mariadb.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/tests/integration/memory.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/tests/integration/mod.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/tests/integration/mongodb.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/tests/integration/mongodb_raw.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/tests/integration/mongodb_tls.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/tests/integration/mqtt.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/tests/integration/mysql.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/tests/integration/nats.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/tests/integration/object_store.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/tests/integration/performance_static.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/tests/integration/postgres.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/tests/integration/postgres_cdc.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/tests/integration/redis_streams.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/tests/integration/route.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/tests/integration/scripts/gen_certs.sh +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/tests/integration/sqlite.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/tests/integration/tls_helpers.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/tests/integration/websocket.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/tests/integration/zeromq.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/tests/integration_test.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/tests/memory_leak_test.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/tests/ref_test.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/tests/reference_docs_test.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/tests/sqlite_test.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/tests/tls_example.rs +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/tests/unit_tests.md +0 -0
- {mq_bridge_py_basic-0.4.1 → mq_bridge_py_basic-0.4.3}/tests/websocket_test.rs +0 -0
|
@@ -1900,9 +1900,9 @@ dependencies = [
|
|
|
1900
1900
|
|
|
1901
1901
|
[[package]]
|
|
1902
1902
|
name = "fast-uuid-v7"
|
|
1903
|
-
version = "0.1.
|
|
1903
|
+
version = "0.1.9"
|
|
1904
1904
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1905
|
-
checksum = "
|
|
1905
|
+
checksum = "dc759201eb9b09178fb2174087a451d19fd46a8b8b4f5f6103de83b9f5949f52"
|
|
1906
1906
|
dependencies = [
|
|
1907
1907
|
"rand 0.10.2",
|
|
1908
1908
|
]
|
|
@@ -3348,7 +3348,7 @@ dependencies = [
|
|
|
3348
3348
|
|
|
3349
3349
|
[[package]]
|
|
3350
3350
|
name = "mq-bridge"
|
|
3351
|
-
version = "0.4.
|
|
3351
|
+
version = "0.4.3"
|
|
3352
3352
|
dependencies = [
|
|
3353
3353
|
"aes-gcm",
|
|
3354
3354
|
"anyhow",
|
|
@@ -3431,7 +3431,7 @@ dependencies = [
|
|
|
3431
3431
|
|
|
3432
3432
|
[[package]]
|
|
3433
3433
|
name = "mq-bridge-bindings-common"
|
|
3434
|
-
version = "0.4.
|
|
3434
|
+
version = "0.4.3"
|
|
3435
3435
|
dependencies = [
|
|
3436
3436
|
"anyhow",
|
|
3437
3437
|
"fast-uuid-v7",
|
|
@@ -3445,7 +3445,7 @@ dependencies = [
|
|
|
3445
3445
|
|
|
3446
3446
|
[[package]]
|
|
3447
3447
|
name = "mq-bridge-node"
|
|
3448
|
-
version = "0.4.
|
|
3448
|
+
version = "0.4.3"
|
|
3449
3449
|
dependencies = [
|
|
3450
3450
|
"anyhow",
|
|
3451
3451
|
"async-trait",
|
|
@@ -3487,7 +3487,7 @@ dependencies = [
|
|
|
3487
3487
|
|
|
3488
3488
|
[[package]]
|
|
3489
3489
|
name = "mq-bridge-py"
|
|
3490
|
-
version = "0.4.
|
|
3490
|
+
version = "0.4.3"
|
|
3491
3491
|
dependencies = [
|
|
3492
3492
|
"anyhow",
|
|
3493
3493
|
"async-trait",
|
|
@@ -16,13 +16,13 @@ members = ["bindings-common", "python/mq-bridge-py"]
|
|
|
16
16
|
resolver = "2"
|
|
17
17
|
|
|
18
18
|
[workspace.package]
|
|
19
|
-
version = "0.4.
|
|
19
|
+
version = "0.4.3"
|
|
20
20
|
|
|
21
21
|
[workspace.dependencies]
|
|
22
22
|
anyhow = "1.0"
|
|
23
23
|
async-trait = "0.1"
|
|
24
24
|
bytes = "1"
|
|
25
|
-
fast-uuid-v7 = "0.1.
|
|
25
|
+
fast-uuid-v7 = "0.1.9"
|
|
26
26
|
serde = "1.0"
|
|
27
27
|
serde_json = { version = "1.0", features = ["raw_value"] }
|
|
28
28
|
serde_yaml_ng = "0.10"
|
|
@@ -150,6 +150,19 @@ Batch processing is a core concept in mq-bridge and is required for all endpoint
|
|
|
150
150
|
- Each route can be configured with a `concurrency` parameter, which determines how many worker tasks will process batches in parallel.
|
|
151
151
|
- Batch size is also configurable per route.
|
|
152
152
|
|
|
153
|
+
#### Ordering
|
|
154
|
+
Two independent guarantees, each decided once per route by the endpoint itself:
|
|
155
|
+
|
|
156
|
+
- **Commits** follow `MessageConsumer::commit_requires_order()` (default `true`). Cumulative-ack sources such as Kafka funnel their commits through one sequencer; individually-acking sources commit concurrently, bounded by `commit_concurrency_limit`.
|
|
157
|
+
- **Publishing** follows `MessagePublisher::requires_ordered_publish()` (default `false`). Above `concurrency: 1` workers call `send_batch` in parallel, so whole batches can reach the sink out of source order — rows keep their order *within* a batch. The `file` sink declares `true` and the route sequences the `send_batch` calls; batch prep and commits stay parallel, so the ordered path measures within noise of an unordered one.
|
|
158
|
+
|
|
159
|
+
Only `file` declares it, for two reasons that any other candidate has to clear:
|
|
160
|
+
|
|
161
|
+
- **The cost must be low.** The file sink already holds a write lock across the whole batch, so sequencing changes who writes, not how many write at once. Sequencing a sink that ends in a network round trip instead costs the whole concurrency factor — one batch in flight instead of N.
|
|
162
|
+
- **The guarantee must be reachable.** `object_store` does *not* declare it: read order there is object-key order, and the uuidv7 key randomises everything below the millisecond, so ordering the writes would buy nothing. Ordered cloud export needs a source-sequenced key first.
|
|
163
|
+
|
|
164
|
+
Broker sinks keep it off. NATS (both Core and JetStream) publishes a batch through `send_batch_helper`, which keeps up to `SEND_BATCH_CONCURRENCY` publishes in flight, so wire order inside one batch is already unordered — only the results are re-sorted. Kafka and AMQP preserve order within a batch (they submit the whole batch, then await confirms), but for them the order-critical part is the cheap submit loop, not the round trip, so a useful fix would have to release the sequence after submit rather than after `send_batch`. For per-key Kafka ordering today, use `concurrency: 1`.
|
|
165
|
+
|
|
153
166
|
### Helper Utilities
|
|
154
167
|
mq-bridge provides several helper functions to make implementing batching easier, especially if your endpoint only supports single-message operations:
|
|
155
168
|
|
|
@@ -22,7 +22,7 @@ sink writes data.
|
|
|
22
22
|
| MongoDB | `id_field` set to a payload field or replay-stable template such as `${metadata:mqb.id}` |
|
|
23
23
|
| PostgreSQL / SQLite | `sqlx.insert_query` uses a unique key with `ON CONFLICT` |
|
|
24
24
|
| MySQL / MariaDB | `sqlx.insert_query` uses a unique key with `ON DUPLICATE KEY` |
|
|
25
|
-
| File / object store | `idempotency: true` with a replayable Kafka
|
|
25
|
+
| File / object store | `idempotency: true` with a replayable Kafka, Postgres CDC, SQL cursor, MongoDB CDC or file source |
|
|
26
26
|
|
|
27
27
|
For a source without stable identity, derive one once and consume it at the sink:
|
|
28
28
|
|
|
@@ -143,7 +143,7 @@ one identity to every message missing the field, so it is dropped instead.
|
|
|
143
143
|
| `mongodb` | Unique `_id` index; dup-key (11000) is treated as an idempotent success | `id_field` |
|
|
144
144
|
| `sqlx` (PostgreSQL / MySQL / SQLite) | The table's own `UNIQUE`/`PRIMARY KEY` | `ON CONFLICT` / `ON DUPLICATE KEY` in `insert_query` |
|
|
145
145
|
| `clickhouse` | `ReplacingMergeTree` collapses at merge time | Table DDL — no `mq-bridge` config |
|
|
146
|
-
| `file`, `object_store` | Deterministic part names + covered-range recovery | `idempotency: true` (needs a replayable source) |
|
|
146
|
+
| `file`, `object_store` | Deterministic, sortable part names + covered-range recovery | `idempotency: true` (needs a replayable source) |
|
|
147
147
|
| `kafka` | `enable.idempotence` dedups **producer retries within one session** — this is *not* exactly-once semantics | On by default |
|
|
148
148
|
| `nats`, `amqp`, `mqtt`, `redis_streams`, `aws`, `ibm_mq`, `zeromq` | None | Deduplicate at the next consumer instead |
|
|
149
149
|
|
|
@@ -359,6 +359,13 @@ but before the branch's downstream send committed, the replay hits a duplicate k
|
|
|
359
359
|
|
|
360
360
|
### Files & object storage — `idempotency`
|
|
361
361
|
|
|
362
|
+
> **Check first whether you need any of this.** If your records already carry a business key — an
|
|
363
|
+
> `id` field in the payload, or `mqb.id` from the [`id` middleware](#giving-a-source-an-identity) —
|
|
364
|
+
> then a key-addressed sink (Mongo `id_field`, SQL `ON CONFLICT`) already gives you effective
|
|
365
|
+
> exactly-once, and the order objects happen to land in does not matter. Positional naming below is
|
|
366
|
+
> for the case where the *sink itself* has to recognise a replay, or where a downstream reader
|
|
367
|
+
> depends on replay **order**. Turning it on when you do not need it only adds restrictions.
|
|
368
|
+
|
|
362
369
|
A filesystem has no unique constraint, so the `file` and `object_store` sinks get replay safety a
|
|
363
370
|
different way: **deterministic names plus covered-range recovery**. Set `idempotency: true` and the
|
|
364
371
|
sink stops writing UUID-named objects. Instead it groups each batch into runs of consecutive source
|
|
@@ -371,7 +378,7 @@ kafka_to_s3:
|
|
|
371
378
|
output:
|
|
372
379
|
object_store:
|
|
373
380
|
url: "s3://my-bucket/orders"
|
|
374
|
-
idempotency: true # →
|
|
381
|
+
idempotency: true # → part-orders-<partition>-<start>-<end>.jsonl (zero-padded)
|
|
375
382
|
```
|
|
376
383
|
|
|
377
384
|
On startup the sink lists what is already there, parses the ranges out of the part names, and drops
|
|
@@ -381,13 +388,58 @@ checkpoint protocol. Local files stage to a temp name, `fsync`, then `rename` (a
|
|
|
381
388
|
filesystem); object stores have no atomic rename, so a single PUT under the final name *is* the
|
|
382
389
|
commit.
|
|
383
390
|
|
|
384
|
-
This needs a replayable source position
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
`
|
|
390
|
-
|
|
391
|
+
This needs a replayable source position. Today that means:
|
|
392
|
+
|
|
393
|
+
| Source | Position | Restriction |
|
|
394
|
+
| --- | --- | --- |
|
|
395
|
+
| `kafka` | topic / partition / offset | — |
|
|
396
|
+
| `postgres_cdc` | commit LSN + in-transaction ordinal | `sqlx` with `publication` maps onto the same consumer |
|
|
397
|
+
| `mongodb` | cluster time + ordinal; the initial snapshot uses its `_id` scan index | `capture_new` / `capture_all` only — `consumer` and `snapshot` have no position |
|
|
398
|
+
| `file` | record index within the file | all modes; only `consume` deduplicates across runs (see below) |
|
|
399
|
+
| `sqlx` | the `cursor_column` value | cursor polling only; the column must be a unique, non-negative integer |
|
|
400
|
+
|
|
401
|
+
A route with an idempotent output turns `source_metadata` on for its input automatically; set it
|
|
402
|
+
explicitly only when you want the `mqb.src.*` keys for something else. Any other input is rejected
|
|
403
|
+
when the route starts. NATS and AMQP also accept `source_metadata` and emit provenance keys, but a
|
|
404
|
+
subject or routing key is not a replayable offset, so they cannot drive an idempotent sink.
|
|
405
|
+
|
|
406
|
+
**Without `idempotency: true`, an `object_store` sink names objects in write order**, and key order
|
|
407
|
+
is the only order a bucket has. At `concurrency: 1` that is source order; above it the name is minted
|
|
408
|
+
inside the worker pool, so it is arrival order, and replaying a change stream through the bucket can
|
|
409
|
+
reorder updates to the same key. The route logs a warning when it starts in that configuration.
|
|
410
|
+
|
|
411
|
+
**Numbers inside a part name are zero-padded** so that ASCII sort equals numeric sort — a bucket is
|
|
412
|
+
listed lexicographically and that listing order *is* the replay order. A bucket written by a version
|
|
413
|
+
before this padding contains unpadded names; the two forms do not sort correctly against each other,
|
|
414
|
+
so do not mix them in one prefix.
|
|
415
|
+
|
|
416
|
+
**MongoDB `capture_all`** reads the collection, then streams changes. Both phases are numbered into
|
|
417
|
+
the key so the snapshot always sorts ahead of the changes; a change can never replay before the
|
|
418
|
+
document it modifies. The snapshot does not resume across runs — it re-scans from the start — but the
|
|
419
|
+
numbering is deterministic, so a restart reproduces the same names and the covered-range recovery
|
|
420
|
+
skips them. The exception is inserts or deletes landing in the collection between the two runs: those
|
|
421
|
+
shift the numbering, and some documents are then written twice.
|
|
422
|
+
|
|
423
|
+
**The `sqlx` cursor source** uses the `cursor_column` value itself as the position — the same shape
|
|
424
|
+
Kafka Connect's S3 sink gives a Kafka offset. That only works for a unique, non-negative integer
|
|
425
|
+
column: a text cursor pages fine but has no contiguous numeric order, and a repeated value would
|
|
426
|
+
resolve two rows to one position and drop one of them. The reader rejects both at read time rather
|
|
427
|
+
than naming records wrongly. Consecutive ids coalesce into one part file; gaps split it.
|
|
428
|
+
|
|
429
|
+
**The `file` source** numbers records by their index in the file, not their byte offset (byte offsets
|
|
430
|
+
are not consecutive, so every record would become its own part).
|
|
431
|
+
|
|
432
|
+
That index repeats across runs only in `consume` mode, which always reads from byte 0 — and repeating
|
|
433
|
+
is the point: a re-read produces the same names, so the sink recognises them as already covered and
|
|
434
|
+
writes nothing. `subscribe` starts at the current end of the file and `group_subscribe` resumes at a
|
|
435
|
+
stored byte offset, so their index restarts at 0 over records an earlier run already numbered. Those
|
|
436
|
+
two modes therefore carry a **run epoch** in the object name.
|
|
437
|
+
|
|
438
|
+
The epoch keeps names distinct and keeps runs in order (a later run reads later records, so its
|
|
439
|
+
objects sort after the earlier run's). What it does not give you is deduplication across a restart:
|
|
440
|
+
records re-read after a crash are written again under new names. That is ordinary at-least-once, and
|
|
441
|
+
it is the honest guarantee for a source with no durable per-record position — these modes are
|
|
442
|
+
allowed, not rejected, because that guarantee is fine for plenty of pipelines.
|
|
391
443
|
|
|
392
444
|
For the `file` sink, `idempotency: true` changes what `path` means: it is the directory that receives
|
|
393
445
|
the part files, not the file that is appended to. The sink creates it on startup, so pointing it at an
|
|
@@ -1012,13 +1012,18 @@
|
|
|
1012
1012
|
"default": "normal"
|
|
1013
1013
|
},
|
|
1014
1014
|
"idempotency": {
|
|
1015
|
-
"description": "Write replay-safe source ranges as immutable part files under this directory.\nRequires
|
|
1015
|
+
"description": "Write replay-safe source ranges as immutable part files under this directory.\nRequires a source that stamps a replayable position: kafka, postgres_cdc, mongodb CDC, sqlx or file.",
|
|
1016
1016
|
"type": "boolean",
|
|
1017
1017
|
"default": false
|
|
1018
1018
|
},
|
|
1019
1019
|
"path": {
|
|
1020
1020
|
"description": "Path to the file, or to the directory holding the part files when `idempotency` is true.",
|
|
1021
1021
|
"type": "string"
|
|
1022
|
+
},
|
|
1023
|
+
"source_metadata": {
|
|
1024
|
+
"description": "(Consumer only) Include authoritative `mqb.src.file_*` source positions; only `consume` mode reproduces them across restarts. Defaults to false.",
|
|
1025
|
+
"type": "boolean",
|
|
1026
|
+
"default": false
|
|
1022
1027
|
}
|
|
1023
1028
|
},
|
|
1024
1029
|
"anyOf": [
|
|
@@ -2972,7 +2977,7 @@
|
|
|
2972
2977
|
"minimum": 1
|
|
2973
2978
|
},
|
|
2974
2979
|
"concurrency": {
|
|
2975
|
-
"description": "(Optional) Number of concurrent processing tasks for this route. While it improves throughput for high-latency\nhandlers, it adds synchronization overhead for ordered commits and may lead to out-of-order processing\nin the handler.
|
|
2980
|
+
"description": "(Optional) Number of concurrent processing tasks for this route. While it improves throughput for high-latency\nhandlers, it adds synchronization overhead for ordered commits and may lead to out-of-order processing\nin the handler. Above 1, whole batches may also reach the sink out of source order (rows keep their order\nwithin a batch) unless the sink declares itself order-sensitive, as `file` does.\nDefaults to 1.",
|
|
2976
2981
|
"type": "integer",
|
|
2977
2982
|
"format": "uint",
|
|
2978
2983
|
"default": 1,
|
|
@@ -3217,6 +3222,11 @@
|
|
|
3217
3222
|
"null"
|
|
3218
3223
|
]
|
|
3219
3224
|
},
|
|
3225
|
+
"source_metadata": {
|
|
3226
|
+
"description": "(Consumer only) Include authoritative `mqb.src.sqlx_*` source positions; `cursor_column` must then be a unique integer. Defaults to false.",
|
|
3227
|
+
"type": "boolean",
|
|
3228
|
+
"default": false
|
|
3229
|
+
},
|
|
3220
3230
|
"table": {
|
|
3221
3231
|
"description": "The table to interact with.",
|
|
3222
3232
|
"type": "string"
|
|
@@ -154,6 +154,7 @@ class FileConfig(TypedDict, total=False):
|
|
|
154
154
|
format: FileFormat
|
|
155
155
|
idempotency: bool
|
|
156
156
|
path: Required[str]
|
|
157
|
+
source_metadata: bool
|
|
157
158
|
|
|
158
159
|
|
|
159
160
|
class GrpcConfig(TypedDict, total=False):
|
|
@@ -476,6 +477,7 @@ class SqlxConfig(TypedDict, total=False):
|
|
|
476
477
|
select_query: Optional[str]
|
|
477
478
|
shared: Optional[bool]
|
|
478
479
|
slot_name: Optional[str]
|
|
480
|
+
source_metadata: bool
|
|
479
481
|
table: Required[str]
|
|
480
482
|
test_before_acquire: Optional[bool]
|
|
481
483
|
tls: TlsConfig
|
|
@@ -19,7 +19,7 @@ use std::any::Any;
|
|
|
19
19
|
use std::collections::HashMap;
|
|
20
20
|
use std::io::Seek;
|
|
21
21
|
use std::path::Path;
|
|
22
|
-
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
|
|
22
|
+
use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
|
|
23
23
|
use std::sync::{Arc, Mutex as StdMutex, Weak};
|
|
24
24
|
use std::time::{Duration, SystemTime};
|
|
25
25
|
use tokio::fs::{self, File, OpenOptions};
|
|
@@ -1017,6 +1017,12 @@ impl MessagePublisher for FilePublisher {
|
|
|
1017
1017
|
Ok(())
|
|
1018
1018
|
}
|
|
1019
1019
|
|
|
1020
|
+
/// A file is an ordered log by construction: appending batches in the order they
|
|
1021
|
+
/// were read is the whole point of exporting to JSONL/CSV.
|
|
1022
|
+
fn requires_ordered_publish(&self) -> bool {
|
|
1023
|
+
true
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1020
1026
|
fn as_any(&self) -> &dyn Any {
|
|
1021
1027
|
self
|
|
1022
1028
|
}
|
|
@@ -2007,15 +2013,44 @@ pub struct FileConsumer {
|
|
|
2007
2013
|
/// A live tail keeps waiting for the file to appear; a drain (`exit_on_empty`)
|
|
2008
2014
|
/// cannot, so it fails with this instead of blocking forever.
|
|
2009
2015
|
startup_open_error: Option<String>,
|
|
2016
|
+
/// Stamp `mqb.src.file_*` so an idempotent sink can name records in source order.
|
|
2017
|
+
source_metadata: bool,
|
|
2018
|
+
/// Index of the next record in this file.
|
|
2019
|
+
next_record: u64,
|
|
2020
|
+
/// Set only for the modes that do not start at byte 0, so their record indexes cannot
|
|
2021
|
+
/// collide with a previous run's. `None` for `consume`, whose names must repeat.
|
|
2022
|
+
run_epoch: Option<u64>,
|
|
2010
2023
|
exit_on_empty: bool,
|
|
2011
2024
|
}
|
|
2012
2025
|
|
|
2026
|
+
/// Hands out a strictly increasing run epoch. Seeded from wall-clock millis so epochs still
|
|
2027
|
+
/// sort across process restarts, but never repeats or goes backwards within one process —
|
|
2028
|
+
/// consumers built in the same millisecond, or across a clock step back, stay distinct.
|
|
2029
|
+
fn next_run_epoch() -> u64 {
|
|
2030
|
+
static LAST: AtomicU64 = AtomicU64::new(0);
|
|
2031
|
+
let now = SystemTime::now()
|
|
2032
|
+
.duration_since(std::time::UNIX_EPOCH)
|
|
2033
|
+
.map(|d| d.as_millis() as u64)
|
|
2034
|
+
.unwrap_or(0);
|
|
2035
|
+
let mut last = LAST.load(Ordering::Relaxed);
|
|
2036
|
+
loop {
|
|
2037
|
+
let epoch = now.max(last + 1);
|
|
2038
|
+
match LAST.compare_exchange_weak(last, epoch, Ordering::Relaxed, Ordering::Relaxed) {
|
|
2039
|
+
Ok(_) => return epoch,
|
|
2040
|
+
Err(observed) => last = observed,
|
|
2041
|
+
}
|
|
2042
|
+
}
|
|
2043
|
+
}
|
|
2044
|
+
|
|
2013
2045
|
impl FileConsumer {
|
|
2014
2046
|
fn wrap(backend: ConsumerBackend) -> Self {
|
|
2015
2047
|
Self {
|
|
2016
2048
|
backend,
|
|
2017
2049
|
path: String::new(),
|
|
2018
2050
|
startup_open_error: None,
|
|
2051
|
+
source_metadata: false,
|
|
2052
|
+
next_record: 0,
|
|
2053
|
+
run_epoch: None,
|
|
2019
2054
|
exit_on_empty: false,
|
|
2020
2055
|
}
|
|
2021
2056
|
}
|
|
@@ -2025,6 +2060,17 @@ impl FileConsumer {
|
|
|
2025
2060
|
let mut consumer = Self::new_backend(config).await?;
|
|
2026
2061
|
consumer.path = config.path.clone();
|
|
2027
2062
|
consumer.startup_open_error = startup_open_error;
|
|
2063
|
+
consumer.source_metadata = config.source_metadata;
|
|
2064
|
+
// `consume` always reads from byte 0, so its record index is reproducible and two
|
|
2065
|
+
// runs deliberately produce the same names — that is what makes the sink idempotent.
|
|
2066
|
+
// `subscribe` starts at the current end and `group_subscribe` resumes at a stored
|
|
2067
|
+
// byte offset, so their index restarts at 0 over records a previous run already
|
|
2068
|
+
// numbered. Without an epoch those names would collide and the sink would discard
|
|
2069
|
+
// the new records as already-covered; with one they stay distinct and ordered, at
|
|
2070
|
+
// the cost of cross-restart deduplication.
|
|
2071
|
+
consumer.run_epoch = (config.source_metadata
|
|
2072
|
+
&& !matches!(&config.mode, None | Some(FileConsumerMode::Consume { .. })))
|
|
2073
|
+
.then(next_run_epoch);
|
|
2028
2074
|
Ok(consumer)
|
|
2029
2075
|
}
|
|
2030
2076
|
|
|
@@ -2338,6 +2384,37 @@ impl MessageConsumer for FileConsumer {
|
|
|
2338
2384
|
// a cumulative byte offset (the max acked `file_offset`), so out-of-order
|
|
2339
2385
|
// commits could advance the offset past un-acked messages and lose them.
|
|
2340
2386
|
async fn receive_batch(&mut self, max_messages: usize) -> Result<ReceivedBatch, ConsumerError> {
|
|
2387
|
+
let mut batch = self.receive_batch_inner(max_messages).await?;
|
|
2388
|
+
if self.source_metadata {
|
|
2389
|
+
for message in &mut batch.messages {
|
|
2390
|
+
message
|
|
2391
|
+
.metadata
|
|
2392
|
+
.insert("mqb.src.file_path".to_string(), self.path.clone());
|
|
2393
|
+
message.metadata.insert(
|
|
2394
|
+
"mqb.src.file_record".to_string(),
|
|
2395
|
+
self.next_record.to_string(),
|
|
2396
|
+
);
|
|
2397
|
+
if let Some(epoch) = self.run_epoch {
|
|
2398
|
+
message
|
|
2399
|
+
.metadata
|
|
2400
|
+
.insert("mqb.src.file_epoch".to_string(), epoch.to_string());
|
|
2401
|
+
}
|
|
2402
|
+
self.next_record += 1;
|
|
2403
|
+
}
|
|
2404
|
+
}
|
|
2405
|
+
Ok(batch)
|
|
2406
|
+
}
|
|
2407
|
+
|
|
2408
|
+
fn as_any(&self) -> &dyn Any {
|
|
2409
|
+
self
|
|
2410
|
+
}
|
|
2411
|
+
}
|
|
2412
|
+
|
|
2413
|
+
impl FileConsumer {
|
|
2414
|
+
async fn receive_batch_inner(
|
|
2415
|
+
&mut self,
|
|
2416
|
+
max_messages: usize,
|
|
2417
|
+
) -> Result<ReceivedBatch, ConsumerError> {
|
|
2341
2418
|
// The path was unopenable at startup. A live tail waits for it (rotation,
|
|
2342
2419
|
// a writer that hasn't created it yet), but a drain has nothing to wait
|
|
2343
2420
|
// for, so re-probe and fail rather than block forever on a typo'd path.
|
|
@@ -2560,10 +2637,6 @@ impl MessageConsumer for FileConsumer {
|
|
|
2560
2637
|
}
|
|
2561
2638
|
}
|
|
2562
2639
|
}
|
|
2563
|
-
|
|
2564
|
-
fn as_any(&self) -> &dyn Any {
|
|
2565
|
-
self
|
|
2566
|
-
}
|
|
2567
2640
|
}
|
|
2568
2641
|
|
|
2569
2642
|
/// Wraps a message body for the Json/Text file formats. Generic over the payload type
|