ryerraguntla commented on code in PR #4258:
URL: https://github.com/apache/iggy/pull/4258#discussion_r4088476645
##########
gateways/kafka/src/bridge/iggy_bridge/topics.rs:
##########
@@ -217,4 +255,140 @@ impl IggyBridge {
Err(err) => Err(err),
}
}
+
+ /// Looks up `kafka_topic`, resolved through the configured
+ /// [`TopicMapping`](crate::bridge::topic_map::TopicMapping), without
creating it.
+ ///
+ /// Returns `Ok(None)` when either the mapped stream or the mapped topic
doesn't exist -
+ /// callers (`CreateTopics`' existence check, `Metadata`'s lookup) treat
both the same way:
+ /// nothing answers to this Kafka-side name yet.
+ ///
+ /// # Errors
+ ///
+ /// Returns [`BridgeError::InvalidKafkaTopicName`] if `kafka_topic` fails
Kafka's own
+ /// topic-naming rules. Returns [`BridgeError::Timeout`] if a call takes
longer than
+ /// `REQUEST_TIMEOUT`. Returns [`BridgeError::Iggy`] for connectivity/auth
failures.
+ pub async fn get_kafka_topic(
+ &self,
+ kafka_topic: &str,
+ ) -> Result<Option<TopicDetails>, BridgeError> {
+ validate_kafka_topic_name("kafka_topic", kafka_topic)?;
+ let (stream_name, topic_name) =
self.config.topic_mapping.resolve(kafka_topic);
+ let stream_id =
Identifier::named(stream_name).map_err(BridgeError::Iggy)?;
+ let topic_id =
Identifier::named(topic_name).map_err(BridgeError::Iggy)?;
+ // No separate get_stream probe: get_topic already answers Ok(None)
when the stream
+ // itself is missing (see high_watermarks' own doc on this same fact),
so a probe first
+ // would just pay a second round trip to learn something this one call
already tells us.
+ with_request_timeout(self.client.get_topic(&stream_id,
&topic_id)).await
+ }
+
+ /// Creates the Iggy stream/topic backing `kafka_topic`, or reports that
it already exists.
+ ///
+ /// Atomic from this call's perspective, unlike a separate existence check
+ /// ([`Self::get_kafka_topic`]) followed by
[`Self::ensure_stream_and_topic`]: that sequence
+ /// has a TOCTOU window between the two calls, and
`ensure_stream_and_topic`'s own idempotent
+ /// contract would then absorb a second concurrent caller's create into a
silent `Ok`, so both
+ /// callers see success for a `CreateTopics` request Kafka promises
exactly one `NONE` for.
+ /// Here, the create attempt itself is the existence check: no separate
read precedes it, and
+ /// [`TopicCreationOutcome::AlreadyExists`] comes from the server's own
rejection of the write,
+ /// not from an earlier read that could already be stale by the time this
call's write lands.
+ ///
+ /// # Errors
+ ///
+ /// Same as [`Self::ensure_stream_and_topic`], except an already-existing
topic is reported as
+ /// [`TopicCreationOutcome::AlreadyExists`] rather than
[`BridgeError::PartitionCountMismatch`].
+ /// `CreateTopics` is not an upsert, so a pre-existing topic is never
itself an error here,
+ /// regardless of whether its partition count matches `partition_count`.
+ pub async fn create_kafka_topic(
+ &self,
+ kafka_topic: &str,
+ partition_count: u32,
+ ) -> Result<TopicCreationOutcome, BridgeError> {
+ validate_kafka_topic_name("kafka_topic", kafka_topic)?;
+ if partition_count == 0 {
+ return Err(BridgeError::InvalidPartitionCount {
+ kafka_topic: kafka_topic.to_string(),
+ });
+ }
+ let (stream_name, topic_name) =
self.config.topic_mapping.resolve(kafka_topic);
+ let stream_id = self.ensure_stream(stream_name).await?;
+
+ let options = TopicCreateOptions {
+ partitions_count: Some(partition_count),
+ ..TopicCreateOptions::default()
+ };
+ match with_request_timeout(self.client.create_topic(&stream_id,
topic_name, &options)).await
+ {
+ Ok(created) => {
+ info!("created Iggy topic '{topic_name}' with
{partition_count} partitions");
+ // Same postcondition check ensure_topic's own create branch
makes: partitions_count
+ // is a hard argument (Some(partition_count), never None), so
a mismatch here means
+ // a future server-side clamp/cap, not a client input problem
- fails loudly instead
+ // of silently reporting Created under a broken contract.
+ if created.partitions_count != partition_count {
+ return Err(BridgeError::PartitionCountMismatch {
+ topic: kafka_topic.to_string(),
+ existing: created.partitions_count,
+ requested: partition_count,
+ });
+ }
+ Ok(TopicCreationOutcome::Created)
+ }
+ Err(BridgeError::Iggy(IggyError::TopicNameAlreadyExists(_, _))) =>
{
+ Ok(TopicCreationOutcome::AlreadyExists)
+ }
+ Err(err) => Err(err),
+ }
+ }
+
+ /// Every Kafka-visible topic: the target of every configured
+ /// [`TopicMapping`](crate::bridge::topic_map::TopicMapping) override that
actually exists in
+ /// Iggy, plus every topic in the default stream that isn't itself one of
those override
+ /// targets - checked so an overridden topic is never listed twice, once
under its Kafka-side
+ /// name and once under its raw Iggy name.
+ ///
+ /// An Iggy stream this bridge has no mapping rule pointing at (neither
the default stream nor
+ /// any override's target) holds data no Kafka client ever named -
deliberately excluded, the
+ /// same way a real Kafka broker never reports storage it doesn't own.
+ ///
+ /// # Errors
+ ///
+ /// Returns [`BridgeError::Timeout`] if a call takes longer than
`REQUEST_TIMEOUT`. Returns
+ /// [`BridgeError::Iggy`] for connectivity/auth failures.
+ pub async fn list_kafka_topics(&self) -> Result<Vec<KafkaTopicMetadata>,
BridgeError> {
+ let default_stream = self.config.topic_mapping.default_stream();
+ let mut default_stream_override_targets: HashSet<&str> =
HashSet::new();
+ let mut results = Vec::new();
+
+ for (kafka_topic, over) in self.config.topic_mapping.overrides() {
+ if over.stream == default_stream {
+ default_stream_override_targets.insert(over.topic.as_str());
+ }
+ if let Some(details) = self.get_kafka_topic(kafka_topic).await? {
+ results.push(KafkaTopicMetadata {
+ kafka_topic: kafka_topic.to_string(),
+ partitions_count: details.partitions_count,
+ });
+ }
+ }
+
+ let default_stream_id =
Identifier::named(default_stream).map_err(BridgeError::Iggy)?;
+ if with_request_timeout(self.client.get_stream(&default_stream_id))
+ .await?
+ .is_some()
+ {
+ let topics =
with_request_timeout(self.client.get_topics(&default_stream_id)).await?;
+ for topic in topics {
+ if
default_stream_override_targets.contains(topic.name.as_str()) {
Review Comment:
Now also collects override keys and skips default-stream topics matching
either set.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]