Search code examples
springrabbitmqamqpspring-amqp

Spring-amqp-QueueBuilder should hold ref to Queue and not String name


I have 2 anonymous queues. They both are setup with ___Declarables extends AbstractAmqpDeclarables which are instantiated as @Bean on startup.

But at the time of startup - rabbitMQ hasnt given the queues a name, so one of the 2 queues gets 2 bindings and the other queue gets no binding. If QueueBuilder held a reference to queueObject and not name this object creation might be delayed enough to have rabbitMQ generate the name.

This is for a rabbitMQ generated name on the format amq.gen-<random> and not spring-amqp generated name on the format spring.gen-<random>

Edit: if I call QueueBuilder.durable(); instead of QueueBuilder.durable(queueName); the generated name becomes spring.gen-<random> instead of amq.gen-<random>

Minimal reproducible example:

  1. Producer
@Bean("amqpEmitterEventPublisherAdapter")
public EventPublisher<OurChangeEvent, Boolean> emitterEventsPublisherAdapter(
@Qualifier("amqpTemplate") RabbitTemplate rabbitTemplate
) {
final var publisher = new OurAmqpProducerImpl(rabbitTemplate);
return new OurEventAmqpPublisherAdapter(
    publisher,
    amqpConfigProps.getGlobalExchangeName(),
    ourEventsConfigProps.getOurEventsBindingPrefix(),
    ourEventsConfigProps.getOurEmitEventsBindingDataSuffix(),
    amqpConfigProps.isEnabled()
);
}
  1. Producer - with different suffix - a new binding
@Bean("amqpEmitterEventPublisherAdapter2")
public EventPublisher<OurChangeEvent, Boolean> emitterEventsPublisherAdapter2(
@Qualifier("amqpTemplate") RabbitTemplate rabbitTemplate
) {
final var publisher = new OurAmqpProducerImpl(rabbitTemplate);
return new OurEventAmqpPublisherAdapter(
    publisher,
    amqpConfigProps.getGlobalExchangeName(),
    ourEventsConfigProps.getOurEventsBindingPrefix(),
    ourEventsConfigProps.getOurDifferentSuffix(),
    amqpConfigProps.isEnabled()
);
}
  1. Declarables - this is the problem. Name of queue is "". Ok with 1 declarable. Not with 2 declarables.
    @Bean("emitEventsDeclarables")
    public Declarables emitEventsDeclarables(
        @Qualifier("mainAmqpAdmin") RabbitAdmin admin,
        @Qualifier("GlobalAmqpExchange") Exchange exchange
    ) {
        final var bindingKey =
            ourEventsConfigProps.getOurEventsBindingPrefix() +
                ".*." +
                ourEventsConfigProps.getOurEmitEventsBindingDataSuffix();
        final var cfg = new OurEventsExclusiveDeclarables(
            exchange,
            ourEventsConfigProps.getOurEmitEventsQueueName(), //=""
            bindingKey,
            true
        );

        final var declarables = cfg.declarables();
        for (Declarable d : declarables.getDeclarables()) {
            d.setAdminsThatShouldDeclare(admin);
        }
        return declarables;
    }
  1. Declarable has queue Name also ""
@Bean("emitEventsDeclarables2")
public Declarables emitEventsDeclarables2(
@Qualifier("mainAmqpAdmin") RabbitAdmin admin,
@Qualifier("GlobalAmqpExchange") Exchange exchange
) {
final var bindingKey =
    ourEventsConfigProps.getOurEventsBindingPrefix() +
        ".*." +
        ourEventsConfigProps.getOurDifferentSuffix();
final var cfg = new OurDifferentExclusiveDeclarables(
    exchange,
    ourEventsConfigProps.getOurEmitEventsQueueName(), //=""
    bindingKey,
    true
);

final var declarables = cfg.declarables();
for (Declarable d : declarables.getDeclarables()) {
    d.setAdminsThatShouldDeclare(admin);
}
return declarables;
}

Hence we got 2 declarables with anonymous queue name. Now the Spring-amqp-QueryBuilder gets confused. One of the 2 anonymous queues gets both bindings, and the other queue gets no binding.

public class OurEventsExclusiveDeclarables extends AbstractAmqpDeclarables {
    private final boolean isSingleActiveConsumer;

    public OurEventsExclusiveDeclarables(
        Exchange exchange,
        String queueName,
        String bindingKey,
        boolean isSingleActiveConsumer
    ) {
        this.exchange = exchange;
        this.queueName = queueName;
        this.bindingKey = bindingKey;
        this.isSingleActiveConsumer = isSingleActiveConsumer;
    }

    /** Exclusive queue with no name.
     * @return Declarables
     */
    @Override
    protected Declarables declarables() {
        QueueBuilder queueBuilder = 
 //Here is the problem - QueryBuilder is built too soon. RabbitMQ has not had time to generate a name
QueueBuilder.durable(queueName).exclusive();
//This will result in a spring generated name - not a RabbitMQ generated name
//QueueBuilder.durable().exclusive();
        System.out.println("binding queue name:" + queueName+ " to exchange:" + exchange.getName() + " with binding key:" + bindingKey);
        if (isSingleActiveConsumer) {
            queueBuilder.singleActiveConsumer();
        }
        final Queue queue = queueBuilder.build();
        final Binding binding = BindingBuilder.bind(queue).to(exchange).with(bindingKey).noargs();
        return new Declarables(queue, binding);
    }
}

Solution

  • Yeah - looks like the BindingBuilder.DestinationConfigurer needs to keep a reference to the Queue object rather than extracting the name during the build process.

    Please open an issue on GitHub.

    Fix will be in the next release, in the meantime, this is a work around:

    @Bean
    Binding b1(Queue q1, DirectExchange ex) {
        return new Binding(q1.getName(), DestinationType.QUEUE, "ex", "foo", Collections.emptyMap()) {
    
            @Override
            public String getDestination() {
                return q1.getActualName();
            }
    
        };
    }