Skip to content
cccskills
FreePublished on GitHub

connector-source

Use this skill to create, modify, or review an Apache Iggy source plugin. A source polls an external system and produces stream messages, with guidance on state, acknowledgments, schemas, polling, errors, and integration tests.

OriginalShow the original description

Author a new Apache Iggy connector source plugin under core/connectors/sources/. Sources poll an external system (DB, API, queue) and produce messages into Apache Iggy streams. Load when creating, modifying, or reviewing a source crate. Use for source plugin authoring. NOT for runtime internals (see connector-runtime).

apache/iggy5,223 stars(GitHub 5,223 + cccskills 0)

Run score Not tested

See how to install

Good for

  • Creating a source that polls a database
  • Reading records from an API into an Iggy stream
  • Reviewing cursor state and acknowledgment handling

Files included (2)

  • SKILL.md11.9 KB
  • TEMPLATE.md5.7 KB

SKILL.md (original)

Review the instructions the agent will receive before you install.

Writing an Apache Iggy Connector Source

A source is a Rust cdylib that implements iggy_connector_sdk::Source and exposes FFI symbols via the source_connector! macro. The runtime calls poll() in a loop, applies transforms, encodes via the configured Schema, sends to Apache Iggy, and persists the returned ConnectorState after every successful send.

Universal connector rules (SecretString, benchmark, verbose flag, drop accounting, filter contract, exemplar patterns) live in connectors-overview. This skill covers only what's source-specific.

Contents

STOP and ask the user before

  • Changing the SDK trait surface (Source::open / poll / on_batch_result / close) - that's an SDK change.
  • Adding a long-running side task in the plugin - the runtime owns lifecycle, and orphans survive close(). Sanctioned only where the source is itself a server the runtime cannot drive, as in http_source's listener, and then only with an explicit shutdown in the last close() that awaits its tasks before returning.
  • Persisting unbounded state - State is rewritten every batch.
  • Adding a source that requires authoritative offsets external to Apache Iggy without coordinating retention.

Quick reference

  • Skeleton: TEMPLATE.md (load on demand).
  • Exemplars: random_source (minimal + canonical state tests), postgres_source (cursor / delete-after-read / processed-column modes, restart-survives-state tests), elasticsearch_source (scroll cursor), influxdb_source (time-series scan).

Hard rules

poll() signature is &self

The macro shares the source as Arc<T> across the FFI callback and forwarding loop. Signature: async fn poll(&self) -> ... - any mutable state behind tokio::sync::Mutex. Single most common new-contributor mistake.

Lock discipline

Never hold the state Mutex across upstream I/O. Build a candidate from committed state, then stage it until the runtime reports the batch result:

let mut candidate = self.state.lock().await.clone();
let rows = client.query(&sql, &[&candidate.cursor]).await?;
candidate.cursor = Some(new_cursor);
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);

Delivery acknowledgment

on_batch_result (added by #3855) is how a source learns what happened to the batch it just returned. The SDK keeps exactly one batch in flight: it will not call poll() again until this returns. By default it stops after MAX_CONSECUTIVE_NACKS (5) consecutive NACKs; a source can override batch_policy() to disable the limit when its accepted input cannot be replayed after restart. A stop is reported to the runtime only when the plugin exports the current SDK's stop callback, so rebuild older source plugins.

  • Ack means the runtime sent the batch and persisted its state. Nack means it could not confirm both, which is not the same as neither happening: a batch that reached the topic but whose state save failed is NACKed. A missing runtime result leaves the batch pending; it does not produce a synthetic NACK. A source that replays on Nack is at-least-once, not exactly-once.
  • The trait has a default no-op, which suits only a source with no staged cursor and no destructive work. If poll() advances a cursor, deletes rows, or drains an in-memory buffer, omitting this loses data silently and nothing will tell you. random_source and http_source implement it; the other shipped sources take the default and skip rows on a NACK.
  • Stage in poll(), apply on Ack, discard or replay on Nack. A source whose input is pushed to it, rather than re-readable upstream, has to hold the batch itself: see http_source's staging.
  • Returning Err from it stops the source immediately, so it is not a retry signal.

State persistence

  • ConnectorState is Vec<u8> via MessagePack (rmp_serde). Use ConnectorState::serialize(&state, NAME, id) and ConnectorState::deserialize::<State>(NAME, id).
  • poll() must not commit cursors or destructive work. Return messages with candidate state and keep the corresponding work staged.
  • The runtime sends the batch, saves its candidate state to {state_path}/source_{key}.state, then calls on_batch_result(Ack). Commit staged in-memory state and external delete or mark operations only on ACK. A NACK discards the candidate so the same data can be polled again.
  • A crash between poll() returning and state persistence leaves the prior cursor for the next poll, so downstream must tolerate at-least-once delivery.
  • Cursor sources that stage state until on_batch_result can attach state to the corresponding message batch. Control-plane state that is independent of a publish can ride an empty batch instead.
  • Return state: None for an empty poll when no watermark changed. If an empty poll advances a watermark, stage and return the new state through the same ACK handshake.
  • The runtime can still NACK an empty batch when state storage is latched or a pending checkpoint cannot resolve. Never treat hand-off as durable; on_batch_result reports the outcome.
  • Treat candidate-state serialization failure as a poll error. Do not send messages without the state needed to resume them safely.
  • Keep State small - rewritten every batch. No unbounded vecs.

The SDK allows one in-flight batch. Five consecutive NACKs stop a source using the default policy and require a manual restart. Returning Err from on_batch_result is fatal regardless of the NACK limit, so retry transient backend failures inside the callback before returning an error. If the runtime has not reported ACK or NACK after 30 seconds, the SDK warns and keeps waiting for that result without polling or replaying the batch. Once the result is received, the SDK waits for on_batch_result to finish, so the callback must bound its own connection acquisition and retry budget.

Sleep first

poll() must sleep(self.poll_interval).await before any work. Without it, an empty source spins a CPU.

Schema selection

Match ProducedMessages.schema to the bytes in messages[i].payload:

  • JSON-serialized rows → Schema::Json
  • Already-protobuf bytes → Schema::Proto
  • Already-avro bytes → Schema::Avro
  • Opaque → Schema::Raw

IDs and timestamps

  • ProducedMessage.id: Option<u128> - set when a natural ID exists (DB PK, document id). It rides the wire as the message id, which is what a consumer can dedupe an at-least-once duplicate on.
  • Iggy itself does not dedupe on that id. The server's only read of the field is core/server/src/http/wire.rs, which mints a fresh uuid when the incoming id is 0 and otherwise passes it through. Its dedup path is a per-client request-id watermark (dedup_clients_max) that never looks at this field.
  • origin_timestamp: Option<u64> - source-system event time in nanoseconds. Lets downstream sinks reason about lag.
  • timestamp and checksum are Iggy-side - leave None.

Concurrency

  • Runtime spawns ONE poll() task per source. No concurrent poll().
  • Don't spawn your own long-running Tokio tasks: the runtime owns lifecycle. The exception is a source that listens rather than polls, which has to own its listener; http_source is the worked example, and it shuts its tasks down in the last close() rather than leaving them to outlive the connector.

Errors

ScenarioVariant
Bad config in new()/open()Error::InitError
Cannot reach external system at startupError::InitError or Error::Connection
Transient fetch failure (retry-worthy)Error::Connection or Error::HttpRequestFailed
Permanent fetch failure (auth, schema gone)Error::PermanentHttpError
Row failed to serializeError::Serialization(...)
State serialization failedError::Serialization(...)

Returning Err from poll() is only logged by the SDK's FFI bridge (sdk/src/source.rs::handle_messages) - the loop continues, the next poll() runs. Connector status does NOT flip to Error from a poll failure. Status Error is set by the runtime only on transform/encode failure, Iggy send failure, or state save failure (runtime/src/source.rs::source_forwarding_loop calls to context.sources.set_error). To surface a poll failure as unhealth, raise it through the metric counter or escalate to Error::InitError from open().

Logging

info!("Opened <connector> connector ID: {}, endpoint: {}", self.id, ...);
info!("Restored state for <connector> ID: {id}, cursor: {:?}", ...);
debug!("Polled {} rows for <connector> ID: {}", rows.len(), self.id);
warn!("Transient fetch failure for <connector> ID: {}, will retry: {error}", self.id);
error!("Failed to <op> for <connector> ID: {}, error: {error}", self.id);
info!("Closed <connector> connector ID: {}, total produced: {}", self.id, ...);

Iggy consumer-loop labels use literal API names (offset=, current_offset=).

Common pitfalls

  1. async fn poll(&mut self) - won't compile. Use &self + Mutex<State>.
  2. Holding state.lock() across the fetch I/O - blocks close(), causes shutdown timeouts.
  3. Forgetting to sleep - 100% CPU on idle source.
  4. Committing a cursor or deleting source data in poll() - stage it and wait for ACK.
  5. Unbounded data in State - rewritten every batch. keep O(constant).
  6. std::sync::Mutex - blocks the executor. Use tokio::sync::Mutex.
  7. Not setting ProducedMessage.id when a stable ID exists - leaves a consumer nothing to dedupe a replayed duplicate on. It does not make the write idempotent server-side, because nothing there reads it.
  8. Spawning side tasks - the runtime owns the scheduler. The one exception is a source that listens rather than polls and must own its listener (see Concurrency and the STOP list); it owes an explicit shutdown in the last close() that awaits its tasks.

Tests

Mandatory four canonical source state tests (see connector-testing for the full pattern). Copy from sources/random_source/src/lib.rs::tests. Plus config defaults, payload building, schema selection.

Integration tests under core/integration/tests/connectors/<backend>/ for any source backed by external infra. Use #[iggy_harness] + a TestFixture backed by testcontainers-modules. Reference: core/integration/tests/connectors/postgres/postgres_source.rs (multi-mode tests) + restart.rs (state survives restart). Exercise both ACK and NACK paths when the source stages cursors or destructive work.

Before declaring done

cargo fmt --all
cargo sort --no-format --workspace
cargo clippy -p iggy_connector_<name>_source --all-targets -- -D warnings
cargo test -p iggy_connector_<name>_source

# Integration tests:
cargo test -p integration -- connectors::<backend>::<test_name>

Update core/connectors/sources/README.md and add a sample TOML under core/connectors/runtime/example_config/connectors/.


Discussion / help: see AGENTS.md.

Reviews

No reviews yet. Tell others what you thought.

Comments

No login needed. Share how it worked for you, ask a question, or note where it did not run.

No comments yet. Be the first to write one.

Comments are published right away. Do not include personal information such as email addresses or phone numbers. Up to 2 URLs.

0 / 1,000 characters

This form is protected by reCAPTCHA, and the Google Privacy Policy and Terms of Service apply.

More skills from this repository

connector-review

FreeSummary

Review an Apache Iggy connectors pull request, branch, or commit range for defects in connector code and related integration tests. Independent reviewers examine focused areas, and a separate validation pass checks each finding before producing a report.

  • Reviewing a connectors pull request
  • Checking a branch before merge
  • Validating reported connector defects
apache/iggy5.2KUpdated Oct 29, 2026

Use this skill to work on the Apache Iggy Connectors host runtime: the process that loads plugins and moves data between Iggy streams and them. It covers lifecycle, FFI dispatch, configuration, state storage, HTTP control, metrics, and transforms.

  • Changing how connector plugins are loaded or managed
  • Updating source and sink message forwarding
  • Modifying state storage or configuration providers
apache/iggy5.2KUpdated Oct 29, 2026

connector-sdk

FreeSummary

Use this skill when changing the Apache Iggy Connectors SDK, such as adding a schema, encoder, decoder, or error type. It maps related updates and compatibility concerns across the SDK and its plugins.

  • Adding a schema format to the SDK
  • Implementing a stream encoder or decoder
  • Changing SDK traits or FFI macros
apache/iggy5.2KUpdated Oct 29, 2026

connector-sink

FreeSummary

Use this skill to create, modify, or review an Apache Iggy sink plugin. A sink receives stream messages and sends them to an external system, with guidance on configuration, payloads, retries, batching, lifecycle, and integration tests.

  • Creating a sink that writes messages to a database
  • Sending Iggy stream batches to an HTTP endpoint
  • Reviewing sink retries, payload handling, or lifecycle
apache/iggy5.2KUpdated Oct 29, 2026

Write and review tests for Apache Iggy connector plugins, including sinks, sources, transforms, the SDK, and runtime. Follow the project’s unit-test naming and state-restoration patterns, and structure integration tests around real services running in Docker containers.

  • Adding unit tests for a connector plugin
  • Testing source state restoration after restart
  • Writing integration tests against a real backend
apache/iggy5.2KUpdated Oct 29, 2026

Add configuration-driven transforms to Apache Iggy connectors. Create message operations that modify or filter fields, convert formats, or unwrap envelopes, then connect them to the SDK configuration and test expected, pass-through, filtered, and invalid-config cases.

  • Adding a field-level message transform
  • Filtering messages by field values
  • Converting between message formats
apache/iggy5.2KUpdated Oct 29, 2026

See all skills by apache

Report a problem with this skill