Apache Iggyのコネクター変更を複数の専門視点から厳しくレビューし、指摘ごとに独立した検証を行って、修正が必要な問題を整理します。
- コネクターのPRをマージ前に確認したいとき
- ブランチの変更を専門視点でレビューしたいとき
- 指摘の妥当性を独立して検証したいとき
Apache Iggyとコネクタープラグインをつなぐランタイムの開発に使います。読み込みやデータ転送、状態保存、設定、HTTP API、メトリクスなどの変更箇所を把握できます。
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 Connectorsのランタイム側を変更するときに、プラグインの読み込みやライフサイクル管理、Apache Iggyとのデータ転送、状態保存、設定プロバイダー、HTTP制御API、メトリクス、変換処理の接続を案内します。ソースからプラグインへの転送ループや、シンクがメッセージを受け取る処理、設定ファイルと環境変数の扱い、エラーや処理状況の記録など、関連するコードの構成と設計上の前提を確認できます。変更時にデータの欠落や互換性の問題を起こしやすい箇所や、処理の終了・再起動に関する注意も示します。
プラグイン本体ではなく、それを読み込んで管理するホスト側を変更したいときに向いています。ソースやシンクの転送処理、設定の取得、状態ファイルの保存、監視用メトリクスなど、ランタイム内部の仕組みを調べながら作業できます。
プラグインの実装ではなく、ランタイム内部が対象です。FFIのデータ構造、状態保存手順、既定のコンシューマーグループ名、シリアライズ方式の変更は、既存プラグインや保存済みオフセットとの互換性に関わります。
この紹介文は、公開されている SKILL.md をもとに AI(OpenAI Codex)が作成しました。正確な仕様は下の原文を確認してください。
インストールする前に、エージェントに与えられる指示の中身を確認できます。
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. This skill covers runtime internals only.
state.rs)verbose flagmetrics.rs)#[repr(C)] layouts (SourceApi, SinkApi, RawMessages, headers) - existing plugin .sos misalign.state.rs save protocol (atomic tmp + rename + fsync ordering) - corruption risk.iggy-connect-sink-{key} - operators hold offsets there..so - mid-FFI tasks would segfault. Architectural change required.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
Load-bearing assumptions. Breaking them silently corrupts data or hangs.
plugin_id: u32 from PLUGIN_ID: AtomicU32 in main.rs. Never reused..so may load many instances. each gets a fresh plugin_id but shares the Container (dlopen handle) via Arc (manager/{sink,source}.rs).plugin_id. Mixing IDs = data to the wrong plugin.(*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.dlopen2::wrapper::Container<{Sink,Source}Api> (defined in main.rs) is shared via Arc across all instances of the same .so.{Sink,Source}Details for the connector lifetime..so while a task is mid-FFI-call would segfault. Runtime does not unload. Hot reload needs a rearchitect.SinkManager / SourceManager use DashMap<String, Arc<Mutex<Details>>>. Key = TOML key field.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.TopicMetadata, MessagesMetadata, RawMessages, headers): postcard (compact, stable wire).serde_json (TOML in, JSON over FFI - human-editable).ConnectorState bytes inside plugins: rmp_serde (MessagePack - compact, opaque to runtime).Don't mix.
state.rs){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.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".iggy_source_handle(id, send_callback) - plugin registers itself.send_callback(plugin_id, ptr, len).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.source_forwarding_loop pulls from the channel, deserializes, applies transforms, encodes via StreamEncoder, and sends to the Iggy producer. The SDK warns after 30 seconds without a result but keeps the same batch pending; it does not poll or enqueue another copy.ConnectorState via FileStateProvider, then return the batch result to the plugin. A result NACK may cause replay with capped backoff.SOURCE_SENDERS and update status and metrics. A self-stop reports Error and removes the source from the running gauge; normal shutdown reports Stopped.Shutdown ordering (manager/source.rs::stop_connector):
iggy_source_close FIRST. It blocks until the plugin's polling task stops, so no new send callbacks fire after it returns.cleanup_sender(plugin_id) NEXT - dropping the channel sender lets the forwarding task finish any queued batches before recv_async() resolves with Disconnected and exits cleanly, instead of blocking until the abort timeout.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.spawns 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.iggy_source_handle never returns stalls one blocking worker for the process lifetime.Per [[streams]] entry per sink:
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.spawn_consume_tasks).batch_length = 1000). A consumer.next() Err (transport/decode at the Iggy client boundary) bumps errors and continues. A rejected poll committed nothing, so the consumer retries the same offset. Polls commit via AutoCommit::When(AutoCommitWhen::PollingMessages), so only a reply lost after the server committed can skip its batch.Schema::decoder(). Decode failure logs + bumps errors. messages_filtered is reserved for the transform filter contract (Ok(None)).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"}.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.iggy_sink_consume(id, topic_meta, messages_meta, messages).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:
message_offset == current_offset), when the batch is full, OR before a message of another partition, because the plugin gets one partition id per batch. max_wait and min_count on the stream make the server collect larger replies.IggyConsumer::shutdown, which rewinds over the batches the consumer holds. Without either step, the next run skips messages that the polls committed.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.
verbose flagSinkConfig 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:
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.
configs/connectors/local_provider.rs)*.toml from config_dir. Skips hidden files (.) and Cargo.toml.{key}_{type}[_v{N}].toml (e.g., postgres_sink_v2.toml).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.configs/connectors/http_provider.rs)reqwest_middleware + RetryTransientMiddleware. Retries 5xx + connection errors. Never 4xx (PermanentHttpError convention).runtime/README.md. Configurable via TOML.ConfigEnv deriveSinkConfig, 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)].
version: u64. Local provider auto-increments if absent. HTTP provider trusts the server.restart_guard.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.rs)All families labeled by connector_key + connector_type (histogram adds stage):
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).errors_total - unexpected drops (decode/encode/build failure, missing field, ...) + batch-level failures, one per failed run.sink_runs_total - FFI consume() calls for sink batches, one per contiguous payload-variant run. Divide by stage_duration_seconds_count{stage="total"} for runs per batch.iggy_connector_stage_duration_seconds{stage} (snake_case stage labels - prepare, ffi, decode, iggy_send, state_save, total). Buckets STAGE_BUCKETS_SECONDS. Always populated regardless of any flag. One sample per batch for every stage; ffi is summed over the batch's runs. Scraped at /metrics when [http.metrics] enabled = true.iggy_connectors_{sources,sinks}_{total,running}./stats JSON surface mirrors counters per-connector via ConnectorStats (sdk/api.rs): messages_filtered, errors, kind-specific counters.
When adding a metric:
Metrics struct + init, register with name + help text. Never end a Counter family's registered name in _total: the encoder appends it and the series renders _total_total. Gauges get no suffix, so a gauge name may end in _total literally.EncodeLabelSet struct + label enum (hand-impl EncodeLabelValue for snake_case values - the derive emits PascalCase).fn() -> Histogram to Family::new_with_constructor.mod tests with given_*_when_*_should_* BDD names.Per-message drops in the batch loops are counted into a local u64 and flushed once after the loop via inc_errors_by_with_labels / inc_messages_filtered_with_labels - one Family lookup per batch, not per message. One-shot drops outside a loop (e.g. consumer.next() Err) still call inc_errors_with_labels directly. Hot path uses pre-built SinkLabels/SourceLabels. &str-based wrappers are #[cfg(test)]-only.
sink.rs::consume_messages - consumer.next() Errsink.rs::process_messages - decode, transform Err (drop-and-continue), missing required fields, payload conversion, header serialization (accumulated, flushed once per batch)sink.rs::spawn_consume_tasks task wrapper - bumps once on consume_messages Errsource.rs::source_forwarding_loop - payload decode, prepare (transform/encode) failure, Iggy send Err, state save Errsource.rs::process_messages - transform Err (logs + bumps errors + continue. does NOT propagate, so one bad payload doesn't flip the connector to permanent ERROR), transform encode failure, build_iggy_message failuresource.rs::handle_produced_messages - postcard deserialize failure, sender.send channel-closedFilter case bumps messages_filtered via inc_messages_filtered_with_labels. Adding a new drop path: mirror this pattern.
spawn_blocking only for FFI registration (already in place).0 success, non-zero failure.LOG_CALLBACK, PLUGIN_ID, SOURCE_SENDERS.cleanup_sender(id) with shutdown for sources (avoid flume leak). Order: close FFI -> cleanup sender -> drain/abort tasks.restart_guard.try_lock() - no thundering-herd regression.INSTANCES (in SDK macro) from runtime side - impossible by design. only the macro touches it.iggy_*_open with a duplicate ID - returns -1. Means runtime forgot to iggy_*_close first.DashMap shard guard across .await - lockups. Pattern: .get().clone(), drop guard, await.#[repr(C)] types without an SDK version bump - existing plugins misalign on postcard::from_bytes.cargo fmt --all
cargo sort --no-format --workspace
cargo clippy -p iggy-connectors --all-targets -- -D warnings
cargo test -p iggy-connectors
# Sanity-build in-tree plugins:
cargo build -p iggy_connector_stdout_sink -p iggy_connector_random_source
# Runtime-focused integration tests:
cargo test -p integration -- connectors::runtime::
# Smoke-test with example config:
IGGY_CONNECTORS_CONFIG_PATH=core/connectors/runtime/example_config/config.toml \
cargo run --bin iggy-connectors
Update runtime/README.md if endpoints, env vars, or config schema change.
Discussion / help: see AGENTS.md.
まだレビューはありません。使ってみた感想をお寄せください。
ログインなしで書き込めます。使ってみた感想、質問、動かなかった環境などを気軽にどうぞ。
まだコメントはありません。最初のコメントを書いてみませんか。
Apache Iggyのコネクター変更を複数の専門視点から厳しくレビューし、指摘ごとに独立した検証を行って、修正が必要な問題を整理します。
Apache Iggy Connectors SDKのスキーマ、エンコーダー、デコーダー、エラー型やFFIなどを追加・変更するときに、関連箇所と互換性への影響を整理するスキルです。
Apache Iggyのストリームからメッセージを受け取り、データベースやHTTPなど外部システムへ送るSinkプラグインの作成・変更を支援します。
データベース、API、キューなど外部システムをポーリングし、Apache Iggyのストリームへメッセージを送るSourceプラグインの作成・変更を支援します。
Apache Iggyのコネクター向けに、ユニットテストや実サービスを使う統合テストを設計・作成し、命名規則や状態復元などのテスト慣例に沿って確認します。
Apache Iggyコネクターのメッセージにフィールド追加・削除や形式変換などの処理を加える変換機能を設計し、設定連携やテストまで整えます。