Synopsis
I was experimenting with the new Kubernetes probes support in Spring Actuator. The official documentation suggests that we can listen to changes in readiness or liveness by registering a bean like the following:
@Component
public class ReadinessStateExporter {
@EventListener
public void onStateChange(AvailabilityChangeEvent<ReadinessState> event) {
switch (event.getState()) {
case ACCEPTING_TRAFFIC:
// create file /tmp/healthy
break;
case REFUSING_TRAFFIC:
// remove file /tmp/healthy
break;
}
}
}
However, running an application with a bean like this fails with:
java.lang.ClassCastException: org.springframework.boot.availability.LivenessState cannot be cast to org.springframework.boot.availability.ReadinessState
Details
This failure makes sense because during the application startup, Spring Boot fires both AvailabilityChangeEvent<ReadinessState>
and AvailabilityChangeEvent<LivenessState>
events.
Due to erasure, both of those events would e handled by the ReadinessStateExporter
. Quite reasonably, the LivenessState
change event should fail because we can't cast LivenessState
to ReadinessState
.
Hence the error:
java.lang.ClassCastException: org.springframework.boot.availability.LivenessState cannot be cast to org.springframework.boot.availability.ReadinessState
at ReadinessChangedListener.onStateChange(ReadinessChangedListener.java:13) ~[classes/:na]
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_161]
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_161]
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_161]
at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_161]
at org.springframework.context.event.ApplicationListenerMethodAdapter.doInvoke(ApplicationListenerMethodAdapter.java:305) ~[spring-context-5.2.6.RELEASE.jar:5.2.6.RELEASE]
at org.springframework.context.event.ApplicationListenerMethodAdapter.processEvent(ApplicationListenerMethodAdapter.java:190) ~[spring-context-5.2.6.RELEASE.jar:5.2.6.RELEASE]
at org.springframework.context.event.ApplicationListenerMethodAdapter.onApplicationEvent(ApplicationListenerMethodAdapter.java:153) ~[spring-context-5.2.6.RELEASE.jar:5.2.6.RELEASE]
at org.springframework.context.event.SimpleApplicationEventMulticaster.doInvokeListener(SimpleApplicationEventMulticaster.java:172) ~[spring-context-5.2.6.RELEASE.jar:5.2.6.RELEASE]
at org.springframework.context.event.SimpleApplicationEventMulticaster.invokeListener(SimpleApplicationEventMulticaster.java:165) ~[spring-context-5.2.6.RELEASE.jar:5.2.6.RELEASE]
at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:139) ~[spring-context-5.2.6.RELEASE.jar:5.2.6.RELEASE]
at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:403) ~[spring-context-5.2.6.RELEASE.jar:5.2.6.RELEASE]
at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:360) ~[spring-context-5.2.6.RELEASE.jar:5.2.6.RELEASE]
at org.springframework.boot.availability.AvailabilityChangeEvent.publish(AvailabilityChangeEvent.java:81) ~[spring-boot-2.3.0.RELEASE.jar:2.3.0.RELEASE]
at org.springframework.boot.availability.AvailabilityChangeEvent.publish(AvailabilityChangeEvent.java:67) ~[spring-boot-2.3.0.RELEASE.jar:2.3.0.RELEASE]
at org.springframework.boot.context.event.EventPublishingRunListener.started(EventPublishingRunListener.java:103) ~[spring-boot-2.3.0.RELEASE.jar:2.3.0.RELEASE]
at org.springframework.boot.SpringApplicationRunListeners.started(SpringApplicationRunListeners.java:71) ~[spring-boot-2.3.0.RELEASE.jar:2.3.0.RELEASE]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:321) [spring-boot-2.3.0.RELEASE.jar:2.3.0.RELEASE]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1237) [spring-boot-2.3.0.RELEASE.jar:2.3.0.RELEASE]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1226) [spring-boot-2.3.0.RELEASE.jar:2.3.0.RELEASE]
at DemoApplication.main(DemoApplication.java:10) [classes/:na]
I guess we probably should change the documentation as the sample code makes the Spring App to fail at startup.
Also, we could listen to AvailabilityState
:
@Component
public class ReadinessChangedListener {
@EventListener
public void onStateChange(AvailabilityChangeEvent<AvailabilityState> event) {
// check if it's liveness or readiness or anything else
}
}
Please kindly let me know what you think of this. Cheers!
Comment From: philwebb
I was under the impression that ApplicationListenerMethodAdapter
would deal with generics correctly. Do you by any chance have a sample application that reproduces the problem?
Comment From: alimate
Sure. This a simple sample application https://github.com/alimate/boot-probes
Comment From: philwebb
Thanks for the detailed report and the sample app. I've tracked this down and things should work as expected with the next release. In the meantime, I'm afraid you'll have to use your workaround.
Comment From: lifejwang11
Sure. This a simple sample application https://github.com/alimate/boot-probes
hello,i see this problem to understand the source code,try this without changing the source code,I hope that will be helpful.
@EventListener
public void onStateChange(AvailabilityChangeEvent<AvailabilityState> event) {
if (event.getState() instanceof LivenessState){
switch ((LivenessState)event.getState()){
case CORRECT:
System.out.println("CORRECT");
break;
case BROKEN:
System.out.println("BROKEN");
break;
default:
}
}else if(event.getState() instanceof ReadinessState){
switch ((ReadinessState)event.getState()){
case ACCEPTING_TRAFFIC:
System.out.println("ACCEPTING_TRAFFIC");
break;
case REFUSING_TRAFFIC:
System.out.println("REFUSING_TRAFFIC");
break;
default:
}
}
}
Comment From: alimate
@lifejwang11 Definitely helpful. Thanks. Moreover, this bug apparently is going to be fixed by 2.4.0 release.
Comment From: bclozel
@alimate the fix is scheduled for 2.3.2