Remote Events migrated from Spring Bus to Spring Cloud Stream (#2563)

* Remote Events migrated from Spring Bus to Spring Cloud Stream

---------

Co-authored-by: vasilchev <vasil.ilchev@bosch.com>
This commit is contained in:
Vasil Ilchev
2025-07-30 16:58:00 +03:00
committed by GitHub
parent 10da0288d9
commit 4a8e60764f
49 changed files with 1147 additions and 461 deletions

View File

@@ -30,7 +30,6 @@ import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.hawkbit.im.authentication.SpPermission;
import org.eclipse.hawkbit.repository.TenantConfigurationManagement;
import org.eclipse.hawkbit.repository.event.remote.RemoteTenantAwareEvent;
import org.eclipse.hawkbit.repository.event.remote.TenantConfigurationDeletedEvent;
import org.eclipse.hawkbit.repository.event.remote.entity.RemoteEntityEvent;
import org.eclipse.hawkbit.repository.exception.InsufficientPermissionException;
@@ -58,7 +57,6 @@ import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cloud.bus.ServiceMatcher;
import org.springframework.context.ApplicationContext;
import org.springframework.context.event.EventListener;
import org.springframework.core.convert.support.ConfigurableConversionService;
@@ -89,9 +87,7 @@ public class JpaTenantConfigurationManagement implements TenantConfigurationMana
private final CacheManager cacheManager;
private final AfterTransactionCommitExecutor afterCommitExecutor;
private ServiceMatcher serviceMatcher;
protected JpaTenantConfigurationManagement(
public JpaTenantConfigurationManagement(
final TenantConfigurationRepository tenantConfigurationRepository,
final TenantConfigurationProperties tenantConfigurationProperties,
final CacheManager cacheManager, final AfterTransactionCommitExecutor afterCommitExecutor,
@@ -103,11 +99,6 @@ public class JpaTenantConfigurationManagement implements TenantConfigurationMana
this.applicationContext = applicationContext;
}
@Autowired(required = false)
public void setServiceMatcher(final ServiceMatcher serviceMatcher) {
this.serviceMatcher = serviceMatcher;
}
@Override
@CacheEvict(value = "tenantConfiguration", key = "#configurationKeyName")
@Transactional
@@ -186,10 +177,6 @@ public class JpaTenantConfigurationManagement implements TenantConfigurationMana
*/
@EventListener
public void onTenantConfigurationDeletedEvent(final TenantConfigurationDeletedEvent event) {
if (!shouldProcessRemoteTenantAwareEvent(event)) {
return;
}
evictCacheEntryByKeyIfPresent(event.getConfigKey());
}
@@ -200,10 +187,6 @@ public class JpaTenantConfigurationManagement implements TenantConfigurationMana
*/
@EventListener
public void onTenantConfigurationRemoteEntityEvent(final RemoteEntityEvent<TenantConfiguration> event) {
if (!shouldProcessRemoteTenantAwareEvent(event)) {
return;
}
event.getEntity().ifPresent(tenantConfiguration -> evictCacheEntryByKeyIfPresent(tenantConfiguration.getKey()));
}
@@ -391,8 +374,4 @@ public class JpaTenantConfigurationManagement implements TenantConfigurationMana
cache.evictIfPresent(key);
}
}
private boolean shouldProcessRemoteTenantAwareEvent(final RemoteTenantAwareEvent event) {
return serviceMatcher == null || !serviceMatcher.isFromSelf(event) && serviceMatcher.isForSelf(event);
}
}

View File

@@ -51,7 +51,6 @@ import org.eclipse.hawkbit.repository.event.remote.entity.ActionUpdatedEvent;
import org.eclipse.hawkbit.repository.jpa.utils.MapAttributeConverter;
import org.eclipse.hawkbit.repository.model.Action;
import org.eclipse.hawkbit.repository.model.ActionStatus;
import org.eclipse.hawkbit.repository.model.BaseEntity;
import org.eclipse.hawkbit.repository.model.DistributionSet;
import org.eclipse.hawkbit.repository.model.Rollout;
import org.eclipse.hawkbit.repository.model.RolloutGroup;

View File

@@ -11,49 +11,42 @@ package org.eclipse.hawkbit.repository.event.remote;
import static org.junit.jupiter.api.Assertions.fail;
import java.util.LinkedHashMap;
import java.util.Map;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.eclipse.hawkbit.event.BusProtoStuffMessageConverter;
import org.eclipse.hawkbit.event.EventProtoStuffMessageConverter;
import org.eclipse.hawkbit.repository.event.TenantAwareEvent;
import org.eclipse.hawkbit.repository.jpa.AbstractJpaIntegrationTest;
import org.junit.jupiter.api.BeforeEach;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.bus.event.RemoteApplicationEvent;
import org.springframework.cloud.bus.jackson.BusJacksonAutoConfiguration;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.integration.support.MutableMessageHeaders;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHeaders;
import org.springframework.messaging.converter.AbstractMessageConverter;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.util.ClassUtils;
import org.springframework.util.MimeType;
import org.springframework.messaging.converter.MappingJackson2MessageConverter;
import org.springframework.messaging.converter.MessageConverter;
import org.springframework.util.MimeTypeUtils;
/**
* Test the remote entity events.
*/
@TestPropertySource(properties = { "spring.cloud.bus.enabled=true" })
@SuppressWarnings("java:S6813") // constructor injects are not possible for test classes
public abstract class AbstractRemoteEventTest extends AbstractJpaIntegrationTest {
@Import(AbstractRemoteEventTest.EventProtoStuffTestConfig.class)
public abstract class AbstractRemoteEventTest extends AbstractJpaIntegrationTest {
@Autowired
private BusProtoStuffMessageConverter busProtoStuffMessageConverter;
private EventProtoStuffMessageConverter eventProtoStuffMessageConverter;
private AbstractMessageConverter jacksonMessageConverter;
@BeforeEach
public void setup() throws Exception {
final BusJacksonAutoConfiguration autoConfiguration = new BusJacksonAutoConfiguration();
this.jacksonMessageConverter = autoConfiguration.busJsonConverter(null);
ReflectionTestUtils.setField(
jacksonMessageConverter, "packagesToScan",
new String[] { "org.eclipse.hawkbit.repository.event.remote", ClassUtils.getPackageName(RemoteApplicationEvent.class) });
((InitializingBean) jacksonMessageConverter).afterPropertiesSet();
public void setup() {
this.jacksonMessageConverter = new MappingJackson2MessageConverter();
}
@SuppressWarnings("unchecked")
@@ -65,24 +58,33 @@ import org.springframework.util.MimeTypeUtils;
@SuppressWarnings("unchecked")
protected <T extends TenantAwareEvent> T createProtoStuffEvent(final T event) {
final Message<?> message = createProtoStuffMessage(event);
return (T) busProtoStuffMessageConverter.fromMessage(message, event.getClass());
return (T) eventProtoStuffMessageConverter.fromMessage(message, event.getClass());
}
private Message<?> createProtoStuffMessage(final TenantAwareEvent event) {
final Map<String, Object> headers = new LinkedHashMap<>();
headers.put(MessageHeaders.CONTENT_TYPE, BusProtoStuffMessageConverter.APPLICATION_BINARY_PROTOSTUFF);
return busProtoStuffMessageConverter.toMessage(event, new MutableMessageHeaders(headers));
return eventProtoStuffMessageConverter.toMessage(
event, new MutableMessageHeaders(Map.of(MessageHeaders.CONTENT_TYPE,
EventProtoStuffMessageConverter.APPLICATION_BINARY_PROTOSTUFF))
);
}
private Message<String> createJsonMessage(final Object event) {
final Map<String, MimeType> headers = new LinkedHashMap<>();
headers.put(MessageHeaders.CONTENT_TYPE, MimeTypeUtils.APPLICATION_JSON);
try {
final String json = new ObjectMapper().writeValueAsString(event);
return MessageBuilder.withPayload(json).copyHeaders(headers).build();
} catch (final JsonProcessingException e) {
String json = new ObjectMapper().writeValueAsString(event);
return MessageBuilder.withPayload(json)
.setHeader(MessageHeaders.CONTENT_TYPE, MimeTypeUtils.APPLICATION_JSON)
.build();
} catch (JsonProcessingException e) {
fail(e.getMessage());
}
return null;
}
@TestConfiguration
static class EventProtoStuffTestConfig {
@Bean
public MessageConverter eventProtoBufConverter() {
return new EventProtoStuffMessageConverter();
}
}
}

View File

@@ -0,0 +1,150 @@
/**
* Copyright (c) 2025 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.repository.event.remote;
import org.eclipse.hawkbit.repository.event.EventPublisherHolder;
import org.eclipse.hawkbit.repository.event.remote.entity.TargetCreatedEvent;
import org.eclipse.hawkbit.repository.event.remote.service.CancelTargetAssignmentServiceEvent;
import org.eclipse.hawkbit.repository.event.remote.service.MultiActionAssignServiceEvent;
import org.eclipse.hawkbit.repository.event.remote.service.TargetAssignDistributionSetServiceEvent;
import org.eclipse.hawkbit.repository.event.remote.service.TargetAttributesRequestedServiceEvent;
import org.eclipse.hawkbit.repository.event.remote.service.TargetCreatedServiceEvent;
import org.eclipse.hawkbit.repository.event.remote.service.TargetDeletedServiceEvent;
import org.eclipse.hawkbit.repository.model.Action;
import org.eclipse.hawkbit.repository.model.DistributionSet;
import org.eclipse.hawkbit.repository.model.Target;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.cloud.stream.function.StreamBridge;
import org.springframework.context.ApplicationEventPublisher;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.*;
import java.util.List;
import java.util.Set;
class ServiceEventsTest {
private StreamBridge streamBridge;
private ApplicationEventPublisher delegate;
private ApplicationEventPublisher publisher;
@BeforeEach
void setUp() throws IllegalAccessException, NoSuchFieldException {
streamBridge = mock(StreamBridge.class);
delegate = mock(ApplicationEventPublisher.class);
EventPublisherHolder.getInstance().setApplicationEventPublisher(delegate);
EventPublisherHolder.getInstance().setStreamBridge(streamBridge);
publisher = EventPublisherHolder.getInstance().getEventPublisher();
// Set up fields via reflection
var remoteEventsEnabledField = EventPublisherHolder.class.getDeclaredField("remoteEventsEnabled");
remoteEventsEnabledField.setAccessible(true);
remoteEventsEnabledField.set(EventPublisherHolder.getInstance(), true);
var remoteServiceEventsEnabledField = EventPublisherHolder.class.getDeclaredField("remoteServiceEventsEnabled");
remoteServiceEventsEnabledField.setAccessible(true);
remoteServiceEventsEnabledField.set(EventPublisherHolder.getInstance(), true);
var fanoutChannelField = EventPublisherHolder.class.getDeclaredField("fanoutEventChannel");
fanoutChannelField.setAccessible(true);
fanoutChannelField.set(EventPublisherHolder.getInstance(), "fanout");
var groupChannelField = EventPublisherHolder.class.getDeclaredField("serviceEventChannel");
groupChannelField.setAccessible(true);
groupChannelField.set(EventPublisherHolder.getInstance(), "group");
}
@Test
void testExpectedServiceEvents(){
var expected = Set.of(
TargetAssignDistributionSetEvent.class,
MultiActionAssignEvent.class,
MultiActionCancelEvent.class,
CancelTargetAssignmentEvent.class,
TargetDeletedEvent.class,
TargetCreatedEvent.class,
TargetAttributesRequestedEvent.class
);
assertEquals(EventPublisherHolder.SERVICE_EVENTS, expected);
}
@Test
void testProcessingTargetAssignDistributionSetEventIsSent() {
TargetAssignDistributionSetEvent event = new TargetAssignDistributionSetEvent(mockAction());
publisher.publishEvent(event);
verify(streamBridge).send("fanout", event);
verify(streamBridge).send(eq("group"), any(TargetAssignDistributionSetServiceEvent.class));
}
@Test
void testProcessingTargetCreatedEventIsSent() {
TargetCreatedEvent event = new TargetCreatedEvent(mock(Target.class));
publisher.publishEvent(event);
verify(streamBridge).send("fanout", event);
verify(streamBridge).send(eq("group"), any(TargetCreatedServiceEvent.class));
}
@Test
void testProcessingTargetDeletedEventIsSent() {
TargetDeletedEvent event = new TargetDeletedEvent("testtenant", 1l, Target.class, "testControllerId", "address");
publisher.publishEvent(event);
verify(streamBridge).send("fanout", event);
verify(streamBridge).send(eq("group"), any(TargetDeletedServiceEvent.class));
}
@Test
void testProcessingTargetAttributesRequestedEventIsSent() {
TargetAttributesRequestedEvent event = new TargetAttributesRequestedEvent("testtenant", 1l, Target.class, "testControllerId","address");
publisher.publishEvent(event);
verify(streamBridge).send("fanout", event);
verify(streamBridge).send(eq("group"), any(TargetAttributesRequestedServiceEvent.class));
}
@Test
void testProcessingMultiActionAssignmentEventIsSent() {
MultiActionAssignEvent event = new MultiActionAssignEvent("testtenant", List.of(mockAction()));
publisher.publishEvent(event);
verify(streamBridge).send("fanout", event);
verify(streamBridge).send(eq("group"), any(MultiActionAssignServiceEvent.class));
}
@Test
void testCancelTargetAssignmentEventIsSent() {
CancelTargetAssignmentEvent event = new CancelTargetAssignmentEvent(mockAction());
publisher.publishEvent(event);
verify(streamBridge).send("fanout", event);
verify(streamBridge).send(eq("group"), any(CancelTargetAssignmentServiceEvent.class));
}
private Action mockAction() {
final Action actionMock = mock(Action.class);
final Target targetMock = mock(Target.class);
final DistributionSet distributionSetMock = mock(DistributionSet.class);
when(distributionSetMock.getId()).thenReturn(1L);
when(actionMock.getDistributionSet()).thenReturn(distributionSetMock);
when(actionMock.getId()).thenReturn(1l);
when(actionMock.getTenant()).thenReturn("DEFAULT");
when(actionMock.getTarget()).thenReturn(targetMock);
when(actionMock.getActionType()).thenReturn(Action.ActionType.SOFT);
when(targetMock.getControllerId()).thenReturn("target1");
return actionMock;
}
}

View File

@@ -9,7 +9,6 @@
#
### Debug & Monitor Eclipselink - START
logging.level.org.eclipse.persistence=ERROR
## Uncomment to see the debug of persistence, e.g. to see the generated SQLs
#logging.level.org.eclipse.persistence=DEBUG
@@ -56,5 +55,5 @@ hawkbit.repository.cluster.lock.refreshOnRemainMS=200
hawkbit.repository.cluster.lock.refreshOnRemainPercent=10
# reduce scheduler tic period to speed up tests
hawkbit.repository.cluster.lock.ticPeriodMS=10
# disable spring cloud bus for tests
spring.cloud.bus.enabled=false
org.eclipse.hawkbit.events.remote-enabled=false