connector-sink
GitHub指导编写 Apache Iggy 连接器 Sink 插件,涵盖生命周期管理、配置处理及可靠性实现。
Trigger Scenarios
Install
npx skills add apache/iggy --skill connector-sink -g -y
SKILL.md
Frontmatter
{
"name": "connector-sink",
"description": "Author a new Apache Iggy connector sink plugin under core\/connectors\/sinks\/. Sinks consume messages from Apache Iggy streams and push them to an external system (DB, search engine, HTTP endpoint, object store). Load when creating, modifying, or reviewing a sink crate. Use for sink plugin authoring. NOT for runtime internals (see connector-runtime)."
}
Writing an Apache Iggy Connector Sink
A sink is a Rust cdylib that implements iggy_connector_sdk::Sink
and exposes FFI symbols via the sink_connector! macro. The runtime
loads it as .so/.dylib/.dll, polls Apache Iggy on its behalf,
runs the transform chain, decodes via the configured Schema, and
hands the sink batches of ConsumedMessage. The sink is responsible
for getting them to the external system reliably and efficiently.
Universal connector rules (SecretString, benchmark, verbose flag, drop accounting, filter contract, exemplar patterns) live in connectors-overview. This skill covers only what's sink-specific.
Contents
STOP and ask the user before
- Changing the SDK trait surface (
Sink::open/consume/close) - that's an SDK change, not a plugin one. - Touching default
batch_size, retry caps, or backoff defaults baked into existing in-tree sinks. - Promoting an
Errvariant to retryable when it previously was not - retrying bad data trips circuit breakers. - Adding a new sink that introduces a backend the runtime doesn't yet integration-test for.
Quick reference
- Skeleton: TEMPLATE.md (load on demand when authoring).
- Exemplars:
stdout_sink(minimal),postgres_sink(DB + transient detection),http_sink(validation, batch modes, retry middleware),mongodb_sink(atomic counters),elasticsearch_sink/iceberg_sink(backend-specific idioms).
Hard rules
Lifecycle
Sink::consumetakes&self- never&mut self. Mutable state goes behindtokio::sync::Mutex/AtomicU64. Prefer atomics for hot counters -mongodb_sinkusesmessages_processed: AtomicU64+insertion_errors: AtomicU64and avoids a state lock on the hot path entirely.Sink::openbuilds the client AND tests connectivity. Fail fast withError::InitError.Sink::closeflushes + drops resources. Take ownership viaclient: Option<Client>+.take(). sqlx pools have.close().await. reqwest/mongodb/elasticsearch clients just drop. Log final stats either way.
Config
- Every runtime-tunable field is
Option<T>with a default applied innew(). Forward-compat: adding a field doesn't break existing TOML. - Durations:
Option<String>, parsed viahumantime::Durationinnew(). Two equivalent in-tree idioms - inlineHumanDuration::from_str(&raw).map(|d| *d).unwrap_or_else(..)(random_source,postgres_source) or a localparse_durationhelper (http_sink,sdk/src/retry.rs). Fall back to a default on parse failure withwarn!- never panic. We do not usehumantime_serde. - Conflict resolution: if two config fields can be in invalid relative ordering, fix it in
new()+warn!- don't error.http_sinkswapsretry_delayandmax_retry_delaywhen reversed to avoid anExponentialBackoffpanic downstream. - Validation order: structural in
new(), connectivity inopen(), never inconsume().
Payloads (efficiency-critical)
- Dispatch on
messages_metadata.schemaand thePayloadvariant. HandleJson,Raw,Textat minimum. Unsupported ->Error::InvalidPayloadTypeor fall back to base64 (elasticsearch_sink). - For
Payload::Json, calltry_to_bytes(&self)- single-pass serialization without cloning theOwnedValuetree (sdk/src/lib.rs::Payload::try_to_bytes). Neverpayload.clone().try_into_vec(). - To take a payload out of
ConsumedMessagewithout cloning:std::mem::replace(&mut message.payload, Payload::Raw(vec![])). Standard pattern acrosshttp_sink::send_individual/send_ndjson/send_json_array. - Pre-allocate per-batch buffers with
Vec::with_capacity(messages.len())(or* factorfor serialized bodies). Used acrosspostgres_sink,mongodb_sink,quickwit_sink,elasticsearch_sink,http_sink.
Headers
message.headers: Option<BTreeMap<HeaderKey, HeaderValue>>. Check.is_empty()before iterating - runtime treats empty asNone.- For binary values use
v.as_raw()+ base64. for text usev.to_string_value(). Seehttp_sink::EncodedHeader. BTreeMapfor deterministic ordering. Don't collect intoHashMap.
Errors
| Scenario | SDK variant | Retry? |
|---|---|---|
| Init / connectivity failure | Error::InitError |
n/a (fails open) |
| Bad config value | Error::InvalidConfigValue(field) |
n/a |
| Single-message validation failure | Error::InvalidRecordValue(reason) |
no - skip + log |
| Transient backend (5xx, conn reset, ...) | Error::HttpRequestFailed / Error::Connection / Error::CannotStoreData |
yes |
| Permanent 4xx, schema mismatch, constraint | Error::PermanentHttpError / Error::SchemaMismatch |
no - retrying trips circuit breakers |
| I/O failure mid-write (parquet/iceberg) | Error::WriteFailure |
caller decides |
| Catalog commit failure (Iceberg) | Error::CatalogCommitError |
no - not idempotent |
Retry
- SDK helpers cover the simple case:
iggy_connector_sdk::retry::check_connectivity_with_retry(...)foropen(),HttpRetryMiddlewarefor default 429/5xx/network policy on reqwest clients. - Custom strategies:
reqwest-middleware+reqwest_retry::RetryTransientMiddleware::new_with_policy_and_strategy.http_sinkdefines its ownHttpSinkRetryStrategy(honorssuccess_status_codes, per-status decisions). - Non-HTTP clients: write
is_transient_error(&e)mapping driver-specific codes.postgres_sink::is_transient_errormaps SQLSTATEs40001,40P01,57P01-03,08000/03/06. - Backoff:
iggy_connector_sdk::retry::exponential_backoff(base, attempt, max)+jitter(). - Cap retries at 3 total attempts.
Idempotency
Runtime retries on any Err - same batch can be redelivered. In-tree guarantees:
- Postgres (
postgres_sink::build_batch_insert_query): PK onid, but plainINSERT(noON CONFLICT). Duplicate retry triggers23505,is_transient_errordoes NOT mark it transient -> error propagates. AddingON CONFLICT (id) DO NOTHINGis a known improvement. - MongoDB: composite
_idfromstream:topic:partition:message_id, viainsert_many(docs).ordered(false). Per-document duplicate-key errors don't abort the batch - effective idempotency under retry. - Elasticsearch (
bulk_index_documents): bulk action specifies only_index, no_id-> ES auto-generates ids -> NOT idempotent. Extensions should add"_id". - HTTP: endpoint-dependent. Document in the plugin README.
- Iceberg: transactional commit.
Error::CatalogCommitErrordocuments that the txn is consumed oncommit(). Rebuild-then-retry can duplicate data - callers must check the catalog first.
When writing a new sink, prefer dedup-on-write using the message id field. Mongo's upsert-style is the cleanest reference.
Batching
- Default
batch_size: 100. Tune per-backend. - Chunk via
messages.chunks(self.batch_size). Do not buffer acrossconsume()- runtime already batches at the consumer-poll boundary. - For HTTP backends support multiple batch modes when applicable (individual / ndjson / json_array / raw). See
http_sink::BatchMode.
Logging
info!("Opened <connector> connector ID: {}, endpoint: <redacted>", self.id);
debug!("Processing batch of {} messages, offset: {}", batch.len(), offset);
warn!("Retry {attempt}/{max} for <connector> ID: {}, reason: {error}", self.id);
error!("Failed to <op> for <connector> ID: {}, error: {error}", self.id);
info!("Closed <connector> connector ID: {}, processed: {}", self.id, count);
Use literal API field names as labels (offset=, current_offset=).
Common pitfalls
- Missing
[lib] crate-type = ["cdylib", "lib"]- runtime can't dlopen a regular rlib. - Cloning the whole
Payload::Jsonto serialize - usetry_to_bytes(&self). - Holding
Mutexacross.awaitwhen the locked region does I/O. &mut selfonconsume- won't compile. you need interior mutability.- Returning the first error and dropping subsequent batches - track
last_err, process all, return. - Forgetting to map
PermanentHttpError- retries hammer the backend with bad data. - Mutating config in
consume()- config is a snapshot fromnew(). Reload requires runtime restart. - Logging the connection string. Redact at construction:
http_sink::sanitize_url_for_logpopulatesHttpSink.log_url.
Tests
Unit tests at EOF of src/lib.rs. See connector-testing for BDD naming + test_config() helper.
Real-infra integration tests live under core/integration/tests/connectors/<backend>/ using #[iggy_harness] + a backend fixture that spins up Docker via testcontainers-modules. Plugins integrating with external services must have an integration test. Reference: core/integration/tests/connectors/postgres/postgres_sink.rs + fixtures/postgres/.
Before declaring done
cargo fmt --all
cargo sort --no-format --workspace
cargo clippy -p iggy_connector_<name>_sink --all-targets -- -D warnings
cargo test -p iggy_connector_<name>_sink
# Integration tests (filter by name; integration crate has no per-area target):
cargo test -p integration -- connectors::<backend>::<test_name>
Update core/connectors/sinks/README.md and add a sample TOML under core/connectors/runtime/example_config/connectors/.
Discussion / help: see AGENTS.md.
Version History
- 3cd0860 Current 2026-08-20 14:49


