Skip to content

Commit f4a3715

Browse files
authored
fix: restore Jackson 2.x property order in RqueueRedisSerializer to prevent stale processing-queue entries after 3.x → 4.x upgrade (#300)
* fix: restore Jackson 2.x property order in RqueueRedisSerializer to prevent stale processing-queue entries after 3.x → 4.x upgrade * build: bump version to 4.0.0-RC10 * feat: add rqueue.serialization.property.order property to control JSON field ordering Introduces RqueueRedisSerializer.PropertyOrder enum (ALPHABETICAL | DECLARATION) and wires it via rqueue.serialization.property.order (default: ALPHABETICAL). ALPHABETICAL uses Jackson 3.x alphabetical ordering, the native default for RQueue 4.x deployments. No configuration change required for new installs. DECLARATION uses declaration order, matching the Jackson 2.x behaviour of RQueue 3.x. Set this when upgrading from 3.x with messages still in Redis queues, as switching while messages are in-flight causes unexpected retries. The setting is applied in RqueueListenerBaseConfig before any Redis template is created (overriding RedisUtils providers when DECLARATION is requested), and flows through RqueueConfig to RqueueInternalPubSubChannel so all serialiser instances in the application use the same order. Docs: configuration.md and migrations.md updated with property description, accepted values, and the 3.x → 4.x migration warning. Assisted-By: Claude Sonnet 4.6
1 parent 406c8b1 commit f4a3715

9 files changed

Lines changed: 498 additions & 7 deletions

File tree

build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ ext {
8484

8585
subprojects {
8686
group = "com.github.sonus21"
87-
version = "4.0.0-RC9"
87+
version = "4.0.0-RC10"
8888

8989
dependencies {
9090
// https://mvnrepository.com/artifact/org.springframework/spring-messaging

docs/configuration/configuration.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,19 @@ whenever Rqueue generates the message ID internally.
274274

275275
## Additional Configuration
276276

277+
- **`rqueue.serialization.property.order`**: Controls the JSON property ordering used
278+
when serialising `RqueueMessage` to Redis. Accepted values:
279+
- `ALPHABETICAL` *(default)* — alphabetical order, the Jackson 3.x native default.
280+
This is the standard setting for RQueue 4.x deployments.
281+
- `DECLARATION` — declaration order, matching the Jackson 2.x behaviour used by RQueue 3.x.
282+
Use this when upgrading from 3.x with messages still present in Redis queues.
283+
284+
{: .warning}
285+
Switching between values while messages are present in the processing queue will cause
286+
those in-flight messages to be unexpectedly retried — the visibility-timeout rescue
287+
path preserves raw bytes verbatim, so the serialisation mismatch persists across
288+
re-deliveries. Drain the processing queue before changing this setting.
289+
277290
- **`rqueue.retry.per.poll`**: Determines how many times a polled message is retried
278291
immediately if processing fails, before it is moved back to the queue for a
279292
subsequent poll. The default value is `1`. If increased to `N`, the message will

docs/migrations.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,3 +73,28 @@ ZCARD rqueue-processing::<queueName>
7373
7474
If all commands return **0**, your queues are empty and you can proceed with the
7575
migration without additional configuration.
76+
77+
---
78+
79+
## Upgrading from 3.x to 4.x
80+
81+
RQueue 4.x switched from Jackson 2.x (`com.fasterxml.jackson`) to Jackson 3.x
82+
(`tools.jackson`). Jackson 3.x defaults to **alphabetical** JSON property ordering,
83+
while Jackson 2.x used **declaration order**. Messages enqueued by 3.x and messages
84+
enqueued by 4.x therefore have different byte representations in Redis.
85+
86+
The processing queue uses byte-exact lookups (ZSCORE/ZREM) to move or acknowledge
87+
messages. If stored bytes do not match the re-serialised bytes, the lookup silently
88+
fails and the message is repeatedly re-delivered via the visibility-timeout rescue path.
89+
90+
**If you are upgrading with messages still present in Redis**, set the following
91+
property to keep using declaration order (matching what 3.x stored):
92+
93+
```properties
94+
rqueue.serialization.property.order=DECLARATION
95+
```
96+
97+
{: .warning}
98+
Changing `rqueue.serialization.property.order` while messages are present in the
99+
processing queue will cause those messages to be unexpectedly retried. Drain the processing
100+
queue before switching values.

rqueue-core/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ dependencies {
5252
api "org.apache.commons:commons-collections4:${apacheCommonCollectionVerion}"
5353
// https://mvnrepository.com/artifact/io.micrometer/micrometer-core
5454
api "io.micrometer:micrometer-core:${microMeterVersion}"
55+
testImplementation "com.fasterxml.jackson.core:jackson-databind:2.21.2"
5556
testImplementation "io.lettuce:lettuce-core:${lettuceVersion}"
5657
testImplementation "io.projectreactor:reactor-test:${projectReactorReactorTestVersion}"
5758
testImplementation project(":rqueue-test-util")

rqueue-core/src/main/java/com/github/sonus21/rqueue/config/RqueueConfig.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package com.github.sonus21.rqueue.config;
1818

19+
import com.github.sonus21.rqueue.converter.RqueueRedisSerializer;
1920
import com.github.sonus21.rqueue.models.enums.RqueueMode;
2021
import com.github.sonus21.rqueue.utils.Constants;
2122
import com.github.sonus21.rqueue.utils.StringUtils;
@@ -166,6 +167,9 @@ private static String generateBrokerId() {
166167
@Value("${rqueue.worker.registry.enabled:true}")
167168
private boolean workerRegistryEnabled;
168169

170+
@Value("${rqueue.serialization.property.order:ALPHABETICAL}")
171+
private RqueueRedisSerializer.PropertyOrder serializationPropertyOrder;
172+
169173
@Value("${rqueue.worker.registry.worker.ttl:300}")
170174
private long workerRegistryWorkerTtlInSeconds;
171175

rqueue-core/src/main/java/com/github/sonus21/rqueue/config/RqueueListenerBaseConfig.java

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import com.github.sonus21.rqueue.common.RqueueRedisTemplate;
2020
import com.github.sonus21.rqueue.converter.MessageConverterProvider;
21+
import com.github.sonus21.rqueue.converter.RqueueRedisSerializer;
2122
import com.github.sonus21.rqueue.core.RqueueBeanProvider;
2223
import com.github.sonus21.rqueue.core.RqueueMessageIdGenerator;
2324
import com.github.sonus21.rqueue.core.RqueueMessageTemplate;
@@ -37,6 +38,9 @@
3738
import org.springframework.context.annotation.Conditional;
3839
import org.springframework.data.redis.connection.ReactiveRedisConnectionFactory;
3940
import org.springframework.data.redis.connection.RedisConnectionFactory;
41+
import org.springframework.data.redis.core.RedisTemplate;
42+
import org.springframework.data.redis.serializer.RedisSerializationContext;
43+
import org.springframework.data.redis.serializer.StringRedisSerializer;
4044

4145
/**
4246
* This is a base configuration class for Rqueue, that is used in Spring and Spring boot Rqueue libs
@@ -60,6 +64,9 @@ public abstract class RqueueListenerBaseConfig {
6064
@Value("${rqueue.reactive.enabled:false}")
6165
protected boolean reactiveEnabled;
6266

67+
@Value("${rqueue.serialization.property.order:ALPHABETICAL}")
68+
private RqueueRedisSerializer.PropertyOrder serializationPropertyOrder;
69+
6370
@Value(
6471
"${rqueue.message.converter.provider.class:com.github.sonus21.rqueue.converter.DefaultMessageConverterProvider}")
6572
private String messageConverterProviderClass;
@@ -109,6 +116,31 @@ public RqueueConfig rqueueConfig(
109116
@Value("${rqueue.backend:REDIS}") Backend backend,
110117
@Value("${rqueue.version.key:__rq::version}") String versionKey,
111118
@Value("${rqueue.db.version:}") Integer dbVersion) {
119+
if (serializationPropertyOrder == RqueueRedisSerializer.PropertyOrder.DECLARATION) {
120+
RqueueRedisSerializer serializer =
121+
new RqueueRedisSerializer(RqueueRedisSerializer.PropertyOrder.DECLARATION);
122+
StringRedisSerializer keySerializer = new StringRedisSerializer();
123+
RedisUtils.redisTemplateProvider = new RedisUtils.RedisTemplateProvider() {
124+
@Override
125+
public <V> RedisTemplate<String, V> getRedisTemplate(
126+
RedisConnectionFactory redisConnectionFactory) {
127+
RedisTemplate<String, V> template = new RedisTemplate<>();
128+
template.setConnectionFactory(redisConnectionFactory);
129+
template.setKeySerializer(keySerializer);
130+
template.setValueSerializer(serializer);
131+
template.setHashKeySerializer(keySerializer);
132+
template.setHashValueSerializer(serializer);
133+
return template;
134+
}
135+
};
136+
RedisUtils.redisSerializationContextProvider =
137+
() -> RedisSerializationContext.<String, Object>newSerializationContext()
138+
.key(keySerializer)
139+
.value(serializer)
140+
.hashKey(keySerializer)
141+
.hashValue(serializer)
142+
.build();
143+
}
112144
boolean sharedConnection = false;
113145
RedisConnectionFactory connectionFactory =
114146
simpleRqueueListenerContainerFactory.getRedisConnectionFactory();

rqueue-core/src/main/java/com/github/sonus21/rqueue/converter/RqueueRedisSerializer.java

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import tools.jackson.core.JacksonException;
2626
import tools.jackson.core.JsonGenerator;
2727
import tools.jackson.databind.DefaultTyping;
28+
import tools.jackson.databind.MapperFeature;
2829
import tools.jackson.databind.ObjectMapper;
2930
import tools.jackson.databind.SerializationContext;
3031
import tools.jackson.databind.jsontype.BasicPolymorphicTypeValidator;
@@ -34,14 +35,35 @@
3435
@Slf4j
3536
public class RqueueRedisSerializer implements RedisSerializer<Object> {
3637

38+
/**
39+
* Controls JSON property ordering for {@link com.github.sonus21.rqueue.core.RqueueMessage}
40+
* serialisation. Configure via {@code rqueue.serialization.property.order}.
41+
*
42+
* <ul>
43+
* <li>{@link #ALPHABETICAL} — alphabetical order, Jackson 3.x native behaviour. This is the
44+
* default for RQueue 4.x.
45+
* <li>{@link #DECLARATION} — declaration order, matching Jackson 2.x / RQueue 3.x. Use when
46+
* upgrading from 3.x with messages still present in Redis queues.
47+
* </ul>
48+
*/
49+
public enum PropertyOrder {
50+
ALPHABETICAL,
51+
DECLARATION
52+
}
53+
3754
private final RedisSerializer<Object> serializer;
3855

3956
public RqueueRedisSerializer(RedisSerializer<Object> redisSerializer) {
4057
this.serializer = redisSerializer;
4158
}
4259

60+
/** Creates a serialiser using {@link PropertyOrder#ALPHABETICAL} (Jackson 3.x default). */
4361
public RqueueRedisSerializer() {
44-
this(new RqueueRedisSerDes());
62+
this(PropertyOrder.ALPHABETICAL);
63+
}
64+
65+
public RqueueRedisSerializer(PropertyOrder order) {
66+
this(new RqueueRedisSerDes(order));
4567
}
4668

4769
@Override
@@ -66,17 +88,20 @@ public Object deserialize(byte[] bytes) throws SerializationException {
6688
private static class RqueueRedisSerDes implements RedisSerializer<Object> {
6789
private final ObjectMapper mapper;
6890

69-
RqueueRedisSerDes() {
70-
this.mapper = SerializationUtils.getObjectMapper()
91+
RqueueRedisSerDes(PropertyOrder order) {
92+
var builder = SerializationUtils.getObjectMapper()
7193
.rebuild()
7294
.addModule(new SimpleModule().addSerializer(new NullValueSerializer()))
7395
.activateDefaultTyping(
7496
BasicPolymorphicTypeValidator.builder()
7597
.allowIfSubType(Object.class)
7698
.build(),
7799
DefaultTyping.NON_FINAL,
78-
As.PROPERTY)
79-
.build();
100+
As.PROPERTY);
101+
if (order == PropertyOrder.DECLARATION) {
102+
builder = builder.disable(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY);
103+
}
104+
this.mapper = builder.build();
80105
}
81106

82107
@Override

rqueue-core/src/main/java/com/github/sonus21/rqueue/core/RqueueInternalPubSubChannel.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,8 @@ public RqueueInternalPubSubChannel(
5757
this.rqueueConfig = rqueueConfig;
5858
this.stringRqueueRedisTemplate = stringRqueueRedisTemplate;
5959
this.rqueueBeanProvider = rqueueBeanProvider;
60-
this.rqueueRedisSerializer = new RqueueRedisSerializer();
60+
this.rqueueRedisSerializer =
61+
new RqueueRedisSerializer(rqueueConfig.getSerializationPropertyOrder());
6162
}
6263

6364
@Override

0 commit comments

Comments
 (0)