4

I have a spring boot (v.1.57) application which uses Spring Cloud Stream (v1.3.0) and Kafka (v1.1.6). I want to be able to gracefully shut it down, i.e., when shutting down, all stream listeners (i.e., annotated with @StreamListener) should:

  1. Stop polling new messages
  2. Finish their work
  3. Commit the offset to Kafka

I noticed that there's a property called 'shutdownTimeout' in ContainerProperties (which is set to a default of 10000ms) so I've tried to modify it to 30000 by extending ConcurrentKafkaListenerContainerFactoryConfigurer class (Since it has a @ConditionalOnMissingBean annotation) via reflection like so:

@Slf4j
@Component
public class BehalfConcurrentKafkaListenerContainerFactoryConfigurer extends ConcurrentKafkaListenerContainerFactoryConfigurer {

    @Autowired
    private KafkaProperties kproperties;

    @Override
    public void configure(ConcurrentKafkaListenerContainerFactory<Object, Object> listenerContainerFactory,
                          ConsumerFactory<Object, Object> consumerFactory) {
        PropertyAccessor myAccessor = PropertyAccessorFactory.forDirectFieldAccess(this);
        myAccessor.setPropertyValue("properties", kproperties);

        ContainerProperties containerProperties = listenerContainerFactory
                .getContainerProperties();
        super.configure(listenerContainerFactory, consumerFactory);
        containerProperties.setShutdownTimeout(30000);
    }
}

But it wasn't successful. Also tried putting it (shutdownTimeout: 30000) in application.yml under the spring cloud stream binder settings, but again it didn't help.

Is there any way to control the shutdown process and achieve my goals?

Eyal Ringort
  • 601
  • 6
  • 19

1 Answers1

2

EDIT

It is no longer necessary to do this reflection hack; just add a ListenerContainerCustomizer @Bean to the application context. See here.

EDIT_END

spring-kafka 1.1.x is no longer supported; you should be using 1.3.9 with boot 1.5.x.

The current Boot 1.5.x version is 1.5.21.

You should upgrade immediately.

However, there a much newer versions of all of these projects.

Spring Cloud Stream doesn't use that factory, or the boot properties, to create its containers; it doesn't expose a mechanism to configure that property on the container.

Spring Cloud Stream 2.1 added the ListenerContainerCustomizer which allows you to customize the binding container by setting any properties on it.

I suggest you upgrade to Boot 2.1.6 and Spring Cloud Stream Germantown (2.2.0).

EDIT

This is a bit of a hack, but it should work until you can upgrade to a newer stream release...

@SpringBootApplication
@EnableBinding(Sink.class)
public class So56883620Application {

    public static void main(String[] args) {
        SpringApplication.run(So56883620Application.class, args).close();
    }

    private final CountDownLatch latch = new CountDownLatch(1);

    @StreamListener(Sink.INPUT)
    public void listen(String in) throws InterruptedException {
        this.latch.countDown();
        System.out.println(in);
        Thread.sleep(6_000);
        System.out.println("exiting");
    }

    @Bean
    public ApplicationRunner runner(KafkaTemplate<byte[], byte[]> template) {
        return args -> {
            IntStream.range(0,2).forEach(i -> template.send("mytopic", ("foo" + i).getBytes()));
            // wait for listener to start
            this.latch.await(10, TimeUnit.SECONDS);
            System.out.println("Shutting down");
        };
    }

    @Bean
    public SmartLifecycle bindingFixer(BindingService bindingService) {
        return new SmartLifecycle() {

            @Override
            public int getPhase() {
                return Integer.MAX_VALUE;
            }

            @Override
            public void stop() {
                // no op
            }

            @Override
            public void start() {
                @SuppressWarnings("unchecked")
                Map<String, Binding<?>> consumers = (Map<String, Binding<?>>) new DirectFieldAccessor(bindingService)
                        .getPropertyValue("consumerBindings");
                @SuppressWarnings("unchecked")
                Binding<?> inputBinding = ((List<Binding<?>>) consumers.get("input")).get(0);
                ((AbstractMessageListenerContainer<?, ?>) new DirectFieldAccessor(inputBinding)
                        .getPropertyValue("lifecycle.messageListenerContainer"))
                                .getContainerProperties().setShutdownTimeout(30_000L);
            }

            @Override
            public boolean isRunning() {
                return false;
            }

            @Override
            public void stop(Runnable callback) {
                callback.run();
            }

            @Override
            public boolean isAutoStartup() {
                return true;
            }
        };
    }

}
Gary Russell
  • 166,535
  • 14
  • 146
  • 179
  • Just to make clear, I'm using Kafka v1.1.6, I'm not using spring-kafka (but using spring-cloud-stream which I guess uses it in some way). – Eyal Ringort Jul 05 '19 at 15:09
  • We plan on upgrading to spring-boot 2 (whatever the latest version of it will be) in the near future (in a few months), but unfortunately we can not do it right now. If spring-cloud-stream doesn't allow us to configure that property, is there any other way of performing the graceful shutdown? Perhaps using another mechanism or custom solution? – Eyal Ringort Jul 05 '19 at 15:17
  • Regardless, you need to override the version that stream pulls in. New 1.5 boot apps created using start.spring.io do that automatically. I am off today. I'll try to see if there is a work around nrxt week. – Gary Russell Jul 05 '19 at 17:37
  • Sorry for the delay; I found a work-around - it's a bit of a hack, though. See the edit. – Gary Russell Jul 12 '19 at 17:20
  • Thank you Gary - I’ll test it with my code a will report back – Eyal Ringort Jul 15 '19 at 09:46
  • @GaryRussell could you please do let us know how we can achieve this in reactive spring cloud stream + kafka connect (Hoxton.SR6). The respective config or any documentation would help us. Thank you in anticipation. – Mansingh Shitole Sep 09 '20 at 20:24
  • It is no longer necessary to do this reflection hack; just add a `ListenerContainerCustomizer` `@Bean` to the application context. See [here](https://cloud.spring.io/spring-cloud-static/spring-cloud-stream/3.0.6.RELEASE/reference/html/spring-cloud-stream.html#_advanced_consumer_configuration). – Gary Russell Sep 10 '20 at 12:47