feat: Lanes B/C/D — Source/Destination lifecycle, config/record codec, acceptance harness + example - #2
Conversation
…tion Lane C of the v0.19 Python connector SDK workstream (docs/design/20260707-python-connector-sdk.md). - conduit/record.py: Record/Change/Operation/Data (bytes | Mapping) per §2.3, plus Metadata well-known-key constants and a representative set of typed accessors (created_at/read_at/collection). - conduit/config.py: BaseConfig (pydantic v2) + Field re-export + to_parameters() introspecting model_fields into config.Parameter/Validation (no codegen, per §2.2). gt=/lt= map exactly; ge=/le= are approximated as exclusive gt/lt (exact for int fields, epsilon-nudged for float, both documented). Literal[...] -> one TYPE_INCLUSION per value. TYPE_DURATION and TYPE_EXCLUSION explicitly raise NotImplementedError (open A-gaps, not silently guessed). - conduit/errors.py: BackoffRetry/BatchWriteError/ConnectorError. BatchWriteError is the B1 data-loss fix (§2.5): construction requires an exhaustive, disjoint success/failures accounting (or a written= prefix); ValueError at construction time if incomplete -- "ack everything not explicitly marked failed" is structurally unrepresentable, not just documented. - pyproject.toml: mypy_path/overrides extended so the generated connector.v2/config.v1/opencdc.v1 stubs (reached via conduit._grpc's sys.path trick, not conduit._grpc.* dotted imports) resolve under mypy; known-first-party isort config so `import conduit._grpc` always sorts before those stub imports (verified: reordering it breaks the sys.path side effect at runtime, not just a style nit). - tests/test_record_codec.py: Hypothesis round-trip tests, including the B3 google.protobuf.Struct int->float precision-loss case pinned exactly (not papered over with `==` laxity). - tests/test_config.py, tests/test_errors.py: mapping-rule and BatchWriteError construction-validation coverage. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015GQFzakPShAYj8CcwajYDD
…C v2 + shutdown watchdog Lane B of the v0.19 Python connector SDK workstream (docs/design/20260707-python-connector-sdk.md). - conduit/_dispatch.py: dual sync/async method dispatch (inspect.iscoroutinefunction detection, sync overrides run in the default thread-pool executor) shared by Source and Destination, per §2.1. - conduit/_introspect.py: shared generic-parameter recovery (Source[Config]/Destination[Config] -> Config) used to validate Configure's config map and build Specify's parameter map without author boilerplate. - conduit/source.py: Source ABC (abc.ABC, read() abstract) + _SourceServicer adapting it to SourcePluginServicer. Read loop reuses the Go SDK's exact backoff constants (Factor=2, Min=100ms, Max=5s, source.go:270-295, re-verified MUST-FIX 1) as a single serial loop. Invariant 1: ack() is called only from _consume_acks, driven only by Conduit's ack_positions on the Run request stream -- the read loop itself never calls ack(). - conduit/destination.py: Destination ABC (write() abstract) + _DestinationServicer. _write_batch is the B1 enforcement site: full success only after write() returns cleanly; BatchWriteError's already-validated success/failures accounting drives every ack/nack decision; any other exception nacks the entire batch. No code path acks an index absent from the exhaustive success set. - conduit/_grpc/_controller.py: hand-written (not generated -- go-plugin's own internal proto, outside conduit-connector-protocol) GRPCController.Shutdown service, registered via grpc.method_handlers_generic_handler. - conduit/_grpc/adapters.py: Record/Data/Change <-> proto conversion. The B3 google.protobuf.Struct int->float boundary is documented at its one exact call site (_data_to_proto). - conduit/serve.py: serve() entry point -- handshake validation (reusing _handshake.py, Lane A), grpc.aio server bootstrap, health/specifier/ connector servicer registration, and _ShutdownCoordinator: the hung-event-loop watchdog (MUST-FIX 3). SIGTERM is caught with low-level signal.signal (not loop.add_signal_handler, which cannot fire if the loop is wedged) to start an independent threading.Timer that force-exits after a bounded, configurable deadline if graceful shutdown (teardown() + GRPCController.Shutdown) hasn't confirmed completion. - tests/test_source.py, tests/test_destination.py: ack-ordering and B1 partial-batch-nack coverage (test_destination_partial_write_nacks_all's three cases: incomplete accounting, well-formed written= prefix, non-BatchWriteError exception). - tests/test_serve.py: the deterministic shutdown test (MUST-FIX 2) -- a real grpc.aio server, a real gRPC client calling /plugin.GRPCController/Shutdown, asserting the RPC succeeds and teardown() ran exactly once beforehand via a spy, not a timing race -- plus the hung-loop watchdog tests (MUST-FIX 3), including one that genuinely wedges a real event loop in a background thread and confirms the watchdog still fires within its documented deadline. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015GQFzakPShAYj8CcwajYDD
Lane D of the v0.19 Python connector SDK workstream (docs/design/20260707-python-connector-sdk.md). - conduit/testing/acceptance.py: AcceptanceTestDriver Protocol, ConfigurableAcceptanceTestDriver convenience wrapper, and AcceptanceTestSuite -- the versioned (CONTRACT_VERSION = "2026-07.v1") acceptance suite an author subclasses in their own pytest module. Covers every category from the design doc §3: specifier existence/validity, config validation (success + required-param-missing), resume-at-position (snapshot and CDC-equivalent), read/write round trip, read timeout behavior, and partial-batch write correctness (paralleling test_destination_partial_write_nacks_all as an SDK-level guarantee). Exercises connectors in-process via the same servicer adapters serve.py uses -- no real gRPC socket, no real Conduit binary (that's compat-nightly.yml/Conduit-repo scope). - conduit/testing/fixtures.py: golden OpenCDC record-shape factories (snapshot/create/update/delete_record, with_collection). - examples/http-poll-source/main.py: the design doc §2.7 worked example, made fully runnable (httpx-based HTTP polling source, BackoffRetry on empty responses, position-based resume). - examples/http-poll-source/pyproject.toml: standalone packaging, mirroring what `conduit connector new --lang python` will scaffold (Phase 3). - tests/test_acceptance_harness.py: the suite run against a synthetic in-memory driver. - tests/test_example_http_poll_source.py: the suite run against the real, unmodified example file, in-process against a real local HTTP server (stdlib http.server, no httpx mocking) -- this test passes, it is not skipped or stubbed. - README/CHANGELOG updates reflecting Lanes B/C/D landing. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015GQFzakPShAYj8CcwajYDD
…code CI (uv sync --all-extras, ruff/mypy jobs) caught two gaps not visible in my local dev environment, whose packages had drifted from what a fresh `pip install -e '.[dev]'`/`uv sync` resolves: - mypy: `types-protobuf` was installed manually in my local venv while developing but never added to pyproject.toml's dev extras, so a fresh environment (CI, or any contributor running `uv sync`) hit "Library stubs not installed for google.protobuf" in serve.py/_grpc/_controller.py. Added `types-protobuf>=6.30,<7` to `[project.optional-dependencies].dev`. - ruff format: ruff 0.16.0 (resolved by CI's `ruff>=0.14,<1` constraint; 0.15.x, what I had locally, treats Markdown formatting as preview-only and skips it by default) now formats Markdown-embedded Python code fences by default. This caught 3 pre-existing, purely cosmetic whitespace inconsistencies in docs/design/20260707-python-connector-sdk.md's embedded code examples (double-space-before-comment, missing blank lines) -- not present in any file this PR otherwise touches, not a content change, verified via `git diff` to be whitespace-only. Verified by simulating CI exactly: a fresh venv, `pip install -e '.[dev]'` (uv unavailable in this sandbox), then ruff format --check / ruff check / mypy / pytest -- all green, matching CI's actual dependency resolution rather than my possibly-stale local venv. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015GQFzakPShAYj8CcwajYDD
DX audit fix #1. `datetime.timedelta`-typed BaseConfig fields now map to config.Parameter.TYPE_DURATION instead of raising NotImplementedError: - New public conduit.config.format_go_duration()/parse_go_duration(): serialize/parse Go's time.Duration.String()/ParseDuration syntax ("5s", "1h30m", "500ms", "1.5h", "-1.5h", ns/us/µs/ms/s/m/h units), using exact fractions.Fraction arithmetic (no float rounding) down to microsecond resolution -- the finest datetime.timedelta supports. - to_parameters() serializes a timedelta field's default via format_go_duration(); gt=/lt=/ge=/le= constraints on duration fields use the same exact (not epsilon-approximated) ±1-microsecond boundary adjustment already used for int fields, since timedelta is likewise a discrete, integer-resolution type. - BaseConfig gained a model_validator(mode="before") that parses a Go-duration string into a real timedelta before pydantic's own validation runs, so the Configure RPC's map<string,string> config values round-trip correctly. Direct construction with a real timedelta still works unchanged. - TYPE_EXCLUSION remains an open, documented A-gap (untouched by this fix) -- still raises NotImplementedError rather than guessing. Tests: tests/test_go_duration.py (known-Go-output pins + Hypothesis round-trip property over arbitrary microsecond counts + malformed-syntax rejection), tests/test_config.py (to_parameters() mapping + Configure-side string parsing + direct-construction-still-works cases). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015GQFzakPShAYj8CcwajYDD
…ard validation detail DX audit fixes #2-#4. - Source.ack()'s default was already a genuine no-op (return None, no log line, no raise) -- verified, and its docstring now says explicitly: "you don't need to override ack() unless you're acknowledging against the source system itself (e.g. committing a Kafka offset, deleting a queue message)." Same note added to the example connector's README. - Renamed lifecycle_on_created/lifecycle_on_updated/lifecycle_on_deleted to on_created/on_updated/on_deleted on both Source and Destination -- the lifecycle_ prefix was redundant (these already live on the connector class). Updated the ABCs, the servicer dispatch code, and tests/test_destination.py. Grepped for every lifecycle_on_ reference (including _dispatch.py/_introspect.py, which had none) to confirm no stragglers. - BatchWriteError.partial(batch_size, written=N, cause=exc): the recommended constructor for the common contiguous-prefix partial-batch case. Every failed index is recorded with the real cause exception instead of a generic "not reached" placeholder, so the ack error detail that reaches Conduit reflects what actually went wrong. Reuses the existing exhaustive-accounting constructor path under the hood -- still no code path that computes "ack everything not explicitly failed." - Configure RPC handlers (Source and Destination) now catch pydantic.ValidationError explicitly and abort with INVALID_ARGUMENT plus a per-field detail message (errors.format_validation_error()), instead of relying on grpc.aio's generic "Unexpected <exception class>: ..." UNKNOWN-status wrapping of an uncaught exception -- per CLAUDE.md's "errors are API, actionable" standard. Tests: tests/test_errors.py (BatchWriteError.partial construction + cause-propagation), tests/test_configure_errors.py (real grpc.aio server + real client, asserting INVALID_ARGUMENT status and that the field name/ message actually appear in the gRPC status detail -- not a mock of pydantic or of the transport). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015GQFzakPShAYj8CcwajYDD
…bug found by it DX audit fix #5 (the #1 lever per the audit). New `conduit-connector-sdk` console script, `build` subcommand (_build.py/_cli.py): packages a connector project into one self-contained, directly-executable artifact. Closes the design doc §1.1.6 packaging gap -- Conduit execs a standalone connector subprocess with a clean environment (no inherited PATH), so a `pip install`-then-shebang-script connector cannot launch; this command produces a file whose shebang is an absolute interpreter path resolved at build time. Vendoring strategy: copies files from the *current environment's* already-installed distributions (importlib.metadata: file manifests + transitive Requires-Dist, marker-filtered for extras) rather than a fresh pip install -- this SDK isn't published to PyPI yet, so a fresh resolve would fail outright; this also means `build` needs no network access and runs in ~0.2-0.4s. conduit-connector-sdk itself is vendored by copying its actual installed location directly (works for editable dev installs too, where importlib.metadata's file manifest is just a .pth redirect). Not a plain zipapp: grpcio and pydantic's pydantic-core both ship compiled extensions, which zipimport cannot load from inside a zip archive. Found this the hard way -- an earlier version of this command raised BuildError on any compiled extension, which would have made this SDK's own core dependencies (grpcio, pydantic) impossible to vendor at all. Fixed by generating a small, dependency-free bootstrap __main__.py (the only thing zipimport ever runs directly) that extracts the real payload to a per-build cache directory on first run -- the same fundamental approach shiv/pex use -- then executes the connector's real entry point from those extracted, real files. Verified end-to-end: built the example connector, exec'd the artifact directly (not `python <artifact>`), confirmed grpc/pydantic-core's .so files are present as real extracted files and the handshake completes correctly. Bug fix, found via that same end-to-end testing: sending a real SIGTERM to a running serve() process whose connector's teardown() raised (e.g. HTTPPollSource.teardown() accessing self._client before open() ever ran) silently hung for the full watchdog deadline instead of shutting down promptly. Root cause: _sigterm_shutdown()'s coroutine is scheduled via run_coroutine_threadsafe from a signal handler with its Future never awaited/checked, so an exception inside it was silently swallowed and shutdown_requested was never set. Fixed: shutdown_requested.set() now runs unconditionally in a finally block, with a clear stderr diagnostic distinguishing "teardown raised" from "loop genuinely wedged" -- a buggy teardown() no longer blocks the entire SIGTERM-triggered graceful path. Also hardened the example connector's own teardown() to guard against running before open() ever did. Tests: tests/test_build.py builds the real example connector and execs the resulting artifact directly as a subprocess (mirroring exactly how Conduit's dispenser launches a plugin) -- asserts a valid handshake line, an absolute-path shebang, compiled extensions present as real extracted files, cache reuse on a second launch, and (the regression test for the bug above) that a real SIGTERM triggers prompt graceful shutdown rather than hanging until the watchdog fires. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015GQFzakPShAYj8CcwajYDD
CI's windows-latest jobs failed test_build.py: Windows has neither POSIX executable bits (os.chmod's execute flags are a no-op there for arbitrary extensions) nor shebang-based direct execution (CreateProcess dispatches by file extension, not by parsing a leading `#!` line -- confirmed via the actual CI failure: `OSError: [WinError 193] %1 is not a valid Win32 application` when invoking the bare .pyz path). This is exactly the "Windows subprocess launch specifics ... untested until the CI matrix actually runs it" risk the design doc already flagged as open, now surfaced for real. - The executable-bit assertion is skipped on Windows (nothing meaningful to assert there for a .pyz). - Every subprocess invocation goes through a new `_exec_argv()` helper: the bare artifact path on POSIX (the actual "no `python` prefix" claim this test suite is about), `[sys.executable, artifact]` on Windows, documented as a real, known platform difference rather than silently worked around. - `test_sigterm_triggers_prompt_graceful_shutdown` is skipped on Windows outright: `Popen.send_signal(SIGTERM)` maps to an unconditional `TerminateProcess()` there, not something `conduit.serve`'s SIGTERM handler ever observes, so the test would not exercise the graceful path it exists to pin. Windows-native graceful shutdown is out of scope for this fix (a real, separate feature involving different Windows IPC/ signal-equivalent mechanisms). - The compiled-extension-module check now also looks for `*.pyd` (Windows' extension suffix), not just `*.so`/`*.dylib`. Verified: all 8 tests in tests/test_build.py still pass locally (macOS/POSIX); the Windows-specific branches were validated against the exact CI failure output, not guessed at blind. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015GQFzakPShAYj8CcwajYDD
DX audit follow-up: 5 must-fixes applied (5 new commits on this branch)Per the coordinator's request, applied all five DX-audit items to this same branch/PR. Summary per item: 1. Duration config → real
2.
3. Lifecycle hook rename (
4.
5.
Gate results (final, this branch's HEAD, all green in CI)
Still Tier 1 — needs DeVaris's explicit sign-off, still not mergedThis PR remains Tier 1 (data path / wire adapter / config codec). The self-review from the original PR description still applies; this comment adds no new unresolved risk beyond what's documented above (the Windows graceful-shutdown gap and the extraction-cache concurrency note in 🤖 Generated with Claude Code |
Tier-1 review gap (invariant 7): serve.py's _sigterm_shutdown ran teardown() immediately on SIGTERM with no regard for an actively streaming Run() call, letting teardown() (e.g. closing a DB pool) race a live Source.read()/Destination.write(). Not a data-loss bug (a raced write nacks the whole batch and Conduit redelivers), but a graceful- shutdown gap on a Tier-1 SDK where graceful shutdown is the contract. - _SourceServicer.drain()/_DestinationServicer.drain() factor out the same stop-then-wait ordering Stop() already performs for the deterministic path (Conduit Stop RPC -> Run ends -> Teardown RPC), reusable from serve.py's SIGTERM handler. Destination's Run() gained the same _stop_event/_stopped_event/finally shape Source already had, so drain() waits for the whole Run() generator to finish (including its already-computed ack response), not just for write() to return. - _sigterm_shutdown awaits drain() before teardown(), bounded by the existing hung-loop watchdog deadline (unchanged) so a stuck connector still force-exits on schedule. - Fixed the design doc overclaim (Risks & open questions §3): it asserted the ordinary SIGTERM-mid-write case was "covered by the ordinary SIGTERM-mid-write test," but the only existing SIGTERM test fires before Open(). Added the actual TestSigtermDrainsInFlightOperation::test_sigterm_mid_read_drains_before_teardown and ::test_sigterm_mid_write_drains_before_teardown integration tests (real grpc.aio server + client, deterministic via direct _on_sigterm invocation) and pointed the doc at them. Gates: ruff format --check, ruff check, mypy --strict (18 source files), full pytest (165 passed). Generated _grpc/ dirs unchanged. Tier 1 (data path adjacent: SDK shutdown contract) -- does not merge without DeVaris sign-off. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015GQFzakPShAYj8CcwajYDD
Invariant-7 gap fix + design-doc overclaim fix (Tier-1 review)The gap (confirmed): Fix:
Doc overclaim fixed: Risks & open questions §3 asserted the ordinary SIGTERM-mid-write case was "covered by the ordinary SIGTERM-mid-write test" — no such test existed; the only SIGTERM test ( New tests (
Both build the real Gates: Not merging — Tier-1, needs DeVaris sign-off per CLAUDE.md. |
Summary
Implements Lanes B, C, D of the Python connector SDK per
docs/design/20260707-python-connector-sdk.md, completing the Phase-1 (v0.19 core) author-facing surface on top of Lane A (handshake + generated gRPC stubs, already merged).Lane C — config/record:
conduit/record.py:Record/Change/Operation/Data(bytes | Mapping[str, Any]),Metadatawell-known-key constants + typed accessors.conduit/config.py:BaseConfig(pydantic v2) +Field+to_parameters()— introspectsmodel_fieldsintoconfig.Parameter/Validationwith no codegen.gt=/lt=map exactly;ge=/le=are approximated as exclusivegt/lt(exact forint, epsilon-nudged forfloat, documented).TYPE_DURATION/TYPE_EXCLUSIONraiseNotImplementedError(documented open A-gaps, never guessed).conduit/errors.py:BackoffRetry,ConnectorError, andBatchWriteError— the B1 fix. Construction requires an exhaustive, disjointsuccess/failuresaccounting (or awritten=prefix); incomplete accounting raisesValueErrorat construction time.Lane B — lifecycle over gRPC v2:
conduit/source.py/conduit/destination.py:Source/Destinationasabc.ABCgeneric over config, dual sync/async dispatch (conduit/_dispatch.py), plus the internal_SourceServicer/_DestinationServiceradapters.conduit/serve.py:serve()entry point — handshake validation (reusing Lane A's_handshake.py),grpc.aioserver, health/specifier/connector servicer registration, and_ShutdownCoordinator— the hung-event-loop watchdog (▶ MUST-FIX 3).conduit/_grpc/_controller.py: hand-writtenGRPCController.Shutdown(go-plugin's own internal proto, not part ofconduit-connector-protocol).conduit/_grpc/adapters.py:Record/Data/Change↔ proto conversion, including the B3Structint→float boundary at its one documented call site.Source.ack()is called only from the ack-consumer loop, driven only by Conduit'sack_positions;Destinationacks/nacks are driven only bywrite()returning cleanly orBatchWriteError's pre-validated accounting.Factor=2, Min=100ms, Max=5s).Lane D — acceptance harness + example:
conduit/testing/acceptance.py:AcceptanceTestDriverProtocol,ConfigurableAcceptanceTestDriver,AcceptanceTestSuite(contract version2026-07.v1) covering all six design-doc categories.conduit/testing/fixtures.py: golden record-shape factories.examples/http-poll-source/: the design doc §2.7 worked example, made fully runnable, plus its ownpyproject.toml.Failure-mode analysis (Tier 1)
ValueErroratBatchWriteError.__init__time — the adapter has no code path that computes "ack everything not explicitly failed." Any non-BatchWriteErrorexception nacks the whole batch. Regression tests:test_destination_partial_write_nacks_all's three cases intests/test_destination.py, plus a duck-typing-resemblance case (_RaisesNaiveIncompleteMapping) proving onlyisinstance(exc, BatchWriteError)grants partial credit.Source.ack()has exactly one call site (_consume_acks), driven only byack_positionsarriving on theRunrequest stream. The read loop (_read_loop) never referencesack(). Verified bytests/test_source.py::TestAckOnlyAfterConduitConfirms.GRPCController.Shutdownnot implemented / silently falls through: implemented and tested end-to-end (realgrpc.aioserver, real client, real RPC) intests/test_serve.py::TestDeterministicShutdownRpc— asserts the RPC succeeds andteardown()ran exactly once beforehand via a spy, not a timing race (▶ MUST-FIX 2's tightened bar)._ShutdownCoordinatorinstallsSIGTERMvia low-levelsignal.signal(notloop.add_signal_handler) and starts an independentthreading.Timerwatchdog. Tested directly (test_watchdog_fires_when_shutdown_never_confirmed,test_watchdog_does_not_fire_if_confirmed_before_deadline,test_start_watchdog_is_idempotent) and against a genuinely wedged real event loop in a background thread (test_watchdog_forces_exit_even_with_a_genuinely_wedged_event_loop), proving the watchdog fires independent of the loop's own thread. Scope note: this does not exercise real OSSIGTERMdelivery timing to a wedged main thread — that interaction is flagged as its own, separately hard-to-construct-deterministically concern (see Self-review below), appropriate forcompat-nightly/Conduit-repo-side verification, not this unit suite.server.stop(grace=None)racing the Shutdown RPC's own response: investigated becausegrpc.aio.Server.stop's docstring says aNonegrace period "aborts all existing RPCs immediately" — a plausible race where the Shutdown RPC's own response gets cut off by the subsequentstop()call from the concurrentdrive_shutdowntask. Empirically stress-tested (30 repeated runs of the shutdown test class, no flakiness) and the full suite (5 repeated full runs, no flakiness). Read as: grpc.aio's internal RPC-completion bookkeeping for an already-returned handler resolves beforestop()'s "wait for last handler to terminate" clause proceeds. Flagged in Self-review as verified-by-repetition, not verified by reading grpc-core's C-level implementation.Structint→float precision loss: documented and pinned exactly (not glossed over with==laxity) intests/test_record_codec.py::test_large_int_in_structured_data_loses_precision_silently. Contrasted with the loud-failurebytes-inside-structured-data case.Self-review (adversarial pass)
_acks_from_batch_write_error: the only ack branch isif i in exc.success; theelsebranch always nacks (usingexc.failures.get(i, <generic RuntimeError>)— never falls through to an ack). Verified this holds even under a hypothetically buggy connector that mismatchesBatchWriteError's declaredbatch_sizeagainst the realrecordslist length (traced both under- and over-declaration cases) — no path over-acks. No issue found.ack_positionsconfirms it?self._source.ackhas exactly one call site,_consume_acks, itself driven only by iterating theRunrequest stream._read_loop(the producer side) never referencesack. No issue found.start_watchdog()is only ever invoked from_on_sigterm— meaning ifSIGTERMnever arrives (the ordinaryGRPCController.Shutdown-only path), nothreading.Timeris created at all, nothing to leak. WhenSIGTERMdoes arrive and shutdown completes normally,confirm_clean_exit()sets the confirmed flag before callingtimer.cancel(), closing the race window against_force_exit(which checks the flag first). No issue found.Structint/float boundary mishandled anywhere outside the one documented site? Grepped forStruct/MessageToDict/structured_dataacross hand-written code — exactly one encode site (_data_to_proto) and one decode site (_data_from_proto), both inconduit/_grpc/adapters.py, both documented. No issue found.server.stop(grace=None)-vs-Shutdown-RPC-response race. Resolved via empirical stress-testing rather than static reasoning alone — flagged so DeVaris can decide whether that's sufficient confidence for this property or whether it warrants a grpc-core-level investigation before sign-off.This is Tier 1 (data path) and needs DeVaris's explicit sign-off — do not merge.
Test plan
ruff format --check .— pass (26 files)ruff check .— passmypy— pass, 16 source files, no errorspytest— 98 passed (5 repeated full runs, no flakiness; 30 repeated runs of the shutdown-RPC test class specifically, no flakiness)git status --porcelain -- src/conduit/_grpc/connector src/conduit/_grpc/opencdc src/conduit/_grpc/config— empty (generated dirs untouched)tests/test_acceptance_harness.py) and against the real, unmodified example connector (tests/test_example_http_poll_source.py) — 8/8 categories each, not skipped or stubbedconduit pipelines stopgraceful-shutdown verification — out of scope for this repo's CI per the design doc (compat-nightly.yml/Conduit-repo-side, not built here)🤖 Generated with Claude Code
https://claude.ai/code/session_015GQFzakPShAYj8CcwajYDD