6

I am using Spring Cloud Stream and want to programmatically create and bind channels. My use case is that during application startup I receive the dynamic list of Kafka topics to subscribe to. How can I then create a channel for each topic?

Jeff Cook
  • 7,956
  • 36
  • 115
  • 186
Nikem
  • 5,716
  • 3
  • 32
  • 59
  • You can check this answer for a similar question here: http://stackoverflow.com/questions/40485421/spring-cloud-stream-to-support-routing-messages-dynamically – Ilayaperumal Gopinathan Jan 17 '17 at 03:56
  • 1
    That answer is for outgoing messages. I need the incoming ones :( – Nikem Jan 17 '17 at 08:23
  • 1
    did you find the answer? I'm having the same issue. It'd be great if you could point me in the right direction. Thanks – CCC Mar 16 '19 at 22:08
  • 1
    @CCC, nope, I haven't. My requirements have changed, so not an issue for me any more. – Nikem Mar 18 '19 at 06:13

6 Answers6

3

I ran into similar scenario recently and below is my sample of creating SubscriberChannels dynamically.

    ConsumerProperties consumerProperties = new ConsumerProperties();
    consumerProperties.setMaxAttempts(1); 
    BindingProperties bindingProperties = new BindingProperties();
    bindingProperties.setConsumer(consumerProperties);
    bindingProperties.setDestination(retryTopic);
    bindingProperties.setGroup(consumerGroup);

    bindingServiceProperties.getBindings().put(consumerName, bindingProperties);
    SubscribableChannel channel = (SubscribableChannel)bindingTargetFactory.createInput(consumerName);
    beanFactory.registerSingleton(consumerName, channel);
    channel = (SubscribableChannel)beanFactory.initializeBean(channel, consumerName);
    bindingService.bindConsumer(channel, consumerName);
    channel.subscribe(consumerMessageHandler);
sash
  • 1,124
  • 2
  • 15
  • 32
  • 2
    can you share the full source? – CCC Mar 06 '19 at 23:28
  • @sash , please,tell where did you find this code? did it work for you? – Yan Khonski May 14 '19 at 14:38
  • @YanKhonski sorry but I dont have the actual srouce with me anymore :( I did write the above after debugging and understanding how the consumers are created. I will try to recreate it when time permits. – sash May 16 '19 at 14:25
  • Sure, no problem, I solved it and posted my solution. Anyway, if you recall, please share. – Yan Khonski May 16 '19 at 19:14
0

I had to do something similar for the Camel Spring Cloud Stream component. Perhaps the Consumer code to bind a destination "really just a String indicating the channel name" would be useful to you?

In my case I only bind a single destination, however I don't imagine it being much different conceptually for multiple destinations.

Below is the gist of it:

    @Override
    protected void doStart() throws Exception {
        SubscribableChannel bindingTarget = createInputBindingTarget();
        bindingTarget.subscribe(message -> {
            // have your way with the received incoming message
        });

        endpoint.getBindingService().bindConsumer(bindingTarget,
                endpoint.getDestination());

       // at this point the binding is done
    }

    /**
     * Create a {@link SubscribableChannel} and register in the
     * {@link org.springframework.context.ApplicationContext}
     */
    private SubscribableChannel createInputBindingTarget() {
        SubscribableChannel channel = endpoint.getBindingTargetFactory()
                .createInputChannel(endpoint.getDestination());
        endpoint.getBeanFactory().registerSingleton(endpoint.getDestination(), channel);
        channel = (SubscribableChannel) endpoint.getBeanFactory().initializeBean(channel,
                endpoint.getDestination());
        return channel;
    }

See here for the full source for more context.

Donovan Muller
  • 3,822
  • 3
  • 30
  • 54
0

I had a task where I did not know the topics in advance. I solved it by having one input channel which listens to all the topics I need.

https://docs.spring.io/spring-cloud-stream/docs/Brooklyn.RELEASE/reference/html/_configuration_options.html

Destination

The target destination of a channel on the bound middleware (e.g., the RabbitMQ exchange or Kafka topic). If the channel is bound as a consumer, it could be bound to multiple destinations and the destination names can be specified as comma-separated String values. If not set, the channel name is used instead.

So my configuration

spring:
  cloud:
    stream:
      default:
        consumer:
          concurrency: 2
          partitioned: true
      bindings:
        # inputs
        input:
          group: application_name_group
          destination: topic-1,topic-2
          content-type: application/json;charset=UTF-8

Then I defined one consumer which handles messages from all these topics.

@Component
@EnableBinding(Sink.class)
public class CommonConsumer {

    private final static Logger logger = LoggerFactory.getLogger(CommonConsumer.class);

    @StreamListener(target = Sink.INPUT)
    public void consumeMessage(final Message<Object> message) {
        logger.info("Received a message: \nmessage:\n{}", message.getPayload());
        // Here I define logic which handles messages depending on message headers and topic.
        // In my case I have configuration which forwards these messages to webhooks, so I need to have mapping topic name -> webhook URI.
    }
}

Note, in your case it may not be a solution. I needed to forward messages to webhooks, so I could have configuration mapping.

I also thought about other ideas. 1) You kafka client consumer without Spring Cloud.

2) Create a predefined number of inputs, for example 50.

input-1
intput-2
...
intput-50

And then have a configuration for some of these inputs.

Related discussions

We use Spring Cloud 2.1.1 RELEASE

Yogesh Prajapati
  • 4,770
  • 2
  • 36
  • 77
Yan Khonski
  • 12,225
  • 15
  • 76
  • 114
0
MessageChannel messageChannel = createMessageChannel(channelName);
messageChannel.send(getMessageBuilder().apply(data));

public MessageChannel createMessageChannel(String channelName) {
return (MessageChannel) applicationContext.getBean(channelName);}

public Function<Object, Message<Object>> getMessageBuilder() {
return payload -> MessageBuilder
.withPayload(payload)
.setHeader(MessageHeaders.CONTENT_TYPE, MimeTypeUtils.APPLICATION_JSON)
.build();}
  • 2
    Please don't post only code as answer, but also provide an explanation what your code does and how it solves the problem of the question. Answers with an explanation are usually more helpful and of better quality, and are more likely to attract upvotes. – Pouria Hemi Nov 21 '20 at 15:12
0

Based on Sash's answer, I have written a demo that can dynamically register consumers.

  @Autowired
  void dynamicBinding(BindingService bindingService,
                      BindingServiceProperties bindingServiceProperties,
                      RabbitExtendedBindingProperties rabbitExtendedBindingProperties,
                      BindingTargetFactory channelFactory) {
    final var CONSUMER_NAME = "onRefresh";               // SCS Consumer Name
    final var RABBIT_EXCHANGE = "ouroboros.app.control"; // RabbitMQ Exchange Name

    // RabbitMQ Properties
    var rabbitConsumerProperties = new RabbitConsumerProperties();
    var consumerProperties = new ExtendedConsumerProperties<>(rabbitConsumerProperties);

    var rabbitBindingProperties = new RabbitBindingProperties();
    var bindingProperties = new BindingProperties();

    rabbitConsumerProperties.setExchangeType("fanout");
    rabbitConsumerProperties.setAcknowledgeMode(AcknowledgeMode.MANUAL);
    rabbitConsumerProperties.setRequeueRejected(false);
    rabbitConsumerProperties.setRepublishToDlq(true);

    rabbitConsumerProperties.setAutoBindDlq(true);
    rabbitConsumerProperties.setDeclareDlx(true);
    rabbitConsumerProperties.setDeadLetterExchange("ouroboros.app.control.dlx");
    rabbitConsumerProperties.setDeadLetterExchangeType("topic");

    consumerProperties.populateBindingName(CONSUMER_NAME);

    bindingProperties.setDestination(RABBIT_EXCHANGE);
    bindingProperties.setConsumer(consumerProperties);

    rabbitBindingProperties.setConsumer(rabbitConsumerProperties);

    bindingServiceProperties.getBindings().put(CONSUMER_NAME, bindingProperties);
    rabbitExtendedBindingProperties.setBindings(Collections.singletonMap(CONSUMER_NAME, rabbitBindingProperties));

    // Channel Name same as SCS Consumer name
    var channel = (SubscribableChannel)channelFactory.createInput(CONSUMER_NAME);

    // bind consumer
    bindingService.bindConsumer(channel, CONSUMER_NAME);

    // subscribe channel
    channel.subscribe((message) -> {
      logger.info("onRefresh: {}", message.getPayload());
      throw new MessagingException("reject"); // How to gracefully reject consuming messages?
    });
  }
BeMxself
  • 1
  • 1
-1

For the incoming messages, you can explicitly use BinderAwareChannelResolver to dynamically resolve the destination. You can check this example where router sink uses binder aware channel resolver.

Ilayaperumal Gopinathan
  • 4,099
  • 1
  • 13
  • 12
  • 1
    I don't understand. I want to subscribe to topics, whose names I know only in runtime. I don't want to send/route messages. – Nikem Jan 18 '17 at 08:53
  • ok, Sorry; I misunderstood. The `dynamic` destination support is for binding the producer only. I believe this feature is yet to be addressed and tracked as a part here: https://github.com/spring-cloud/spring-cloud-stream/issues/746 – Ilayaperumal Gopinathan Jan 18 '17 at 11:29
  • @IlayaperumalGopinathan, do you know if this was ever addressed? – CCC Mar 18 '19 at 20:29