Polishing.

Move reading shadow copy to dedicated method, update GH issue references, add test and fix formatting.

Original Pull Request: #2955
This commit is contained in:
Christoph Strobl
2024-08-06 09:44:34 +02:00
parent 3e8081926d
commit 57c294e508
2 changed files with 114 additions and 68 deletions

View File

@@ -572,8 +572,7 @@ public class RedisKeyValueAdapter extends AbstractKeyValueAdapter
* Convert given source to binary representation using the underlying {@link ConversionService}.
*/
public byte[] toBytes(Object source) {
return source instanceof byte[] bytes ? bytes
: getConverter().getConversionService().convert(source, byte[].class);
return source instanceof byte[] bytes ? bytes : getConverter().getConversionService().convert(source, byte[].class);
}
private String toString(Object value) {
@@ -764,6 +763,7 @@ public class RedisKeyValueAdapter extends AbstractKeyValueAdapter
private final RedisOperations<?, ?> ops;
private final RedisConverter converter;
private final ShadowCopy shadowCopy;
/**
* Creates new {@link MappingExpirationListener}.
*/
@@ -784,26 +784,7 @@ public class RedisKeyValueAdapter extends AbstractKeyValueAdapter
}
byte[] key = message.getBody();
Object value = null;
if (shadowCopy != ShadowCopy.OFF) {
byte[] phantomKey = ByteUtils.concat(key,
converter.getConversionService().convert(KeyspaceIdentifier.PHANTOM_SUFFIX, byte[].class));
Map<byte[], byte[]> hash = ops.execute((RedisCallback<Map<byte[], byte[]>>) connection -> {
Map<byte[], byte[]> phantomValue = connection.hGetAll(phantomKey);
if (!CollectionUtils.isEmpty(phantomValue)) {
connection.del(phantomKey);
}
return phantomValue;
});
value = CollectionUtils.isEmpty(hash) ? null : converter.read(Object.class, new RedisData(hash));
}
Object value = readShadowCopyIfEnabled(key);
byte[] channelAsBytes = message.getChannel();
String channel = !ObjectUtils.isEmpty(channelAsBytes)
@@ -825,6 +806,35 @@ public class RedisKeyValueAdapter extends AbstractKeyValueAdapter
private boolean isKeyExpirationMessage(Message message) {
return BinaryKeyspaceIdentifier.isValid(message.getBody());
}
@Nullable
private Object readShadowCopyIfEnabled(byte[] key) {
if (shadowCopy == ShadowCopy.OFF) {
return null;
}
return readShadowCopy(key);
}
@Nullable
private Object readShadowCopy(byte[] key) {
byte[] phantomKey = ByteUtils.concat(key,
converter.getConversionService().convert(KeyspaceIdentifier.PHANTOM_SUFFIX, byte[].class));
Map<byte[], byte[]> hash = ops.execute((RedisCallback<Map<byte[], byte[]>>) connection -> {
Map<byte[], byte[]> phantomValue = connection.hGetAll(phantomKey);
if (!CollectionUtils.isEmpty(phantomValue)) {
connection.del(phantomKey);
}
return phantomValue;
});
return CollectionUtils.isEmpty(hash) ? null : converter.read(Object.class, new RedisData(hash));
}
}
private boolean keepShadowCopy() {

View File

@@ -1,74 +1,110 @@
/*
* Copyright 2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.redis.core;
import org.junit.jupiter.api.BeforeEach;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.util.ArrayList;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.core.convert.ConversionService;
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.core.convert.RedisConverter;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import java.util.ArrayList;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
/**
* @author Lucian Torje
* @author Christoph Strobl
*/
@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT)
class MappingExpirationListenerTest {
@Mock
private RedisOperations<?, ?> redisOperations;
@Mock
private RedisConverter redisConverter;
@Mock
private RedisMessageListenerContainer listenerContainer;
@Mock
private Message message;
@Mock
private RedisKeyExpiredEvent<?> event;
@Mock
private ConversionService conversionService;
@Mock private RedisOperations<?, ?> redisOperations;
@Mock private RedisConverter redisConverter;
@Mock private RedisMessageListenerContainer listenerContainer;
@Mock private Message message;
private RedisKeyValueAdapter.MappingExpirationListener listener;
private RedisKeyValueAdapter.MappingExpirationListener listener;
@Test
void testOnNonKeyExpiration() {
byte[] key = "testKey".getBytes();
when(message.getBody()).thenReturn(key);
listener = new RedisKeyValueAdapter.MappingExpirationListener(listenerContainer, redisOperations, redisConverter, RedisKeyValueAdapter.ShadowCopy.ON);
@Test // GH-2954
void testOnNonKeyExpiration() {
listener.onMessage(message, null);
byte[] key = "testKey".getBytes();
when(message.getBody()).thenReturn(key);
listener = new RedisKeyValueAdapter.MappingExpirationListener(listenerContainer, redisOperations, redisConverter,
RedisKeyValueAdapter.ShadowCopy.ON);
verify(redisOperations, times(0)).execute(any(RedisCallback.class));
}
listener.onMessage(message, null);
@Test
void testOnValidKeyExpiration() {
List<Object> eventList = new ArrayList<>();
verify(redisOperations, times(0)).execute(any(RedisCallback.class));
}
byte[] key = "abc:testKey".getBytes();
when(message.getBody()).thenReturn(key);
@Test // GH-2954
void testOnValidKeyExpirationWithShadowCopiesDisabled() {
listener = new RedisKeyValueAdapter.MappingExpirationListener(listenerContainer, redisOperations, redisConverter, RedisKeyValueAdapter.ShadowCopy.OFF);
listener.setApplicationEventPublisher(eventList::add);
listener.onMessage(message, null);
List<Object> eventList = new ArrayList<>();
verify(redisOperations, times(1)).execute(any(RedisCallback.class));
assertThat(eventList).hasSize(1);
assertThat(eventList.get(0)).isInstanceOf(RedisKeyExpiredEvent.class);
assertThat(((RedisKeyExpiredEvent) (eventList.get(0))).getKeyspace()).isEqualTo("abc");
assertThat(((RedisKeyExpiredEvent) (eventList.get(0))).getId()).isEqualTo("testKey".getBytes());
}
}
byte[] key = "abc:testKey".getBytes();
when(message.getBody()).thenReturn(key);
listener = new RedisKeyValueAdapter.MappingExpirationListener(listenerContainer, redisOperations, redisConverter,
RedisKeyValueAdapter.ShadowCopy.OFF);
listener.setApplicationEventPublisher(eventList::add);
listener.onMessage(message, null);
verify(redisOperations, times(1)).execute(any(RedisCallback.class));
assertThat(eventList).hasSize(1);
assertThat(eventList.get(0)).isInstanceOf(RedisKeyExpiredEvent.class);
assertThat(((RedisKeyExpiredEvent) (eventList.get(0))).getKeyspace()).isEqualTo("abc");
assertThat(((RedisKeyExpiredEvent) (eventList.get(0))).getId()).isEqualTo("testKey".getBytes());
}
@Test // GH-2954
void testOnValidKeyExpirationWithShadowCopiesEnabled() {
ConversionService conversionService = Mockito.mock(ConversionService.class);
List<Object> eventList = new ArrayList<>();
byte[] key = "abc:testKey".getBytes();
when(message.getBody()).thenReturn(key);
when(redisConverter.getConversionService()).thenReturn(conversionService);
when(conversionService.convert(any(), eq(byte[].class))).thenReturn("foo".getBytes());
listener = new RedisKeyValueAdapter.MappingExpirationListener(listenerContainer, redisOperations, redisConverter,
RedisKeyValueAdapter.ShadowCopy.ON);
listener.setApplicationEventPublisher(eventList::add);
listener.onMessage(message, null);
verify(redisOperations, times(2)).execute(any(RedisCallback.class)); // delete entry in index, delete phantom key
assertThat(eventList).hasSize(1);
assertThat(eventList.get(0)).isInstanceOf(RedisKeyExpiredEvent.class);
assertThat(((RedisKeyExpiredEvent) (eventList.get(0))).getKeyspace()).isEqualTo("abc");
assertThat(((RedisKeyExpiredEvent) (eventList.get(0))).getId()).isEqualTo("testKey".getBytes());
}
}