Skip to content

feat(connectors): add sink/source connector templates, authoring guide, and blog post - #4636

Open
ryerraguntla wants to merge 11 commits into
masterfrom
feat/connectors-3956-sink-source-templates
Open

ryerraguntla wants to merge 11 commits into
masterfrom
feat/connectors-3956-sink-source-templates

Conversation

@ryerraguntla

Copy link
Copy Markdown
Contributor

Fixes #3956

What

Adds two ready-to-copy connector crates under core/connectors/:

  • core/connectors/sinks/sink_template/
  • core/connectors/sources/source_template/

Each implements every framework-level requirement already expected of a production connector — config parsing with unknown-field rejection,SecretString-wrapped credentials, connectivity validation in open(), retry/backoff + circuit breaker via iggy_connector_sdk::retry::RetryPolicy, batch processing / Ack-Nack cursor staging, and a canonical test suite — leaving only the backend-specific bits marked TODO(ConnectorDeveloper):

  • sink_template: the outbound data-push call to the destination system, plus a stable per-message dedup key for idempotent writes.
  • source_template: the connection/client setup, the data-fetch call against the source system, and setting a natural per-record identity on ProducedMessage.id when one exists.

Also included:

  • core/connectors/docs/authoring-sinks-and-sources.md — a written guide walking through the required patterns and why each one exists, meant to be cross-linked from the connector READMEs and reused as the basis for an Apache Iggy blog post.
  • core/connectors/BLOG_POST.md — a narrative draft for the project blog announcing the templates.
  • .claude/skills/connector-pr-review/SKILL.md — a new review-checklist skill (B1-B11 blockers, convention nits) encoding review patterns mined from real apache/iggy connector PRs, so recurring review comments (secrets typing, retry/circuit-breaker gaps, cursor staging, idempotency claims, zero-delivered-still-reports-Ok, ack-path budget blowups) get caught before human review instead of during it.
  • Updated connector-sink, connector-source, connector-testing, and connectors-overview skills to cross-link the templates and the new review checklist, and to reflect the current Source::batch_policy() / NACK-retry-configurability SDK surface (fix(connectors): make source NACK retries configurable and observable #4269) and the current resolver/toolchain setup, replacing stale text that predated both.
  • Workspace registration (Cargo.toml, Cargo.lock) and README entries (sinks/README.md, sources/README.md, core/connectors/README.md) for both new crates, each with publish = false.
  • Example runtime configs
    (core/connectors/runtime/example_config/connectors/{sink,source}_template.toml).

Why

Per the discussion in #3956: connector PRs have repeatedly lost review
cycles to the same non-backend-specific issues (secrets typed as plain
String, missing retry/circuit-breaker, config knob name drift, missing
canonical state tests, zero-delivered batches reported as success, ack-path
work with no bound on its own retry budget). Rather than re-teaching the
same framework contract PR after PR, these templates make the required
pattern the path of least resistance — a new connector starts from working,
reviewed-pattern code and only needs the backend-specific logic filled in.
The paired connector-pr-review skill turns the same lessons into an automatic pre-flight check for future connector PRs.

Testing

  • cargo build -p iggy_connector_template_sink -p iggy_connector_template_source
  • cargo test -p iggy_connector_template_sink -p iggy_connector_template_source (20/20)
  • cargo clippy -p iggy_connector_template_sink -p iggy_connector_template_source --all-targets -- -D warnings
  • cargo fmt --all --check
  • cargo sort --check --no-format (crate + workspace)
  • cargo machete
  • cargo doc -p iggy_connector_template_sink -p iggy_connector_template_source --no-deps with RUSTDOCFLAGS="-D warnings"
  • markdownlint on every new/changed doc
  • ./scripts/ci/license-headers.sh --check

AI Usage

  • Tools: Claude (Claude Code)
  • Scope: template crate authoring, the review-checklist skill, rebasing onto current master and reconciling against SDK changes that landed after the branch was first cut (e.g. fix(connectors): make source NACK retries configurable and observable #4269's batch_policy()), and mining real apache/iggy connector PR review comments for the checklist content.
  • Verification: see Testing above. Full verification chain run locally against the actual rebased/current apache/master, not a stale base.
  • I can explain every line of code and every checklist item if asked.

Notes for reviewers / before marking ready

  • This branch previously went out as feat(connectors): add sink_template and source_template starting points #3965 (closed: corrupted git history from repeated master merges) and separately as a draft on docs/connector-guidelines-template-and-blog-post (closed/abandoned, also under a misleading docs/ prefix despite shipping real code — renamed to this branch for that reason). This PR is the rebuilt, rebased, re-verified version of that work, squashed to a clean history.

AI Usage

If AI tools were used, please answer:

  1. Which tools? Claude
  2. Scope of usage? Analysis of the previous connectors PR reiew, summary of the analysis and template generation
  3. How did you verify the generated code works correctly? Reviewed Manually
  4. Can you explain every line of the code if asked? Yes

@github-actions github-actions Bot added the S-waiting-on-review PR is waiting on a reviewer label Oct 24, 2026
@hubcio

hubcio commented Oct 24, 2026

Copy link
Copy Markdown
Contributor

lets wait with this PR till deferred polling PR is merged, it changes all connectors

@ryerraguntla

Copy link
Copy Markdown
Contributor Author

/author

@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 Oct 24, 2026
@codecov

codecov Bot commented Oct 24, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 74.96318% with 170 lines in your changes missing coverage. Please review.
✅ Project coverage is 81.05%. Comparing base (8d6d0fe) to head (06ea403).
⚠️ Report is 2 commits behind head on master.

Files with missing lines Patch % Lines
core/connectors/sinks/sink_template/src/lib.rs 79.26% 91 Missing and 10 partials ⚠️
core/connectors/sources/source_template/src/lib.rs 64.06% 67 Missing and 2 partials ⚠️

❗ There is a different number of reports uploaded between BASE (8d6d0fe) and HEAD (06ea403). Click for more details.

HEAD has 1 upload less than BASE
Flag BASE (8d6d0fe) HEAD (06ea403)
rust 4 3
Additional details and impacted files
@@              Coverage Diff              @@
##             master    #4636       +/-   ##
=============================================
- Coverage     91.06%   81.05%   -10.02%     
+ Complexity     3878     3875        -3     
=============================================
  Files          1562     1564        +2     
  Lines        405733   406409      +676     
  Branches     339602   340281      +679     
=============================================
- Hits         369496   329395    -40101     
- Misses        28844    69554    +40710     
- Partials       7393     7460       +67     
Components Coverage Δ
Rust Core 80.12% <74.96%> (-12.20%) ⬇️
Java SDK 79.38% <ø> (+0.01%) ⬆️
C# SDK 84.42% <ø> (ø)
Python SDK 86.52% <ø> (ø)
PHP SDK 92.23% <ø> (ø)
Node SDK 96.62% <ø> (+0.02%) ⬆️
Go SDK 71.82% <ø> (+0.15%) ⬆️
C++ SDK 86.84% <ø> (ø)
Files with missing lines Coverage Δ
core/connectors/sources/source_template/src/lib.rs 64.06% <64.06%> (ø)
core/connectors/sinks/sink_template/src/lib.rs 79.26% <79.26%> (ø)

... and 544 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.

@github-actions

github-actions Bot commented Nov 1, 2026

Copy link
Copy Markdown

This pull request has been automatically marked as stale because it has not had recent activity. It will be closed in 7 days if no further activity occurs.

If you need a review, please ensure CI is green and the PR is rebased on the latest master. Don't hesitate to ping the maintainers - either @core on Discord or by mentioning them directly here on the PR.

Thank you for your contribution!

@github-actions github-actions Bot added the S-stale Inactive issue or pull request label Nov 1, 2026
Squashed for a clean rebase onto current master.
# Conflicts:
#	.claude/skills/connector-pr-review/SKILL.md
#	core/connectors/sinks/sink_template/README.md
#	core/connectors/sinks/sink_template/src/lib.rs
#	core/connectors/sources/source_template/README.md
#	core/connectors/sources/source_template/src/lib.rs
@ryerraguntla
ryerraguntla force-pushed the feat/connectors-3956-sink-source-templates branch from e349759 to edac134 Compare November 4, 2026 11:26
@github-actions github-actions Bot removed the S-stale Inactive issue or pull request label Nov 4, 2026
@ryerraguntla

ryerraguntla commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor Author

/ready

@ryerraguntla

Copy link
Copy Markdown
Contributor Author

@hubcio @core please review

@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 Nov 5, 2026
@hubcio

hubcio commented Nov 5, 2026

Copy link
Copy Markdown
Contributor

/skill team-review

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Summary: The review found a data-loss defect in the source fill-in-the-blank kit: poll() commits the cursor before the runtime acknowledges the batch. It also found that the sink template's delivery budget does not bound a hung request, that the secrets guidance claims serialize_secret redacts a credential when it writes the plaintext, and several smaller template and documentation defects.

Counts: critical 2, warning 6, nit 3, simplification 2


This review was generated by Claude Code 2.1.310 on deepseek-flash[1m]. Review the output before you act on it.

let persisted = ConnectorState::serialize(&candidate, CONNECTOR_NAME, self.id)
.ok_or_else(|| Error::Serialization("failed to serialize source state".into()))?;
*self.pending.lock().await = Some(candidate);
let persisted = { // brief write

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

critical: poll() writes state.cursor before the runtime acknowledges the batch, and the kit dropped its on_batch_result override. A Nack therefore advances the cursor and loses that batch. Restore the pending_state staging and the commit-on-Ack handler from core/connectors/sources/source_template/src/lib.rs.

What never happens is the return trip. The runtime holds plugin configuration as a `serde_json::Value` - parsed from TOML, posted as JSON to the control API, or injected by env var - and hands that across the FFI, so nothing re-serializes the plugin's struct. Leaving `Serialize` off makes that compiler-enforced instead of convention-enforced (`sources/http_source/src/lib.rs::HttpSourceConfig` does this, and comments the omission so nobody adds it back).

Pattern:
Any credential-bearing field (connection strings, API keys, bearer tokens, AWS keys) must be `SecretString` from the `secrecy` crate, with the workspace serde wrapper applied so `Debug` and serialization both redact. Runtime exposes plugin configs over the `/stats` HTTP surface via serialization - plain `String` leaks the secret to anyone who can hit the endpoint. Plain `String` for a credential is a review-blocker. Pattern (from `sinks/postgres_sink/src/lib.rs::PostgresSinkConfig`):

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

critical: serialize_secret writes the plaintext, not [REDACTED]​. The redacting helpers are serialize_redacted and serialize_optional_redacted, and the runtime serves the raw plugin_config value, so a plugin-side annotation cannot redact the API output. Use a redacting helper or keep Serialize off, and restore the warning that serialize_secret exposes the secret. Also at line 202.

failed_rounds += 1;
let pause =
retry_backoff(self.retry_delay, failed_rounds, self.retry_max_delay);
if Instant::now() + pause >= deadline {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

warning: deliver_chunk checks the deadline only after attempt() returns. The retry middleware's timed attempts and Retry-After sleeps can therefore run past max_delivery_time inside one round. Bound each attempt with the remaining budget, or build the client with one attempt and reject a timeout above the budget in open().

// would stage a cursor past data that was never produced, and a
// later Ack would commit past it permanently.
let natural_id = record.natural_id;
new_cursor = Some(record.cursor_value);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

warning: The loop skips a record that fails to serialize, but a later record still moves new_cursor past it. The next poll never fetches that record again, and the comment above claims the opposite. Return Err and stage nothing when a record cannot be encoded, or stop at the first failed record.

/// If your destination's write call can succeed at the transport level
/// while rejecting some or all rows individually (a bulk/batch API
/// returning 200 with per-item errors, a stream-load response, a bulk
/// index response), count accepted rows yourself and return `Err` when

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

warning: The push_batch() guidance fails only when zero rows of a non-empty chunk are accepted. deliver_messages then counts a partially rejected chunk as fully written, so at-least-once stores the offset past the rejected rows. Require a failed call whenever a non-empty chunk loses any row in at-least-once mode.

All families labeled by `connector_key` + `connector_type` (histogram adds `stage`):

- **Counters**: `iggy_connector_messages_{produced,sent,consumed,processed,filtered}_total` and `iggy_connector_errors_total`. These are the *rendered* names; each is registered without the `_total`, which the OpenMetrics encoder appends.
- **Counters**: `iggy_connector_messages_{produced,sent,consumed,processed,filtered,errors}_total`.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

nit: The new counters line expands to iggy_connector_messages_errors_total. The runtime renders the errors counter as iggy_connector_errors_total (runtime/src/metrics.rs:562). Name the errors counter separately.

.await;
match result {
Ok(()) => written += chunk.len() as u64,
Err(err @ Error::PermanentHttpError(_)) if self.at_least_once => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

nit: Both permanent branches match only Error::PermanentHttpError. Other permanent variants such as SchemaMismatch and Serialization fall into the unclassified branch, so later chunks are still written. Match err.is_permanent() in both arms.

| Transient fetch failure (retry-worthy) | `Error::Connection` or `Error::HttpRequestFailed` |
| Permanent fetch failure (auth, schema gone) | `Error::PermanentHttpError` |
| Row failed to serialize | `Error::Serialization(...)` |
| State serialization failed | log + skip (non-fatal) |

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

nit: The table says a state serialization failure is logged and skipped. source_template and random_source return Error::Serialization from poll() and send nothing. Change the row to Error::Serialization(...).

at_least_once: bool,
verbose: bool,
state: Mutex<State>,
records_written_total: AtomicU64,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

simplification: The sink increments records_written_total at line 429 and never reads it, while State::messages_written already tracks the same count. Delete the field, its initializer and the fetch_add.

// fallback-with-warning behavior of `iggy_connector_sdk::retry::parse_duration`
// for the one duration field (`poll_interval`) that isn't itself optional in
// spirit (there's always a poll interval, just maybe the default one).
trait DurationExt {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

simplification: DurationExt wraps one humantime parse for a single call site. The crate already imports parse_duration. Replace the shim with parse_duration(config.poll_interval.as_deref(), DEFAULT_POLL_INTERVAL) and drop the humantime dependency.

@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 Nov 5, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

S-waiting-on-author PR is waiting on author response

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add sink/source connector templates under core/connectors/ as fill-in-the-blank starting points

2 participants