Skip to content

Releases: SAY-5/conduit

v6.0.0

Choose a tag to compare

@SAY-5 SAY-5 released this 28 Sep 18:59
42e2c98

This is a major release because four things an existing caller could depend on have changed. Worker takes its quarantine queue as a required keyword argument; in 5.0.1 it defaulted to None, and a worker built that way reported a payload that failed its schema or mapping rules as quarantined, deleted it from the work queue and sent it nowhere. SqsQueue.delete_batch and SqsQueue.purge, which nothing in the repository called, are removed together with conduit.core.retry.backoff_schedule, and SqsQueue.client exposes the boto3 client the handle wraps. Log lines from every CLI command are written to stderr, where in 5.0.1 structlog's default logger printed them to stdout, and the stream is looked up at write time so an in-process run survives typer's CliRunner swapping it; dlq list and dlq replay configure logging like the other commands. Envelope.submitted_at is an AwareDatetime, so a producer that enqueues a naive timestamp, which 5.0.1 accepted and delivered, now has its message treated as malformed.

A queue body that does not parse as an envelope no longer stops the worker. SqsQueue.receive logs queue.invalid_message with the queue name and the message id, never the body, leaves the message unacknowledged for the redrive policy, and returns the valid messages from the same response as a MessageBatch that also records how many messages arrived, so a response holding nothing valid is not mistaken for an idle poll. In 5.0.1 the validation error raised out of receive and ended Worker.run.

The circuit breaker's half-open probe is a lease that only a delivery attempt can hold. The breaker gate now comes after the idempotency claim, so a duplicate or a key held by another worker is answered before the probe is spent; CircuitBreaker.abandon_probe hands back a probe whose delivery reached no verdict, and the worker calls it after a retry loop that exhausted on 429; and a message the breaker turns away has its claim released and is held for the larger of the queue's visibility timeout and the time left on the breaker. In 5.0.1 the gate came first. With a duplicate as the first message after the recovery window, the breaker took it as the probe, the duplicate returned without a verdict, and the worker turned every later message away with a one second visibility hold each time, an hour later still, without calling the target again: a script driving the 5.0.1 worker through that sequence shows two adapter calls in total, where 6.0.0 makes five and closes the breaker. tests/unit/test_worker.py pins the duplicate, the claim held elsewhere, the probe exhausted on 429 and the visibility hold, a random walk of 5,000 steps in tests/unit/test_breaker.py checks that a closed breaker always admits, an open one never does and a half-open one admits exactly one probe whenever none is in flight, and tests/integration/test_backpressure.py proves against LocalStack that an outage which recovers into 429s still drains both tasks.

A status row write that DynamoDB refuses is logged as status.publish_failed and tried again on the next tick; in 5.0.1 the ClientError raised out of Worker.run. The fakes take retry_after_seconds, from FAKE_RETRY_AFTER_SECONDS or the faults endpoint, and send it as Retry-After on every 429 they inject, so test_retry_after_pauses_the_connector_not_just_the_message now asserts that a second task on the connector reaches the fake at least that many seconds after the header, where the 5.0.1 test only counted the header as honoured. tests/integration/test_concurrency.py runs two workers on one queue and asserts 20 deliveries and 10 deduplications for 30 messages, a key in progress elsewhere re-polled and then deduplicated, and a claim abandoned with a two second lease taken over once it expires. make test-unit and CI fail below 85 percent line coverage of conduit/, and the suite reports 86.98 percent at this commit.

make demo now submits 302 tasks: 240 unique, 60 resubmits and two payloads their source schema rejects, a priority outside the enum in schemas/jira-support/v2.yaml and a status outside the one in schemas/webhook-crm/v1.yaml, and the block reports both held in quarantine with their notes while the ops table's quar column reads 1 for each of those connectors. The run first clears quarantined payloads an earlier run left behind, because they are never acknowledged, derives the length of the connector YAML it writes, which is 8 lines where the 5.0.1 block said 10, and stamps its commit, package version, LocalStack image and date on its second line. demo/check_readme.py, run by make readme-check and by CI, fails when that commit is not in the repository or not an ancestor of HEAD, when the version or the image no longer match, and when the block reports a MISMATCH or carries no must-equal checks, and it lists the commits that have touched the measured code since. The block in README.md is run D930E66 at commit f008099, conduit 6.0.0, localstack/localstack:3.8, 2026-09-28. ARCHITECTURE.md describes the worker loop as it runs; the 5.0.1 copy still said a misfit was deleted from the queue as delivery.rejected, the v2 behaviour that v4 replaced with quarantine.

The browser demo reads its configuration from the repository. scripts/embed-config.mjs writes connectors/.yaml, schemas//v*.yaml and the Adapter class into web/src/sim/config.generated.ts, npm run embed:check fails when they disagree and CI runs it, and the simulation parses those files rather than the simplified copies it carried; that brought the mapping stage the port lacked, so a rule violation lands in quarantine with stage mapping as it does in conduit/core/mapping.py. Each of the 49 self-check lines prints both operands of every comparison it makes with the relation that holds between them, so a line cannot say ok beside a predicate it did not test; the idempotency key is compared with the digest conduit/core/idempotency.py produces for the same input, and each retry delay with the ceiling of the attempt it followed. run reproducible hashes the summary together with every value reachable from the engine and requires the same hash from a fresh engine at the same seed and from that engine run again after reset(), which is what Run again does on the page, and it fails when any module in src/sim names Math.random, Date.now, performance.now or new Date. That check exposed that reset() left the rng where the previous run had ended and the fakes counting across runs: on 5.0.1 a second run on the same engine reports the jira fake returning 429 120 times instead of 60, with different delays and latencies, and on 6.0.0 the two runs are identical.

CI gained the web job, runs make readme-check after the unit suite, pins uv 0.11.7 through setup-uv v6, checks out the full history so the ancestry check can run, and accepts workflow_dispatch. At this commit the unit suite is 185 tests at 86.98 percent line coverage, the LocalStack suite 32 tests, the terraform suite 3, and the web self check 49 assertions, all passing in CI run 36468470785.

v5.0.1

Choose a tag to compare

@SAY-5 SAY-5 released this 14 Sep 22:11

Every worker now sets conduit_queue_depth for its own connector's work, dead letter and quarantine queues, visible and in flight, with the same labels conduit ops uses, so a Prometheus scrape of a worker shows queue depth as the v5.0.0 notes claimed.
The gauge is refreshed after a poll at most once every 10 seconds, three GetQueueAttributes calls per refresh, and those calls are counted in the run's SQS requests.
A unit test drives an idle worker against a stubbed SQS client to prove the gauge is set and the interval is respected, and a LocalStack test scrapes a running worker's /metrics endpoint and sees dead letters and a quarantined payload appear after they are enqueued.
The browser demo now plans 8 resources for a new connector, including module.queue.aws_sqs_queue.quarantine, and 26 for the shipped base stack, matching the real Terraform plan, and its dead letter lab sends a malformed record to the quarantine queue rather than the DLQ.
Tests are 168 unit and 31 LocalStack plus terraform, and the web self check runs 44 assertions.

v5.0.0

Choose a tag to compare

@SAY-5 SAY-5 released this 14 Sep 21:30

Every worker now publishes a status row into the idempotency table under status|, refreshed at most every five seconds and once more when it stops. The row carries the run it belongs to, what that run has delivered, deduplicated, quarantined and dead-lettered, how many SQS requests and DynamoDB items it has spent, the breaker state, the tokens left in its bucket and its last error.
conduit ops summary joins those rows to live queue depths read from SQS and prints one line per connector: queue and in-flight depth, dead-letter and quarantine depth, throughput per minute, throttle state, breaker state and last error. Because the state comes from the table rather than a metrics port, the command needs to know neither where the workers run nor how to reach them. A connector whose worker has published nothing reads no worker seen, which distinguishes one that never started from one that is merely idle.
conduit ops costs reports what a run actually bought: remote objects created, SQS requests, and DynamoDB writes and reads, priced at the us-east-1 on-demand list rates kept in one table in the source. The counters are per run rather than cumulative, because each worker records where the store and queue counters stood when it started and reports the difference, so restarting a worker starts a new bill.
Queue depth and billable units are also Prometheus gauges beside the delivery counters from earlier versions, and make demo prints both ops blocks at the end of its run with the collected rows written into demo/out/details.json.
Tests are 167 unit and 30 LocalStack plus terraform. The ops tests seed a connector with three delivered tasks, one dead letter, one quarantined payload and one still queued, then assert those exact depths, the thirteen DynamoDB writes and three reads behind the bill, and the rendered lines themselves.

The claim above that queue depth is a Prometheus gauge beside the delivery counters held only for the conduit ops process, not for a worker's /metrics endpoint, and was corrected in v5.0.1, where every worker refreshes the gauge for its own queues.

v4.0.0

Choose a tag to compare

@SAY-5 SAY-5 released this 13 Sep 23:18

Each connector can now declare what its producer promised to send, in schemas//v.yaml, loaded as an ordered registry. Fields are checked as they arrive rather than coerced, dotted paths like fields.region work, and the first violation wins.
Loading the registry refuses a breaking change between consecutive versions. A version may loosen what it accepts and never tighten it, so making a field required, narrowing a type, introducing an enum or dropping values from one, and introducing or lowering a max_length are all rejected, while dropping a field, relaxing required, widening integer to number, adding enum values and adding an optional field are accepted. conduit schema check prints the registry and exits 2 on a break.
There is a third queue per connector, conduit--quarantine, created by Terraform beside the work queue and the dead-letter queue. A payload that fails the schema or the mapping rules is moved there with a note recording the stage, field, reason and detail, instead of being acknowledged and dropped the way v2 did it. The dead-letter queue keeps its old meaning: deliveries the handler could not complete, replayed when the target is healthy again. Neither queue ever feeds the other.
conduit quarantine list shows what is set aside and why, and conduit quarantine redrive puts it back once the schema or the producer is fixed, keeping the idempotency key and clearing the note so a fixed task still lands exactly once. Redrive only moves what the queue held when it started, because a worker that still rejects the payload re-quarantines it inside the call.
conduit_rejected_total is replaced by conduit_quarantined_total with stage and reason labels, the worker stat rejected by quarantined, and DeliveryStatus.REJECTED by QUARANTINED. Tests are 159 unit and 23 LocalStack plus terraform, covering the compatibility rule in both directions, a bad payload quarantined rather than dead-lettered, a mapping failure carrying its own stage, and a quarantined message redriven and delivered after the fix.

v3.0.0

Choose a tag to compare

@SAY-5 SAY-5 released this 11 Sep 19:16

Every connector now has its own token bucket, so the burst goes out at once and the rest of a backlog is spaced one over the configured requests per second apart instead of escaping in one batch. A 429 carrying Retry-After pauses every send on that connector rather than just the message that was throttled, capped by max_retry_after_seconds so a hostile header cannot stall a worker.
Sustained 5xx responses, timeouts, and connection errors feed a per-connector circuit breaker, which is a different signal from being throttled, so 429 never opens it. Once open the worker stops polling and keeps extending the visibility timeout on anything it is already holding, which means an outage cannot hand a message to a second consumer or burn receive count into the dead-letter queue. After the recovery window one probe decides whether polling resumes or the breaker opens again.
Rate limit waits, honoured Retry-After responses, breaker state, opens, and paused seconds are all on /metrics and in the worker's stop log line. The fakes gained an outage fault that fails every call with 503 until it is cleared, which is how the new LocalStack tests reproduce a downstream being down.
Tests are 129 unit and 17 LocalStack plus terraform: the bucket holds its rate over a window at three different rate and burst settings, six real sends at five per second arrive across a one second window, an outage pauses the worker so it consumes nothing and then drains cleanly once cleared, and a message held across a pause is never redelivered even though the queue's visibility timeout is shorter than the pause.

v2.0.0

Choose a tag to compare

@SAY-5 SAY-5 released this 08 Sep 23:20

Connector mappings are now typed rules instead of bare field names: each remote field can declare a source path or template, a type, required, enum, default, max_length, and truncate. Defaults and constants fill gaps, values are coerced, and overlong strings are cut or refused per rule.

The worker checks every task against the rules before it claims an idempotency key. A task that cannot fit is acknowledged without delivery, logged with the field and reason, and counted in conduit_rejected_total{reason}, so a bad payload no longer burns retries and lands in the DLQ. conduit submit applies the same check and refuses the offending tasks before they are enqueued.

112 unit tests (26 new), integration and Terraform suites unchanged.

v1.0.0

Choose a tag to compare

@SAY-5 SAY-5 released this 08 Sep 23:11

First tagged release of the connector kit. One adapter interface covers Slack, Jira, and signed webhooks, with idempotency keys claimed through DynamoDB conditional puts, full-jitter backoff, and an SQS queue plus dead-letter queue per connector with replay. Terraform turns each connector YAML into its queue pair, IAM role, and SSM placeholders through for_each, and everything runs against LocalStack locally.

86 unit tests, 8 LocalStack integration tests, and 3 Terraform plan tests.