All Downloads are FREE. Search and download functionalities are using the official Maven repository.

dev.responsive.kafka.internal.clients.ResponsiveProducer Maven / Gradle / Ivy

There is a newer version: 0.28.0
Show newest version
/*
 * Copyright 2023 Responsive Computing, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package dev.responsive.kafka.internal.clients;

import java.time.Duration;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.apache.kafka.clients.consumer.ConsumerGroupMetadata;
import org.apache.kafka.clients.consumer.OffsetAndMetadata;
import org.apache.kafka.clients.producer.Callback;
import org.apache.kafka.clients.producer.Producer;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.clients.producer.RecordMetadata;
import org.apache.kafka.common.Metric;
import org.apache.kafka.common.MetricName;
import org.apache.kafka.common.PartitionInfo;
import org.apache.kafka.common.TopicPartition;
import org.apache.kafka.common.Uuid;
import org.apache.kafka.common.errors.ProducerFencedException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class ResponsiveProducer implements Producer {
  private final Producer wrapped;
  private final List listeners;
  private final Logger logger;

  public interface Listener {
    default void onCommit() {
    }

    default void onAbort() {
    }

    default void onSendCompleted(final RecordMetadata recordMetadata) {
    }

    default void onSendOffsetsToTransaction(
        final Map offsets,
        final String consumerGroupId
    ) {
    }

    default void onClose() {
    }
  }

  public ResponsiveProducer(
      final String clientid,
      final Producer wrapped,
      final List listeners
  ) {
    this.logger = LoggerFactory.getLogger(
        ResponsiveProducer.class.getName() + "." + Objects.requireNonNull(clientid));
    this.wrapped = Objects.requireNonNull(wrapped);
    this.listeners = Objects.requireNonNull(listeners);
  }

  @Override
  public void initTransactions() {
    wrapped.initTransactions();
  }

  @Override
  public void beginTransaction() throws ProducerFencedException {
    wrapped.beginTransaction();
  }

  @Override
  @SuppressWarnings("deprecation")
  public void sendOffsetsToTransaction(
      final Map offsets,
      final String consumerGroupId
  ) throws ProducerFencedException {
    wrapped.sendOffsetsToTransaction(offsets, consumerGroupId);
    for (final var l : listeners) {
      l.onSendOffsetsToTransaction(offsets, consumerGroupId);
    }
  }

  @Override
  public void sendOffsetsToTransaction(
      final Map offsets,
      final ConsumerGroupMetadata groupMetadata
  ) throws ProducerFencedException {
    wrapped.sendOffsetsToTransaction(offsets, groupMetadata);
    for (final var l : listeners) {
      l.onSendOffsetsToTransaction(offsets, groupMetadata.groupId());
    }
  }

  @Override
  public void commitTransaction() throws ProducerFencedException {
    wrapped.commitTransaction();
    listeners.forEach(Listener::onCommit);
  }

  @Override
  public void abortTransaction() throws ProducerFencedException {
    wrapped.abortTransaction();
    listeners.forEach(Listener::onAbort);
  }

  @Override
  public Future send(final ProducerRecord record) {
    return new RecordingFuture(wrapped.send(record), listeners);
  }

  @Override
  public Future send(final ProducerRecord record, final Callback callback) {
    return new RecordingFuture(
        wrapped.send(record, new RecordingCallback(callback, listeners)), listeners
    );
  }

  @Override
  public void flush() {
    wrapped.flush();
  }

  @Override
  public List partitionsFor(final String topic) {
    return wrapped.partitionsFor(topic);
  }

  @Override
  public Map metrics() {
    return wrapped.metrics();
  }

  @Override
  public Uuid clientInstanceId(final Duration duration) {
    return wrapped.clientInstanceId(duration);
  }

  @Override
  public void close() {
    wrapped.close();
    closeListeners();
  }

  @Override
  public void close(final Duration timeout) {
    wrapped.close();
    closeListeners();
  }

  private void closeListeners() {
    // TODO(rohan): use consistent error behaviour on all callbacks - just throw up
    for (final var l : listeners) {
      try {
        l.onClose();
      } catch (final Throwable t) {
        logger.error("error during producer listener close", t);
      }
    }
  }

  private static class RecordingFuture implements Future {
    private final Future wrapped;
    private final List listeners;

    public RecordingFuture(final Future wrapped, final List listeners) {
      this.wrapped = wrapped;
      this.listeners = listeners;
    }

    @Override
    public boolean cancel(boolean mayInterruptIfRunning) {
      return wrapped.cancel(mayInterruptIfRunning);
    }

    @Override
    public boolean isCancelled() {
      return wrapped.isCancelled();
    }

    @Override
    public boolean isDone() {
      return wrapped.isDone();
    }

    @Override
    public RecordMetadata get() throws InterruptedException, ExecutionException {
      final RecordMetadata recordMetadata = wrapped.get();
      for (final var l : listeners) {
        l.onSendCompleted(recordMetadata);
      }
      return recordMetadata;
    }

    @Override
    public RecordMetadata get(final long timeout, final TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException {
      final RecordMetadata recordMetadata = wrapped.get(timeout, unit);
      for (final var l : listeners) {
        l.onSendCompleted(recordMetadata);
      }
      return recordMetadata;
    }
  }

  private static class RecordingCallback implements Callback {
    private final Callback wrapped;
    private final List listeners;

    public RecordingCallback(final Callback wrapped, final List listeners) {
      this.wrapped = wrapped;
      this.listeners = listeners;
    }

    @Override
    public void onCompletion(final RecordMetadata metadata, final Exception exception) {
      wrapped.onCompletion(metadata, exception);
      if (exception == null) {
        for (final var l : listeners) {
          l.onSendCompleted(metadata);
        }
      }
    }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy