Skip to content

Commit f5239b8

Browse files
tmp save - introduce options for hash field expiration
1 parent fa81f5e commit f5239b8

File tree

12 files changed

+373
-25
lines changed

12 files changed

+373
-25
lines changed

src/main/java/org/springframework/data/redis/connection/DefaultStringRedisConnection.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import org.springframework.data.geo.Metric;
3131
import org.springframework.data.geo.Point;
3232
import org.springframework.data.redis.RedisSystemException;
33+
import org.springframework.data.redis.connection.Hash.FieldExpirationOptions;
3334
import org.springframework.data.redis.connection.convert.Converters;
3435
import org.springframework.data.redis.connection.convert.ListConverter;
3536
import org.springframework.data.redis.connection.convert.MapConverter;
@@ -2566,6 +2567,11 @@ public Long hStrLen(byte[] key, byte[] field) {
25662567
return convertAndReturn(delegate.hStrLen(key, field), Converters.identityConverter());
25672568
}
25682569

2570+
public @Nullable List<Long> expireHashField(byte[] key, org.springframework.data.redis.core.types.Expiration expiration,
2571+
FieldExpirationOptions options, byte[]... fields) {
2572+
return this.delegate.expireHashField(key, expiration, options, fields);
2573+
}
2574+
25692575
@Override
25702576
public List<Long> hExpire(byte[] key, long seconds, byte[]... fields) {
25712577
return this.delegate.hExpire(key, seconds, fields);
@@ -2606,6 +2612,11 @@ public List<Long> hTtl(byte[] key, TimeUnit timeUnit, byte[]... fields) {
26062612
return this.delegate.hTtl(key, timeUnit, fields);
26072613
}
26082614

2615+
public @Nullable List<Long> expireHashField(String key, org.springframework.data.redis.core.types.Expiration expiration,
2616+
FieldExpirationOptions options, String... fields) {
2617+
return expireHashField(serialize(key), expiration, options, serializeMulti(fields));
2618+
}
2619+
26092620
@Override
26102621
public List<Long> hExpire(String key, long seconds, String... fields) {
26112622
return hExpire(serialize(key), seconds, serializeMulti(fields));

src/main/java/org/springframework/data/redis/connection/DefaultedRedisConnection.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import org.springframework.data.geo.GeoResults;
2929
import org.springframework.data.geo.Metric;
3030
import org.springframework.data.geo.Point;
31+
import org.springframework.data.redis.connection.Hash.FieldExpirationOptions;
3132
import org.springframework.data.redis.connection.stream.ByteRecord;
3233
import org.springframework.data.redis.connection.stream.Consumer;
3334
import org.springframework.data.redis.connection.stream.MapRecord;
@@ -1527,6 +1528,14 @@ default List<Long> hpTtl(byte[] key, byte[]... fields) {
15271528
return hashCommands().hpTtl(key, fields);
15281529
}
15291530

1531+
/** @deprecated in favor of {@link RedisConnection#hashCommands()}}. */
1532+
@Override
1533+
@Deprecated
1534+
default @Nullable List<Long> expireHashField(byte[] key, org.springframework.data.redis.core.types.Expiration expiration,
1535+
FieldExpirationOptions options, byte[]... fields) {
1536+
return hashCommands().expireHashField(key, expiration, options, fields);
1537+
}
1538+
15301539
// GEO COMMANDS
15311540

15321541
/** @deprecated in favor of {@link RedisConnection#geoCommands()}}. */
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
/*
2+
* Copyright 2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.redis.connection;
17+
18+
import java.util.Objects;
19+
20+
import org.springframework.lang.Contract;
21+
import org.springframework.lang.Nullable;
22+
import org.springframework.util.ObjectUtils;
23+
24+
/**
25+
* @author Christoph Strobl
26+
* @since 3.5
27+
*/
28+
public interface Hash {
29+
30+
class FieldExpirationOptions {
31+
32+
private static final FieldExpirationOptions NONE = new FieldExpirationOptions(null);
33+
private @Nullable Condition condition;
34+
35+
FieldExpirationOptions(@Nullable Condition condition) {
36+
this.condition = condition;
37+
}
38+
39+
public static FieldExpirationOptions none() {
40+
return NONE;
41+
}
42+
43+
@Contract("_ -> new")
44+
public static FieldExpireOptionsBuilder builder() {
45+
return new FieldExpireOptionsBuilder();
46+
}
47+
48+
public @Nullable Condition getCondition() {
49+
return condition;
50+
}
51+
52+
@Override
53+
public boolean equals(Object o) {
54+
if (o == this) {
55+
return true;
56+
}
57+
if (o == null || getClass() != o.getClass()) {
58+
return false;
59+
}
60+
FieldExpirationOptions that = (FieldExpirationOptions) o;
61+
return ObjectUtils.nullSafeEquals(this.condition, that.condition);
62+
}
63+
64+
@Override
65+
public int hashCode() {
66+
return Objects.hash(condition);
67+
}
68+
69+
public static class FieldExpireOptionsBuilder {
70+
71+
@Nullable Condition condition;
72+
73+
@Contract("_ -> this")
74+
public FieldExpireOptionsBuilder nx() {
75+
this.condition = Condition.NX;
76+
return this;
77+
}
78+
79+
@Contract("_ -> this")
80+
public FieldExpireOptionsBuilder xx() {
81+
this.condition = Condition.XX;
82+
return this;
83+
}
84+
85+
@Contract("_ -> this")
86+
public FieldExpireOptionsBuilder gt() {
87+
this.condition = Condition.GT;
88+
return this;
89+
}
90+
91+
@Contract("_ -> this")
92+
public FieldExpireOptionsBuilder lt() {
93+
this.condition = Condition.LT;
94+
return this;
95+
}
96+
97+
@Contract("_ -> !null")
98+
public FieldExpirationOptions build() {
99+
return condition == null ? NONE : new FieldExpirationOptions(condition);
100+
}
101+
}
102+
103+
public enum Condition {
104+
105+
/** Set expiration only when the field has no expiration. */
106+
NX,
107+
/** Set expiration only when the field has an existing expiration. */
108+
XX,
109+
/** Set expiration only when the new expiration is greater than current one. */
110+
GT,
111+
/** Set expiration only when the new expiration is greater than current one. */
112+
LT
113+
}
114+
}
115+
}

src/main/java/org/springframework/data/redis/connection/RedisHashCommands.java

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import java.util.Set;
2222
import java.util.concurrent.TimeUnit;
2323

24+
import org.springframework.data.redis.connection.Hash.FieldExpirationOptions;
2425
import org.springframework.data.redis.core.Cursor;
2526
import org.springframework.data.redis.core.ScanOptions;
2627
import org.springframework.lang.Nullable;
@@ -253,19 +254,28 @@ public interface RedisHashCommands {
253254
@Nullable
254255
Long hStrLen(byte[] key, byte[] field);
255256

256-
/**
257-
* Set time to live for given {@code fields} in seconds.
258-
*
259-
* @param key must not be {@literal null}.
260-
* @param seconds the amount of time after which the fields will be expired in seconds, must not be {@literal null}.
261-
* @param fields must not be {@literal null}.
262-
* @return a list of {@link Long} values for each of the fields provided: {@code 2} indicating the specific field is deleted
263-
* already due to expiration, or provided expiry interval is 0; {@code 1} indicating expiration time is set/updated;
264-
* {@code 0} indicating the expiration time is not set (a provided NX | XX | GT | LT condition is not met);
265-
* {@code -2} indicating there is no such field; {@literal null} when used in pipeline / transaction.
266-
* @see <a href="https://redis.io/docs/latest/commands/hexpire/">Redis Documentation: HEXPIRE</a>
267-
* @since 3.5
268-
*/
257+
default @Nullable List<Long> expireHashField(byte[] key, org.springframework.data.redis.core.types.Expiration expiration,
258+
byte[]... fields) {
259+
return expireHashField(key, expiration, FieldExpirationOptions.none(), fields);
260+
}
261+
262+
263+
@Nullable List<Long> expireHashField(byte[] key, org.springframework.data.redis.core.types.Expiration expiration,
264+
FieldExpirationOptions options, byte[]... fields);
265+
266+
/**
267+
* Set time to live for given {@code fields} in seconds.
268+
*
269+
* @param key must not be {@literal null}.
270+
* @param seconds the amount of time after which the fields will be expired in seconds, must not be {@literal null}.
271+
* @param fields must not be {@literal null}.
272+
* @return a list of {@link Long} values for each of the fields provided: {@code 2} indicating the specific field is deleted
273+
* already due to expiration, or provided expiry interval is 0; {@code 1} indicating expiration time is set/updated;
274+
* {@code 0} indicating the expiration time is not set (a provided NX | XX | GT | LT condition is not met);
275+
* {@code -2} indicating there is no such field; {@literal null} when used in pipeline / transaction.
276+
* @see <a href="https://redis.io/docs/latest/commands/hexpire/">Redis Documentation: HEXPIRE</a>
277+
* @since 3.5
278+
*/
269279
@Nullable
270280
List<Long> hExpire(byte[] key, long seconds, byte[]... fields);
271281

src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterHashCommands.java

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616
package org.springframework.data.redis.connection.jedis;
1717

18+
import redis.clients.jedis.args.ExpiryOption;
1819
import redis.clients.jedis.params.ScanParams;
1920
import redis.clients.jedis.resps.ScanResult;
2021

@@ -26,13 +27,16 @@
2627
import java.util.concurrent.TimeUnit;
2728

2829
import org.springframework.dao.DataAccessException;
30+
import org.springframework.data.redis.connection.Hash.FieldExpirationOptions;
2931
import org.springframework.data.redis.connection.RedisHashCommands;
3032
import org.springframework.data.redis.core.Cursor;
3133
import org.springframework.data.redis.core.ScanCursor;
3234
import org.springframework.data.redis.core.ScanIteration;
3335
import org.springframework.data.redis.core.ScanOptions;
36+
import org.springframework.data.redis.core.types.Expiration;
3437
import org.springframework.lang.Nullable;
3538
import org.springframework.util.Assert;
39+
import org.springframework.util.ObjectUtils;
3640

3741
/**
3842
* Cluster {@link RedisHashCommands} implementation for Jedis.
@@ -281,16 +285,54 @@ protected ScanIteration<Entry<byte[], byte[]>> doScan(CursorId cursorId, ScanOpt
281285

282286
ScanParams params = JedisConverters.toScanParams(options);
283287

284-
ScanResult<Entry<byte[], byte[]>> result = connection.getCluster().hscan(key,
285-
JedisConverters.toBytes(cursorId),
288+
ScanResult<Entry<byte[], byte[]>> result = connection.getCluster().hscan(key, JedisConverters.toBytes(cursorId),
286289
params);
287290
return new ScanIteration<>(CursorId.of(result.getCursor()), result.getResult());
288291
}
289292
}.open();
290293
}
291294

295+
@Nullable
296+
@Override
297+
public List<Long> expireHashField(byte[] key, Expiration expiration, FieldExpirationOptions options,
298+
byte[]... fields) {
299+
300+
if (expiration.isPersistent()) {
301+
return hPersist(key, fields);
302+
}
303+
304+
if (ObjectUtils.nullSafeEquals(FieldExpirationOptions.none(), options)) {
305+
if (ObjectUtils.nullSafeEquals(TimeUnit.MILLISECONDS, expiration.getTimeUnit())) {
306+
if (expiration.isUnixTimestamp()) {
307+
return hpExpireAt(key, expiration.getExpirationTimeInMilliseconds(), fields);
308+
}
309+
return hpExpire(key, expiration.getExpirationTimeInMilliseconds(), fields);
310+
}
311+
if (expiration.isUnixTimestamp()) {
312+
return hExpireAt(key, expiration.getExpirationTimeInSeconds(), fields);
313+
}
314+
return hExpire(key, expiration.getExpirationTimeInSeconds(), fields);
315+
}
316+
317+
ExpiryOption option = ExpiryOption.valueOf(options.getCondition().name());
318+
319+
if (ObjectUtils.nullSafeEquals(TimeUnit.MILLISECONDS, expiration.getTimeUnit())) {
320+
if (expiration.isUnixTimestamp()) {
321+
return connection.getCluster().hpexpireAt(key, expiration.getExpirationTimeInMilliseconds(), option, fields);
322+
}
323+
return connection.getCluster().hpexpire(key, expiration.getExpirationTimeInMilliseconds(), option, fields);
324+
}
325+
326+
if (expiration.isUnixTimestamp()) {
327+
return connection.getCluster().hexpireAt(key, expiration.getExpirationTimeInSeconds(), option, fields);
328+
}
329+
return connection.getCluster().hexpire(key, expiration.getExpirationTimeInSeconds(), option, fields);
330+
331+
}
332+
292333
@Override
293334
public List<Long> hExpire(byte[] key, long seconds, byte[]... fields) {
335+
294336
Assert.notNull(key, "Key must not be null");
295337
Assert.notNull(fields, "Fields must not be null");
296338

@@ -368,8 +410,7 @@ public List<Long> hTtl(byte[] key, TimeUnit timeUnit, byte[]... fields) {
368410

369411
try {
370412
return connection.getCluster().httl(key, fields).stream()
371-
.map(it -> it != null ? timeUnit.convert(it, TimeUnit.SECONDS) : null)
372-
.toList();
413+
.map(it -> it != null ? timeUnit.convert(it, TimeUnit.SECONDS) : null).toList();
373414
} catch (Exception ex) {
374415
throw convertJedisAccessException(ex);
375416
}

0 commit comments

Comments
 (0)