Skip to content

fix(connectors): close the source instance a failed start leaves behind - #4064

Merged
spetz merged 20 commits into
apache:masterfrom
mlevkov:runtime-source-start-cleanup
Sep 12, 2026
Merged

fix(connectors): close the source instance a failed start leaves behind#4064
spetz merged 20 commits into
apache:masterfrom
mlevkov:runtime-source-start-cleanup

Conversation

@mlevkov

@mlevkov mlevkov commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Closes #4062.

The leak

SourceManager::start_connector takes a fresh plugin_id, calls init_source, and records that id on SourceDetails only after the handler tasks are spawned. In between, the instance exists inside the plugin and nothing outside it knows the id: stop_connector closes whatever details.info.id holds, which is still the previous instance. setup_source_producer returning early through ? therefore stranded the new one for the life of the process. source::init already cleaned up on the identical failure, which is the asymmetry @hubcio pointed at.

For a plugin whose open only allocates, the orphan is wasted memory. For one that takes a process-global resource it is a live fault: a shared listener stays bound and answering into a queue nothing drains, and every retried restart then fails on the identity the orphan never released.

A guard, not a cleanup branch

This deviates from the fix in the review, which was to mirror source::init's error arm at the call site, so it is worth saying why rather than leaving it to be found.

The window is defined by the two statements that open the instance and record its id, not by which call between them happens to be fallible today. A cleanup branch is correct only for the one ? that exists now, and silently wrong for the next one somebody adds. SourceInstanceGuard is armed at init_source and disarmed once the id is recorded, so every path out of that window closes the instance, including a panic.

It also made the behaviour testable. Container<SourceApi> only comes from dlopen, so start_connector cannot be exercised in a unit test at all, while a guard holding the bare extern "C" fn can be driven directly.

The close-and-report itself is now one function shared with source::init, so the two sites cannot drift. source::init keeps its existing control flow; only the duplicated body moved.

No cleanup_sender on this path: spawn_source_handler is what registers the sender, and it has not run yet.

Tests

Three, each mutation-checked, each mutant confirmed to compile first:

  • an armed guard closes, and closes its own id, since closing another would leave this instance open and tear down a live one
  • a disarmed guard does not close, or a source that just started successfully would be torn down
  • a refused close (-1, the code the SDK returns for an unknown id) is reported and not propagated, because unwinding out of drop would be worse than the leak it is cleaning up after

Each test owns its stub and statics rather than sharing a pair, which would have made two of them race in the same process.

What is not covered, and why

The guard's placement in start_connector has no test. I verified that rather than assuming it: disarming the guard immediately after construction restores the original leak, compiles, and the suite still passes.

Reaching that path needs a real Container, so it cannot be a unit test, and the only route into start_connector is POST /sources/{key}/restart. Making setup_source_producer fail there means either a config the local provider will serve on restart but not at boot, which today works only because of the version selection in #3848 and would break when that is fixed, or stopping the broker mid-test. Both couple this regression test to something unrelated to it, so I left it out rather than write a test that fails for the wrong reason later. Happy to add either if you would rather have the coverage than the independence.

source::init's cleanup remains covered only by error_isolation.rs asserting the connector reports Error, which it did before this change too.

Verification

cargo fmt, cargo sort --no-format, clippy at both feature sets, rustdoc under -D warnings, 198 unit tests in iggy-connectors, and stdout_sink + random_source still build.

@github-actions

github-actions Bot commented Sep 5, 2026

Copy link
Copy Markdown

Thanks for the PR. It is labeled S-waiting-on-review and queued for review.

Slash commands (own line, regular comment) move it around the queue:

  • /ready - back to S-waiting-on-review after addressing feedback
  • /author - flip to S-waiting-on-author while you finish changes
  • /request-review @user-or-team - request a reviewer
  • /pin - exempt the PR from the stale bot, /unpin to undo

See CONTRIBUTING.md for details.

@github-actions github-actions Bot added the S-waiting-on-review PR is waiting on a reviewer label Sep 5, 2026
@mlevkov

mlevkov commented Sep 5, 2026

Copy link
Copy Markdown
Contributor Author

/ready

@mlevkov

mlevkov commented Sep 5, 2026

Copy link
Copy Markdown
Contributor Author

/request-review @hubcio

@github-actions
github-actions Bot requested a review from hubcio September 5, 2026 06:33
@codecov

codecov Bot commented Sep 5, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.01449% with 11 lines in your changes missing coverage. Please review.
✅ Project coverage is 67.43%. Comparing base (262117c) to head (f21a32f).

Files with missing lines Patch % Lines
core/connectors/runtime/src/source.rs 96.10% 6 Missing ⚠️
core/connectors/runtime/src/manager/source.rs 97.00% 3 Missing ⚠️
core/connectors/sdk/src/sink.rs 66.66% 1 Missing ⚠️
core/connectors/sdk/src/source.rs 66.66% 1 Missing ⚠️
Additional details and impacted files
@@              Coverage Diff              @@
##             master    #4064       +/-   ##
=============================================
- Coverage     86.71%   67.43%   -19.28%     
  Complexity     1455     1455               
=============================================
  Files          1267     1267               
  Lines        212095   175633    -36462     
  Branches     177320   140858    -36462     
=============================================
- Hits         183913   118445    -65468     
- Misses        23705    52608    +28903     
- Partials       4477     4580      +103     
Components Coverage Δ
Rust Core 63.52% <96.01%> (-24.17%) ⬇️
Java SDK 67.58% <ø> (ø)
C# SDK 76.91% <ø> (ø)
Python SDK 91.34% <ø> (ø)
PHP SDK 85.65% <ø> (ø)
Node SDK 96.24% <ø> (ø)
Go SDK 69.40% <ø> (ø)
Files with missing lines Coverage Δ
core/connectors/runtime/src/main.rs 87.16% <100.00%> (-0.17%) ⬇️
core/connectors/runtime/src/metrics.rs 99.16% <100.00%> (ø)
core/connectors/runtime/src/sink.rs 76.33% <100.00%> (-2.95%) ⬇️
core/connectors/sdk/src/sink.rs 76.60% <66.66%> (-0.18%) ⬇️
core/connectors/sdk/src/source.rs 90.70% <66.66%> (-0.37%) ⬇️
core/connectors/runtime/src/manager/source.rs 92.60% <97.00%> (+0.62%) ⬆️
core/connectors/runtime/src/source.rs 84.48% <96.10%> (-1.07%) ⬇️

... and 394 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@hubcio hubcio left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a few things outside the diff, none of them blocking this PR:

  • core/connectors/sdk/src/source.rs:597 - iggy_source_open inserts into INSTANCES even when open() failed, and SourceContainer::open stores the source before returning 1. a plugin whose open() bound a listener and then errored keeps it for the life of the process. same leak class as this PR, one statement earlier. the fix is to skip the insert and drop the container, not to close from the runtime side - the SDK already stored the source, so that would run Source::close() on an instance that never opened.
  • core/connectors/runtime/src/source.rs:71 - SOURCE_SENDERS being a process global is why an orphaned forwarding loop can't die. the sink owns its watch::Sender in SinkDetails, so its identical window self-heals. an RAII registration stored next to handler_tasks would fix the source side properly.
  • core/connectors/runtime/src/manager/sink.rs:200-223 - same unrecorded-instance window on the sink path, details.info.id only set at 223. narrower than the source case, since the consume tasks exit when the watch::Sender drops, so the same guard alone is enough there.
  • core/connectors/runtime/src/manager/source.rs:186-190 - stop_connector never clears details.info.id, so after a failed start every later stop re-closes a dead id and line 168 logs "Closed" for it. the shutdown sweep hits this too.
  • core/connectors/runtime/src/manager/source.rs:167 - the stop path drops the iggy_source_close result and logs "Closed" unconditionally. a -1 there means teardown was skipped while the INSTANCES entry is already gone, so nothing can retry.
  • core/connectors/runtime/src/manager/source.rs:311 - a failed restart leaves the connector Stopped with last_error cleared, so GET /sources shows nothing wrong. one set_error on start_connector(..).await? covers all five fallible steps.
  • core/connectors/runtime/src/source.rs:384-427 - setup_source_producer builds and init()s a producer per configured stream but keeps only the last, so two configured streams silently produce to one.

Comment thread core/connectors/runtime/src/manager/source.rs Outdated
Comment thread core/connectors/runtime/src/manager/source.rs Outdated
Comment thread core/connectors/runtime/src/source.rs Outdated
Comment thread core/connectors/runtime/src/manager/source.rs Outdated
Comment thread core/connectors/runtime/src/source.rs Outdated
Comment thread core/connectors/runtime/src/source.rs
Comment thread core/connectors/runtime/src/source.rs Outdated
Comment thread core/connectors/runtime/src/source.rs Outdated
Comment thread core/connectors/runtime/src/source.rs
Comment thread core/connectors/runtime/src/source.rs Outdated
@github-actions github-actions Bot added S-waiting-on-author PR is waiting on author response and removed S-waiting-on-review PR is waiting on a reviewer labels Sep 5, 2026
@mlevkov
mlevkov force-pushed the runtime-source-start-cleanup branch from 1d20ef5 to e88133e Compare September 8, 2026 01:54
@mlevkov
mlevkov force-pushed the runtime-source-start-cleanup branch from e88133e to eed068f Compare September 8, 2026 18:26
@mlevkov

mlevkov commented Sep 8, 2026

Copy link
Copy Markdown
Contributor Author

All 18 inline findings are in, at 898c6a9a1. Ten commits, one per finding
where that made sense. Gate green: fmt, sort, clippy at both feature sets,
rustdoc -D warnings, --locked, 212 unit tests (was 206) over three clean
runs, plus 64 connectors integration tests across runtime, api, random,
stdout, http_config_provider and postgres.

batch findings commit
A3 close pointer tied to its library 1f2c2a490
A2 and its follow-up teardown off the worker 20d03bc7d
A1 cancelled start leaking both tasks a1722c5bf
A4 the sources_running ratchet abacd38d2
B1-B4 the guard tests cbc4a31e0
C1, C5, C8 one close for a plugin instance 0dcf36957
C2, C3, C4, C6 naming, must_use, docs 9c7bb0d21
A4 follow-up a gauge test, and what it showed d90b72fa5
A1 follow-up an await in the start window is now a build error 0c4b859f8
@spetz the status transition and the gauge move together 898c6a9a1

You were right that the fix was incomplete both ways. A1 is a bit worse than
"the tasks are left behind": dropping the JoinHandles detaches them, so the
forwarding loop keeps running against a channel nothing feeds. The lock now
comes before the spawn, so nothing can await between registering the tasks and
recording the id that reaches them.

Rebased onto master twice since this was posted, so the shas above are the
current ones. A tenth commit answers @spetz's review; see the reply below.

Five places I did not do exactly what you asked

A3, the Arc. Held as Arc<dyn Fn(u32) -> i32 + Send + Sync>, built by a
for_container constructor that captures the Arc<Container<SourceApi>>,
rather than as an Arc<Container<SourceApi>> field. A Container only exists
via dlopen, so a guard carrying that field cannot be built in a unit test at
all, and B1 and B2 ask for more guard tests, not fewer. The single production
constructor takes the container, so the ownership stays enforced by the API.
Same lifetime tie, still testable.

A2, both of your options rather than one. drop defers to the blocking
pool, and the error arms await an explicit close(). Deferring on its own costs
the ordering you named, and that ordering matters here: a restart retried inside
the teardown window would collide with the instance being torn down, which is
the failure #4062 exists to prevent. drop stays the net for a cancellation and
for any ? added in the window later.

A4, reported rather than deleted. Dropping the status write outright leaves a
connector that just started answering Stopped to GET /sources until the
forwarding loop takes the lock. start_connector now reports through
update_status, so the gauge still has exactly one owner.

C7, the key field. The lifetime param and the impl<'a> are gone, which
was the objection. key stays, as an owned String: now that the close can be
deferred, its warn! is emitted outside the caller's log context, so the key is
the only thing tying that line to a connector. Happy to drop it for plugin_id
alone if you would rather.

C1, not the stop path. You noted the helper would also suit
manager/source.rs:167. That path drops the iggy_source_close result and logs
"Closed" regardless, which is your own out-of-diff finding, so wiring it in here
would fix a separate bug inside this PR.

What the integration tests do and do not reach

They do reach more than I first credited them with. source_with_invalid_config
in error_isolation fails inside setup_source_producer, which is the arm the
guard now sits on in init, and random_source_produces_messages is what would
break if the disarm() in the other arm were wrong. For the restart path,
given_restart_when_state_exists_should_resume_from_served_state and the two
CDC restart tests POST /sources/{key}/restart and then wait for the source to
reload state and resume, so A1's restructure is exercised end to end.

Two things are still not pinned by any test:

  • A1's cancellation window. Nothing can drop the start_connector future at
    that one await, so it still has no test. It is no longer held by a comment
    either: SourceDetails::record_started takes the spawn as a closure and is
    deliberately not async, so an await added between the spawn and the id
    record does not compile. Checked by adding one, error[E0728]. Same argument
    you made for #[must_use], applied to the window rather than the guard. It
    also made the step testable, which nothing in that region was.

  • A4's arithmetic. There is now a test for it, and it is worth telling you
    what it showed. sources_running_does_not_climb_across_restarts restarts the
    random source three times and requires the gauge back at 1. It passes, but it
    does not isolate the fix: revert the report through update_status and it
    still passes 3 of 3, because taking the lock before the spawn already makes
    start_connector win the race to that lock. Revert that too, so the code is
    what you reviewed, and it still passes 6 of 6. start_connector takes an
    uncontended mutex immediately, while the spawned loop has to be scheduled and
    walk a DashMap to reach the same lock, so it does not get there first.

    So the double count is real in the code and, as far as I can make it behave,
    latent. The fix stands on not wanting correctness to rest on that lock
    ordering, not on a ratchet I could reproduce. Said plainly rather than let the
    commit imply I had seen it climb.

Everything else was mutation-checked, with each mutant confirmed to compile
first: closing inline instead of deferring, close() not awaiting (10 rounds of
10), disarm() moved ahead of the fallible step, a guard that never arms, the
transition guard removed from update_status, and a bare
SourceInstanceGuard::new(..); statement, which #[must_use] now rejects at
build time.

Out of diff

Filed the multi-stream producer bug as #4097. setup_source_producer init()s a
producer per [[streams]] entry and keeps only the last, so two configured
streams silently produce to one. Both auto-create flags default on, so every
configured stream and topic really is created and all but the last stay empty,
which is what makes it hard to spot. The other six are not filed yet.

@mlevkov

mlevkov commented Sep 8, 2026

Copy link
Copy Markdown
Contributor Author

/ready

@mlevkov

mlevkov commented Sep 8, 2026

Copy link
Copy Markdown
Contributor Author

/request-review @hubcio

Comment thread core/connectors/runtime/src/manager/source.rs Outdated
@github-actions github-actions Bot added S-waiting-on-author PR is waiting on author response and removed S-waiting-on-review PR is waiting on a reviewer labels Sep 9, 2026
Closes apache#4062.

`start_connector` allocates a fresh plugin id, calls `init_source`, and only
records that id on `SourceDetails` once the handler tasks are spawned. In
between, the instance exists inside the plugin and nothing outside it knows
the id: `stop_connector` closes whatever `details.info.id` holds, which is
still the previous instance. `setup_source_producer` returning early through
`?` therefore stranded the new one for the life of the process, while the
boot path in `source::init` cleaned up on the identical failure.

For a plugin whose open only allocates, the orphan is wasted memory. For one
that takes a process-global resource, it is a live fault: a shared listener
stays bound and answering into a queue nothing drains, and every retried
restart then fails on the identity the orphan never released.

A guard rather than a cleanup branch at the one call that can fail today,
because the window is defined by the two statements that open and record the
instance, not by which call between them happens to be fallible. Adding a `?`
inside it stays correct. The close-and-report itself is shared with the boot
path so the two cannot drift.
The guard held a bare `extern "C" fn` read out of a dlopened `.so`. Nothing
tied it to the `Container` that owns the mapping. It worked only because
`container` is declared before the guard and therefore drops after it. A
declaration order dependency is thin support for an FFI call into a loaded
library, and it stops being enough at all once the call is deferred off the
calling thread.

`for_container` now captures the `Arc<Container<SourceApi>>` inside the closure
the guard calls, so the mapping is owned for as long as the close is reachable.

The field is `Arc<dyn Fn(u32) -> i32 + Send + Sync>` rather than an
`Arc<Container<SourceApi>>`. A `Container` only exists via dlopen, so a guard
carrying that field cannot be constructed in a unit test at all, and this guard
needs more tests rather than fewer. The single production constructor takes the
container, so the ownership stays enforced by the API and not by convention.

`close_failed_source` now takes one callable shape. An `extern "C" fn` does not
implement `Fn`, so the boot path wraps its pointer at the call site.

The doc comment claimed the plugin id was "durably recorded". `SourceDetails` is
memory only and the id never reaches the state store, so it now says recorded on
`SourceDetails`.
`SourceContainer::close` drives the plugin's own `close()` under `block_on`, and
`block_on(handle)` before it. Calling that from the guard's `drop` put an
unbounded plugin teardown on a tokio worker, in the one place no timeout can
ever be added, because drop glue cannot await.

`drop` now hands the close to the blocking pool. The closure it carries owns the
container, so the library stays mapped until the call returns. That is what
makes deferring safe.

Deferring costs the ordering between teardown and the returned error, so the
known failure arm no longer relies on `drop`. `setup_source_producer`'s error arm
awaits `close()`: off the worker and ordered, so by the time the error reaches an
operator the instance is gone and a restart retried straight away cannot collide
with it. `drop` stays the net for a cancellation and for any `?` added in the
window later.

`Handle::try_current` picks between them. A guard dropped outside a runtime has
no worker to protect and nothing to hand the work to, so it closes inline. That
is also the path the plain `#[test]` cases take.

The deferred test asserts the close lands on a different thread from the one that
dropped the guard, which is the only observable difference between handing it
over and running it inline. The ordered test asserts exactly one call after
`close()` returns, so the teardown was awaited and the following drop did not
repeat it.

`instance` became `instance_guard` while these lines were being rewritten. It
holds a guard, and `instance.disarm()` read as disarming the instance.
The guard closed the plugin instance but could not reach the runtime half of the
same leak. `spawn_source_handler` registers the `SOURCE_SENDERS` entry and spawns
both tasks, and the id that reaches them was recorded only after the next
`details.lock().await`. A cancellation in that gap, which is what a client
disconnect dropping the axum handler future does, left the entry and both tasks
behind with nothing naming them, so the forwarding loop ran for the life of the
process.

The lock is now taken before the spawn, so the spawn and the id record sit in one
block with no await between them. `spawn_source_handler` is synchronous, so
holding the lock across it costs a spawn and nothing else. The forwarding loop's
own first act is to take the same lock, so it waits for the block to end instead
of racing it.

No test covers this. Reaching it means cancelling the future at one specific
await with a dlopened container and a live broker in place, and both integration
routes were already rejected for this PR for reasons that still hold. What
enforces it is that the block contains no await, and an await added inside it
would reopen the window silently.
@mlevkov

mlevkov commented Sep 10, 2026

Copy link
Copy Markdown
Contributor Author

/ready

@github-actions github-actions Bot added S-waiting-on-review PR is waiting on a reviewer and removed S-waiting-on-author PR is waiting on author response labels Sep 10, 2026
@mlevkov

mlevkov commented Sep 10, 2026

Copy link
Copy Markdown
Contributor Author

/request-review @spetz

@github-actions
github-actions Bot requested a review from spetz September 10, 2026 22:28
spetz
spetz previously approved these changes Sep 12, 2026

@hubcio hubcio left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: core/connectors/runtime/src/manager/sink.rs:207 - this pre-existing setup failure strands the opened sink. guard it, await cleanup on failure, and keep task spawning and id registration under the manager lock before disarming.

warning: core/connectors/runtime/src/manager/source.rs:162 - this pre-existing close call blocks an async worker. use spawn_blocking with the library owner, keeping the restart lock and remaining cleanup alive if the caller is cancelled.

warning: core/connectors/runtime/src/manager/source.rs:183 - this pre-existing race leaves the gauge elevated when an initial Running report follows Stopping and batch draining is aborted. apply the final Stopped through apply_status.

warning: core/connectors/runtime/src/sink.rs:737 - this pre-existing call ignores declared sink failures and skips runtime error accounting. propagate nonzero results before success accounting, and update sink status and gauge together when enabling that error path.

Comment thread core/integration/tests/connectors/random/random_source.rs Outdated
Comment thread core/connectors/runtime/src/manager/source.rs Outdated
Comment thread core/connectors/runtime/src/main.rs Outdated
Comment thread core/connectors/runtime/src/main.rs
Comment thread core/integration/tests/connectors/random/random_source.rs Outdated
Comment thread core/connectors/runtime/src/manager/source.rs Outdated
Comment thread core/connectors/runtime/src/source.rs
Comment thread core/connectors/runtime/src/source.rs Outdated
Comment thread core/connectors/runtime/src/source.rs Outdated
Comment thread core/integration/tests/connectors/random/random_source.rs Outdated
@github-actions github-actions Bot added S-waiting-on-author PR is waiting on author response and removed S-waiting-on-review PR is waiting on a reviewer labels Sep 12, 2026
`iggy_source_open` stored the container in the instance map whatever the open
returned, and `SourceContainer::open` assigns the source before it looks at the
result, so a failed open left a fully constructed instance behind. The runtime
gets its error back before it has recorded the plugin id, so nothing outside
the plugin can name that instance to close it, and it stays for the life of the
process holding whatever the plugin took before it failed.

The rollback is not registering it. Dropping the container releases the
instance the same way any other failed construction is released, and the
duplicate id guard above is untouched, so reopening the same id still refuses
rather than silently replacing a live instance.

Sinks had the same shape and get the same fix.

Not covered by a test: the FFI entry points are `cfg(not(test))`, so a unit
test cannot call them, and the map lives inside the plugin where the runtime
cannot observe it. The change is small enough to read, which is the argument
for it rather than around it.
…led runtime

The wait for a gauge value reported its last read by issuing another request,
and that request was the only one with no budget over it. It runs only after
the wait has already timed out, which is precisely when the runtime is stalled,
and the client had no timeout either, so the assertion that should have failed
hung instead and reported nothing. The loop now carries the value it saw, and
every request the file makes is built with the wait timeout on it.

The three stats readers were also the same request and the same decode written
out three times. They share one helper that hands back the `Result`, which is
what keeps the retry loop treating a failed read as "not yet" while the two
direct readers keep failing on it.

The settle window was named for state storage and used for a gauge. Both waits
are waiting on the same thing, a report that may land just after the poll
before it, so the constant says that instead.
…wnership

The guard carried a close callback and an `armed` flag that had to agree with
it, so both teardown paths cloned the callback to leave the flag behind them.
`Option<SourceClose>` is the same state said once: `disarm` clears it, the
awaited close and `Drop` each take it, and neither clones. Awaited close still
finishes before its caller returns, and `Drop` still hands the work to the
blocking pool with the container captured so the library stays mapped.

`record_started` is inlined at its one call site. It existed so the spawn was
passed as a closure and the compiler would refuse an await between the spawn
and the id record. That enforcement goes with it, so the requirement is written
where the statements are: an await between them strands the `SOURCE_SENDERS`
entry and both tasks with nothing naming them. Registration, the status
transition and the disarm stay inside the one lock hold, in that order.

Two tests go too. The callback's test tested the callback. The disarmed-guard
drop is already covered by the success half of the fallible-step helper, which
runs a guard through `disarm` and asserts nothing was closed.
`close_plugin_instance` took the word "source" or "sink" as a string its
signature did not constrain, while `ConnectorType` already defines exactly
those two labels. It takes the enum now, and `as_label` is visible in the crate
rather than only to the encoder. The helper also sat between two connector
struct declarations; it moves above them.

The cleanup argument was written out across the close type, the guard type, its
constructor, the awaited close, the `Drop` body and both call sites, mostly
repeating itself. It is on the guard type now, once, and still says all of it:
the window and why it is a guard rather than a branch per fallible call, that
startup and restart both hand off through it, that the library must stay mapped
for a deferred call, that `Drop` offloads to the blocking pool, and that
awaited close and `Drop` are not interchangeable because only one gives the
caller an ordering. The other sites point at it.

A test comment recorded which person measured an interleaving and on which PR.
It states the interleaving instead.
@mlevkov

mlevkov commented Sep 12, 2026

Copy link
Copy Markdown
Contributor Author

All 11 addressed and each one answered on its own thread. Four commits: the failed-open rollback, the stats-wait fixes, the guard and start window, and the naming and prose pass.

Two things I did not decide on my own, both on their threads and repeated here so they are not missed:

manager/source.rs:92 in the S2 comment does not land on cleanup prose in the current head; it is inside set_error, the gauge transition comment. The branch has taken master three times since you looked, so I think the anchor drifted. I consolidated at source.rs:319 and trimmed the two call-site comments in manager/source.rs that do repeat the argument. Point me at the right line if I missed one.

The record_started closure arrived as 0c4b859f8 fix(connectors): make an await in the start window a compile error — passing the spawn into a non-async fn is what made the compiler refuse an await between the spawn and the id record. Your note says to keep that property knowing the compiler stops enforcing it, so I inlined it and wrote the requirement where the statements now are. Flagging the commit in case it changes your mind.

One thing beyond what you asked: iggy_sink_open had the same unconditional insert as the source, so a failed sink open stranded an instance the same way. Fixed alongside. Happy to split it out if you would rather it rode separately.

213 runtime and 165 SDK unit tests pass, and all 26 connectors::random and connectors::runtime integration tests pass.

@mlevkov

mlevkov commented Sep 12, 2026

Copy link
Copy Markdown
Contributor Author

/ready

@github-actions github-actions Bot added S-waiting-on-review PR is waiting on a reviewer and removed S-waiting-on-author PR is waiting on author response labels Sep 12, 2026
@mlevkov

mlevkov commented Sep 12, 2026

Copy link
Copy Markdown
Contributor Author

/request-review @hubcio

@github-actions
github-actions Bot requested a review from hubcio September 12, 2026 19:46
@spetz
spetz merged commit 67e14c2 into apache:master Sep 12, 2026
68 checks passed
@github-actions github-actions Bot removed the S-waiting-on-review PR is waiting on a reviewer label Sep 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

connectors: source restart leaks the plugin instance when producer setup fails

3 participants