Skip to main content

connector-runtime

Modify the Apache Iggy Connectors runtime (`core/connectors/runtime/`) - the host process that loads plugins, manages lifecycle, and bridges Apache Iggy streams to plugins via FFI. Use when changing plugin loading, the FFI dispatch, the sink/source manager, state storage, config providers, the HTTP control API, metrics, or transforms wiring. NOT for plugin code.

소스 정보

저장소
apache/iggy
최근 소스 활동
2026년 9월 22일 10:30
감지된 SKILL.md 언어
영어
스타
5,067
포크
472

설치 방법

기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.

소스 파일 검토

설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.

SKILL.md 표시 중

SKILL.md
소스 지침 · 읽기 전용 미리보기
name
connector-runtime
description
Modify the Apache Iggy Connectors runtime (`core/connectors/runtime/`) - the host process that loads plugins, manages lifecycle, and bridges Apache Iggy streams to plugins via FFI. Use when changing plugin loading, the FFI dispatch, the sink/source manager, state storage, config providers, the HTTP control API, metrics, or transforms wiring. NOT for plugin code.
# Modifying the Apache Iggy Connectors Runtime Runtime is the host. Loads `.so`/`.dylib`/`.dll` plugins, drives their lifecycle, ferries messages between Apache Iggy and plugins, exposes `/stats`, `/metrics`, control HTTP API. Bugs here affect every plugin. > **Universal connector rules** (benchmark, SecretString, verbose, drop accounting, exemplar patterns) live in > [connectors-overview](../connectors-overview/SKILL.md). This skill covers runtime internals only. ## Contents - [STOP and ask the user before](#stop-and-ask-the-user-before) - [File map](#file-map) - [Architecture invariants](#architecture-invariants) - [State storage (`state.rs`)](#state-storage-staters) - [Source forwarding loop](#source-forwarding-loop) - [Sink consumption loop](#sink-consumption-loop) - [Logging format](#logging-format) - [The `verbose` flag](#the-verbose-flag) - [Config provider rules](#config-provider-rules) - [Error categorization](#error-categorization) - [Metrics (`metrics.rs`)](#metrics-metricsrs) - [Drop accounting (wired sites)](#drop-accounting-wired-sites) - [Hard rules](#hard-rules) - [Common pitfalls](#common-pitfalls) - [Before declaring done](#before-declaring-done) ## STOP and ask the user before - Changing FFI `#[repr(C)]` layouts (`SourceApi`, `SinkApi`, `RawMessages`, headers) - existing plugin `.so`s misalign. - Modifying `state.rs` save protocol (atomic tmp + rename + fsync ordering) - corruption risk. - Renaming the default consumer group `iggy-connect-sink-{key}` - operators hold offsets there. - Hot-reloading or unloading a `.so` - mid-FFI tasks would segfault. Architectural change required. - Changing the SerDe split (postcard for FFI payloads, serde_json for configs, rmp_serde for state) - downstream incompat. ## File map ```text runtime/src/ ├── main.rs Entry, plugin path resolution, SourceApi/SinkApi FFI structs ├── sink.rs Sink lifecycle, Iggy consumer wiring, FFI consume calls ├── source.rs Source lifecycle, flume forwarding, state save loop ├── stream.rs Stream + consumer/producer setup ├── transform.rs Loads transforms from config, applies them in chain ├── state.rs FileStateProvider, atomic ConnectorState file I/O ├── context.rs Per-instance context (Iggy client, metrics, config) ├── error.rs RuntimeError with as_code() for HTTP responses ├── log.rs init_logging (text/JSON x telemetry-on/off matrix) + LOG_CALLBACK ├── benchmark.rs emit_sink_event / emit_source_event + as_micros ├── metrics.rs Prometheus families + gauges + stage histograms + label caches ├── stats.rs /stats endpoint payload assembly ├── manager/{mod,sink,source}.rs SinkManager / SourceManager, status, restart_guard ├── configs/ │ ├── runtime.rs RuntimeConfig + LoggingConfig + LogFormat │ ├── connectors.rs ConnectorConfig, SinkConfig, SourceConfig (verbose + benchmark) │ └── connectors/{local,http}_provider.rs └── api/ HTTP control + observability endpoints ``` ## Architecture invariants Load-bearing assumptions. Breaking them silently corrupts data or hangs. ### Plugin instance identity - Each instance gets a globally unique `plugin_id: u32` from `PLUGIN_ID: AtomicU32` in `main.rs`. Never reused. - Same `.so` may load many instances. each gets a fresh `plugin_id` but shares the `Container` (dlopen handle) via `Arc` (`manager/{sink,source}.rs`). - All FFI calls keyed by `plugin_id`. Mixing IDs = data to the wrong plugin. ### FFI pointer lifetimes - Config and state are `(*const u8, usize)`. Valid for the call duration only. Plugin must copy if it needs the bytes past the call. SDK macros do this correctly. don't bypass. - `LogCallback` lifetime spans the whole process - kept static in `log.rs`. Don't make it dynamic. ### Container ownership - `dlopen2::wrapper::Container<{Sink,Source}Api>` (defined in `main.rs`) is shared via `Arc` across all instances of the same `.so`. - Arc must outlive every spawned task that calls into the plugin. Manager holds the Arc in `{Sink,Source}Details` for the connector lifetime. - Unloading a `.so` while a task is mid-FFI-call would segfault. Runtime does not unload. Hot reload needs a rearchitect. ### Plugin manager state - `SinkManager` / `SourceManager` use `DashMap<String, Arc<Mutex<Details>>>`. Key = TOML `key` field. - Status transitions (`Starting → Running → Stopped/Error`) update running counters only on transitions to/from `Running`. See `SinkManager::update_status`. - `restart_guard: Arc<Mutex<()>>` prevents concurrent restart of the same connector. `restart_connector()` uses `try_lock()` - returns OK without restart if busy. Intentional anti-thundering-herd. ### Serialization split - FFI message payloads (`TopicMetadata`, `MessagesMetadata`, `RawMessages`, headers): `postcard` (compact, stable wire). - Connector configs passed to plugin: `serde_json` (TOML in, JSON over FFI - human-editable). - `ConnectorState` bytes inside plugins: `rmp_serde` (MessagePack - compact, opaque to runtime). Don't mix. ## State storage (`state.rs`) - File path: `{state_path}/source_{connector_key}.state`. Mode `0o600` on Unix. - `save()` is crash-atomic. `rename(2)` is atomic in the namespace (no observer sees a half-renamed file). the preceding `sync_data` on the tmp file plus the post-rename parent-dir `sync_all` are what make a crash leave either the old or new content - never truncated. The parent-dir fsync failure is propagated as `CannotWriteStateFile` (not swallowed) so a lost rename surfaces instead of silently restarting the source from scratch. A `Mutex<()>` serializes concurrent saves on the same provider. - Save after every successful Apache Iggy send. Save failure logs + continues. next batch retries. - `load()` returns `Ok(None)` for missing or empty files. After a `NotFound` read, re-stats the parent directory: missing parent -> `Err(CannotOpenStateFile)` so a broken state path fails at init rather than masquerading as "fresh start". - Sinks have no state - only sources. ## Source forwarding loop 1. `iggy_source_handle(id, send_callback)` - plugin registers itself. 2. Plugin polls + invokes `send_callback(plugin_id, ptr, len)`. 3. Callback runs in the SDK macro's spawned async task. Pushes postcard `ProducedMessages` into a `flume` channel keyed by `plugin_id` in `SOURCE_SENDERS: Lazy<DashMap<u32, SourceSenderEntry>>` (`pub(crate)`). `SourceSenderEntry` wraps the sender + a pre-extracted owned `Counter` (the `errors` series, `Arc<AtomicU64>` inside). The FFI callback bumps errors on deserialize or channel-closed failure with one relaxed atomic - no `Family` lookup, no `Arc<Metrics>` handle. 4. `source_forwarding_loop` pulls from the channel, deserializes, applies transforms, encodes via `StreamEncoder`, sends to Iggy producer. 5. On success, save returned `ConnectorState` via `FileStateProvider`. **Shutdown ordering (`manager/source.rs::stop_connector`):** 1. Call `iggy_source_close` FIRST. It blocks until the plugin's polling task stops, so no new send callbacks fire after it returns. 2. `cleanup_sender(plugin_id)` NEXT - dropping the channel sender makes the forwarding task's `recv_async()` resolve with `Disconnected` and exit cleanly, instead of blocking until the abort timeout. 3. Finally await spawned handlers with `tokio::time::timeout`. On timeout, `handle.abort()` + drain - prevents leaked tasks colliding with the next `start_connector` (a late `file.save()` could otherwise race the new instance). The silent-drop branch in `handle_produced_messages` only covers the window between close and cleanup. Gotchas: - `SOURCE_SENDERS` must be cleaned up on connector close or memory leaks (channel + task). - `spawn_source_handler` wraps the outer `iggy_source_handle(id, callback)` FFI call in `tokio::task::spawn_blocking()`. That call returns quickly - the SDK macro internally `runtime.spawn`s an async `handle_messages` task and returns. **`send_callback` invocations come from that async task, not from `spawn_blocking`.** A long-running synchronous poll inside the plugin would block one Tokio worker. async polls don't. - No timeout on the registration call - a plugin whose `iggy_source_handle` never returns stalls one blocking worker for the process lifetime. ## Sink consumption loop Per `[[streams]]` entry per sink: 1. Build / ensure consumer group `iggy-connect-sink-{key}` (`connect` matches the docker image name `apache/iggy-connect` - see `runtime/src/sink.rs::default_consumer_group`). Overridable via the stream config's `consumer_group`. 2. Spawn one task per topic (`spawn_consume_tasks`). 3. Poll Iggy → batch messages (default `batch_length = 1000`, `poll_interval = 5ms`). A `consumer.next()` `Err` (transport/decode at the Iggy client boundary) bumps `errors` and `continue`s - offset already auto-committed via `AutoCommit::When(AutoCommitWhen::PollingMessages)`, message effectively dropped. 4. Decode via `Schema::decoder()`. Decode failure logs + bumps `errors`. `messages_filtered` is reserved for the transform filter contract (`Ok(None)`). 5. Apply transforms (chain). A transform `Err` is logged, bumps `errors`, and drops only that message (drop-and-continue, mirroring the source) - one bad payload never kills the batch. `Ok(None)` filters the message - bumps `messages_filtered{connector_type="sink"}`. 6. Postcard-encode batch as `RawMessages`. Headers as a separate postcard blob. Per-message failures (missing field, payload conversion, header serialization) bump `errors` + skip. Batch-level failures (postcard serialization of metadata / `RawMessages`) propagate `Err` -> `spawn_consume_tasks` wrapper bumps `errors` + `set_error`. The total histogram and benchmark emit still fire for the failed batch before the `Err` propagates. 7. Call `iggy_sink_consume(id, topic_meta, messages_meta, messages)`. 8. Non-zero return → log + `errors` increment. Per-message counters (decode errors, transform errors, filters, field/serialize drops) are accumulated and flushed once per batch via `inc_errors_by_with_labels` / `inc_messages_filtered_with_labels`, not one `Family` lookup per message. Stages (both labels snake_case): `decode` (schema decode loop), `prepare` (transform + serialize = total - decode - ffi), `ffi` (plugin consume call), `total`. Same `decode` / `prepare` meaning as the source side. Gotchas: - Batch flushes on `current_offset != message_offset` (gap) OR batch full - higher latency for low-volume topics. - Auto-commit happens on poll - idempotency at the sink protects against re-delivery on crash before plugin write. ## Logging format `LoggingConfig` in `configs/runtime.rs` exposes `format: LogFormat` (`Text` default, `Json`), env-addressable via `IGGY_CONNECTORS_LOGGING_FORMAT=json`. `log::init_logging` matches on `(telemetry.enabled, format)` and installs the right `fmt::layer()`/`fmt::layer().json()` + OpenTelemetry layer. OTel pipeline untouched by `format` - only the stdout layer switches. `fmt::layer()` and `fmt::layer().json()` are different concrete types, so the stdout layer is built once as a `Box<dyn Layer<_>>` (via `.boxed()`) by matching on `format`, then a single 2-arm branch on `telemetry.enabled` attaches the OTel layers. When extending, keep the boxed-layer + 2-arm shape rather than re-expanding into a 4-way match. ## The `verbose` flag `SinkConfig` and `SourceConfig` in `configs/connectors.rs` carry `verbose: bool` (default false). Runtime threads it to `spawn_consume_tasks` / source equivalent to gate per-batch log promotion: ```rust if verbose { info!("Processing {messages_count} messages for sink connector with ID: {plugin_id}"); } else { debug!("Processing {messages_count} messages for sink connector with ID: {plugin_id}"); } ``` New per-batch logging follows the same pattern. Default to `debug!`, upgrade to `info!` when `verbose`. No third level. ## Config provider rules ### Local (`configs/connectors/local_provider.rs`) - Discovery walk reads `*.toml` from `config_dir`. Skips hidden files (`.`) and `Cargo.toml`. - Filename: `{key}_{type}[_v{N}].toml` (e.g., `postgres_sink_v2.toml`). - One TOML deserialized into `SinkConfig` or `SourceConfig`. Grouped by `key`. Highest `version` wins unless `.active_versions.toml` overrides. - `.active_versions.toml` is NOT read via the discovery walk (hidden). Read via direct path through `LocalConnectorsConfigProvider::active_versions_file_path`. ### HTTP (`configs/connectors/http_provider.rs`) - `reqwest_middleware` + `RetryTransientMiddleware`. Retries 5xx + connection errors. Never 4xx (`PermanentHttpError` convention). - Default URL templates documented in `runtime/README.md`. Configurable via TOML. ### Env-var overrides via `ConfigEnv` derive `SinkConfig`, `SourceConfig`, and inner structs derive `ConfigEnv` (`configs_derive::ConfigEnv`). Generates env-var addressability as `IGGY_CONNECTORS_<TYPE>_<KEY>_<FIELD>` for primitive fields. Used heavily by integration tests to inject testcontainer ports - see `core/integration/tests/connectors/fixtures/postgres/container.rs` for env-var constants. Mark new compound fields `#[config_env(skip)]`, leaf primitives `#[config_env(leaf)]`. ### Versioning - Configs carry `version: u64`. Local provider auto-increments if absent. HTTP provider trusts the server. - Restart on version change uses `restart_guard`. ## Error categorization `RuntimeError` (`error.rs`) carries `as_code()` for HTTP API responses. | Class | Fatal? | Example | | ------------------ | --------------------------- | -------------------------------- | | Config load | yes (process exit) | Bad TOML, missing required field | | Iggy client init | yes | Auth failure at startup | | State dir create | yes | Permission denied | | Plugin .so resolve | per-plugin (`FailedPlugin`) | Missing file | | Plugin open FFI | per-plugin (status = Error) | Plugin returned non-zero | | Message decode | per-message (skip) | Bad protobuf bytes | | Iggy send | per-batch (metric) | Network blip | | State save | per-batch (log) | Disk full | Fatal errors propagate to `main` and exit. Per-connector / per-message errors are isolated. ## Metrics (`metrics.rs`) 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. - `messages_filtered_total` - intentional drops via transform `Ok(None)`.
GitHub에서 보기
이 SKILL.md는 매우 커서 SkillsMP가 여기에는 첫 섹션만 미리 보여줍니다. GitHub에서 보기