fix(connectors): bound source forwarding channel with backpressure - #3795
Open
mlevkov wants to merge 1 commit into
Open
fix(connectors): bound source forwarding channel with backpressure#3795mlevkov wants to merge 1 commit into
mlevkov wants to merge 1 commit into
Conversation
|
Thanks for the PR. It is labeled Slash commands (own line, regular comment) move it around the queue:
See CONTRIBUTING.md for details. |
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #3795 +/- ##
=============================================
- Coverage 75.72% 17.28% -58.45%
Complexity 969 969
=============================================
Files 1322 1320 -2
Lines 159363 137264 -22099
Branches 132746 110724 -22022
=============================================
- Hits 120684 23727 -96957
- Misses 35041 113022 +77981
+ Partials 3638 515 -3123
🚀 New features to boost your workflow:
|
mlevkov
force-pushed
the
bounded-source-channel
branch
from
August 1, 2026 23:20
e5ecd55 to
f03fe1f
Compare
mlevkov
force-pushed
the
bounded-source-channel
branch
2 times, most recently
from
August 2, 2026 01:57
aa2a33f to
d7580d5
Compare
Contributor
Author
|
/request-review @hubcio |
This was referenced Aug 2, 2026
mlevkov
added a commit
to mlevkov/iggy
that referenced
this pull request
Aug 2, 2026
Iggy has no way to receive a webhook. Every provider that pushes events over HTTP needs something in front of it, and today that means running a separate service whose only job is to accept a POST and republish it. This connector removes that hop: it runs an embedded HTTP server, accepts authenticated POST bodies, and produces them to the instance's stream and topic as raw bytes. One plugin .so is loaded once no matter how many source entries reference it, so the listener cannot live on any single instance. It lives in a process-global registry keyed by listen address: the first open binds the public and admin ports, later opens validate their body limit, admin address, management token and instance name against the running listener before joining, and the last close releases both ports. Mismatches fail that instance's open rather than silently handing it a listener its configuration does not describe. A single port can therefore serve many providers, each routed to its own topic. Requests resolve against an ArcSwap route table that is rebuilt whole on every control-plane change, so one atomic load yields both the endpoint's auth rules and the destination bridge. Secret paths carry 128 bits in the URL itself, on the model of a Slack webhook, with optional bearer or HMAC on top; HMAC is verified over the raw body in constant time. Revoked endpoints answer 404 alongside paths that never existed, so a leaked URL cannot be used to confirm it was once live. Endpoints can be registered, re-keyed and revoked at runtime through a token-guarded API on the admin listener, because revoking a compromised endpoint is time-critical and provisioning one per tenant is inherently programmatic. Those endpoints ride the SDK's ConnectorState, and state is attached only to an empty batch: the runtime saves state solely on the success branch of the Iggy send, and an empty send always succeeds, so a mutation cannot be lost to an unrelated send failure. Revocation writes a tombstone that outranks TOML on restore, so a stale config file cannot resurrect an endpoint an operator revoked. Delivery is best-effort in both directions and the README says so first, before anything else: HTTP 200 means accepted into an in-memory buffer, and both the loss and duplicate windows are enumerated with what mitigates each. A full bridge answers 429 with Retry-After rather than blocking, since holding the connection open would turn a slow Iggy into a retry storm. Gateway metrics on the admin listener cover accept-to-200 latency, which the runtime's own stage histograms begin too late to see. Part of the webhook gateway design accepted in apache#3039. The backpressure chain is only complete once the bounded runtime forwarding channel from apache#3795 lands; until then a full bridge signals an arrival burst rather than a slow Iggy, which the README documents. Co-authored-by: Claude <[email protected]>
The channel between a source plugin's send callback and the runtime's forwarding loop was flume::unbounded(), so a slow or hung Iggy meant batches accumulated without bound instead of propagating backpressure into the plugin's polling loop. Swap it for a bounded crossfire channel (the shard and server-ng standard), sized by an optional SourceConfig channel_capacity counted in batches, defaulting to 1024. The FFI callback retries with send_timeout while re-reading a shutdown flag, set by the manager before iggy_source_close and for every instance ahead of the sequential process-shutdown stops, since same-library instances share one plugin runtime and a wedged sibling would otherwise hold a worker an earlier close needs. A unit test pins that buffered batches drain after the senders drop, which shutdown relies on and crossfire's docs do not promise. This drops flume from the runtime. Requested in the HTTP source discussion (apache#3039). Co-authored-by: Claude <[email protected]>
mlevkov
force-pushed
the
bounded-source-channel
branch
from
August 2, 2026 21:39
d7580d5 to
bd19da2
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The channel between a source plugin's send callback and the runtime's forwarding loop was
flume::unbounded(), so a slow or hung Iggy meant batches accumulated in memory without bound instead of propagating backpressure into the plugin's polling loop. This is the prerequisite runtime fix requested in the HTTP source discussion (#3039), and it applies to every source connector, including the source PRs currently in flight.What changed
crossfire::mpsc::bounded_blocking_async), the same shapeshardandserver-nguse. flume is no longer a runtime dependency.SourceConfigfield,channel_capacity, counted in batches (a single batch can be megabytes), defaulting to 1024 and clamped to [1, 65536] since crossfire eagerly allocates the ring and asserts capacity < 2^31. The existingConfigEnvderive providesIGGY_CONNECTORS_SOURCE_<KEY>_CHANNEL_CAPACITY; configs without the field behave as before apart from the bound.try_sendfast path and asend_timeout(10ms)retry loop that re-reads a per-instance shutdown flag between waits. The manager sets that flag beforeiggy_source_closeso a hung Iggy cannot deadlock the close. Process shutdown sets every instance's flag (signal_shutdown_all) before the sequential stops, because instances loaded from one plugin library share a single tokio runtime and a wedged sibling would otherwise hold a worker an earlier close needs.warn!per backpressure episode (latched, cleared on genuine recovery). A batch that still cannot be enqueued after the stop signal is dropped and counted iniggy_connector_errors_total.One correction to the discussion notes
@hubcio the spec assumed crossfire's blocking sender has no
send_timeout. It does:blocking_tx.rs:288onTx, reachable fromMTxviaDeref. The loop is built on it instead oftry_sendplus sleep, so the sender wakes as soon as capacity frees while shutdown latency stays bounded by the retry interval.Known limitation
Stopping a single connector via the runtime API while enough same-library sibling instances are saturated can delay that close until the siblings drain, because the callback parks a worker of the shared plugin runtime. The code comment and the connector skill document this. The complete fix is an SDK-side worker handoff (
tokio::task::block_in_placearound the callback invocation); happy to file it as a follow-up issue.Test plan
cargo clippy -p iggy-connectors --all-targets -- -D warningscleancargo test -p iggy-connectors: 128 passed, including the new channel and shutdown testscargo build -p iggy_connector_stdout_sink -p iggy_connector_random_sourcecargo test -p integration -- connectors::runtime::could not run on this machine (hwlocality-sysneedspkg-config); relying on CI for the integration suite