-
-
Notifications
You must be signed in to change notification settings - Fork 311
Description
JobRunr Version
8.4.2 (master branch, reproducible since Jackson 3 support was added)
JDK Version
GraalVM CE 21+ (Native Image). Works correctly on HotSpot JVM.
Your SQL / NoSQL database
Any (not database-specific — the bug is in JSON deserialization)
What happened?
When deserializing FailedState on GraalVM Native Image, the primitive boolean doNotRetry field causes:
IllegalArgumentException: Can not set boolean field org.jobrunr.jobs.states.FailedState.doNotRetry to java.lang.Integer
Expected: FailedState deserializes correctly with doNotRetry preserving its true/false value.
Root cause: Jackson 3 (tools.jackson.databind) uses MethodHandle.invokeExact for field setting. On GraalVM Native Image, the MethodHandle adaptation chain that converts (Object, Object)V → (FailedState, boolean)V has a bug that corrupts Boolean into Integer before reaching UnsafeBooleanFieldAccessorImpl.set().
Key observations:
- The deserialized value itself is correctly
java.lang.Boolean— Jackson's error handler confirms this. - The corruption happens inside the MethodHandle chain, not during deserialization.
- Both
true/falsevalues fail equally — this is not a coercion issue. CoercionConfig(Integer → Boolean)does NOT fix it (confirmed — see branch).
How to reproduce?
- Create a
FailedStatewithdoNotRetry = true(e.g., from aJobRunrExceptionwithisProblematicAndDoNotRetry()) - Serialize to JSON with
Jackson3JsonMapper - Deserialize back on GraalVM Native Image
Jackson3JsonMapper jsonMapper = new Jackson3JsonMapper();
// Round-trip: serialize then deserialize
JobRunrException exception = JobRunrException.problematicConfigurationException("Problematic config");
FailedState original = new FailedState("Job failed", exception);
String json = jsonMapper.serialize(original);
FailedState deserialized = jsonMapper.deserialize(json, FailedState.class);
// ^ throws IllegalArgumentException on GraalVM Native Image
// Direct JSON deserialization also fails:
String directJson = """
{
"@class": "org.jobrunr.jobs.states.FailedState",
"state": "FAILED",
"createdAt": "2024-01-15T10:30:00Z",
"message": "Job failed",
"exceptionType": "org.jobrunr.JobRunrException",
"exceptionMessage": "Problematic config",
"stackTrace": "org.jobrunr.JobRunrException: Problematic config",
"doNotRetry": true
}
""";
FailedState fromJson = jsonMapper.deserialize(directJson, FailedState.class);
// ^ same IllegalArgumentException on Native ImageA test suite to reproduce this bug is available in my fork:
tests/e2e-native-jackson3
git clone https://github.com/blogcin/jobrunr.git
cd jobrunr
git checkout fix/donotretry-with-tests
./gradlew :tests:e2e-native-jackson3:nativeTestRelevant log output
java.lang.IllegalArgumentException: Can not set boolean field org.jobrunr.jobs.states.FailedState.doNotRetry to java.lang.Integer
at java.base/jdk.internal.reflect.UnsafeBooleanFieldAccessorImpl.set(UnsafeBooleanFieldAccessorImpl.java)
at java.base/java.lang.reflect.Field.set(Field.java)
at java.base/java.lang.invoke.MethodHandle.invokeExact(MethodHandle.java)
at tools.jackson.databind.deser.impl.FieldProperty.set(FieldProperty.java)
at tools.jackson.databind.deser.impl.FieldProperty.deserializeAndSet(FieldProperty.java)
at tools.jackson.databind.deser.BeanDeserializer.deserializeFromObject(BeanDeserializer.java)