DMF health check (#577)
* DMF support PING message for health checks. Signed-off-by: kaizimmerm <kai.zimmermann@bosch-si.com> * Device simulator supports PING. Signed-off-by: kaizimmerm <kai.zimmermann@bosch-si.com> * Code cleanup. Signed-off-by: kaizimmerm <kai.zimmermann@bosch-si.com> * Revert accidental checkin. Signed-off-by: kaizimmerm <kai.zimmermann@bosch-si.com> * Fix tests. Signed-off-by: kaizimmerm <kai.zimmermann@bosch-si.com> * Simplify API. Signed-off-by: kaizimmerm <kai.zimmermann@bosch-si.com> * Remove simulator dead letter. Signed-off-by: kaizimmerm <kai.zimmermann@bosch-si.com> * Remove dead code. Signed-off-by: kaizimmerm <kai.zimmermann@bosch-si.com> * Reduce code. Signed-off-by: kaizimmerm <kai.zimmermann@bosch-si.com> * Add message for one more error case. Signed-off-by: kaizimmerm <kai.zimmermann@bosch-si.com>
This commit is contained in:
@@ -14,6 +14,9 @@ import java.util.concurrent.ScheduledExecutorService;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.scheduling.TaskScheduler;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
import org.springframework.scheduling.concurrent.ConcurrentTaskScheduler;
|
||||
|
||||
import com.google.common.eventbus.AsyncEventBus;
|
||||
import com.google.common.eventbus.EventBus;
|
||||
@@ -25,6 +28,7 @@ import com.vaadin.spring.annotation.EnableVaadin;
|
||||
*/
|
||||
@SpringBootApplication
|
||||
@EnableVaadin
|
||||
@EnableScheduling
|
||||
public class DeviceSimulator {
|
||||
|
||||
public DeviceSimulator() {
|
||||
@@ -35,7 +39,7 @@ public class DeviceSimulator {
|
||||
* @return an asynchronous event bus to publish and retrieve events.
|
||||
*/
|
||||
@Bean
|
||||
public EventBus eventBus() {
|
||||
EventBus eventBus() {
|
||||
return new AsyncEventBus(Executors.newFixedThreadPool(4));
|
||||
}
|
||||
|
||||
@@ -43,10 +47,15 @@ public class DeviceSimulator {
|
||||
* @return central ScheduledExecutorService
|
||||
*/
|
||||
@Bean
|
||||
public ScheduledExecutorService threadPool() {
|
||||
ScheduledExecutorService threadPool() {
|
||||
return Executors.newScheduledThreadPool(8);
|
||||
}
|
||||
|
||||
@Bean
|
||||
TaskScheduler taskScheduler() {
|
||||
return new ConcurrentTaskScheduler(threadPool());
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the Spring Boot Application.
|
||||
*
|
||||
|
||||
@@ -9,10 +9,11 @@
|
||||
package org.eclipse.hawkbit.simulator;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
@@ -27,8 +28,7 @@ public class DeviceSimulatorRepository {
|
||||
|
||||
private final Map<DeviceKey, AbstractSimulatedDevice> devices = new ConcurrentHashMap<>();
|
||||
|
||||
@Autowired
|
||||
private SimulatedDeviceFactory deviceFactory;
|
||||
private final Set<String> tenants = new HashSet<>();
|
||||
|
||||
/**
|
||||
* Adds a simulated device to the repository.
|
||||
@@ -39,6 +39,7 @@ public class DeviceSimulatorRepository {
|
||||
*/
|
||||
public AbstractSimulatedDevice add(final AbstractSimulatedDevice simulatedDevice) {
|
||||
devices.put(new DeviceKey(simulatedDevice.getTenant().toLowerCase(), simulatedDevice.getId()), simulatedDevice);
|
||||
tenants.add(simulatedDevice.getTenant().toLowerCase());
|
||||
return simulatedDevice;
|
||||
}
|
||||
|
||||
@@ -78,12 +79,17 @@ public class DeviceSimulatorRepository {
|
||||
return devices.remove(new DeviceKey(tenant.toLowerCase(), id));
|
||||
}
|
||||
|
||||
public Set<String> getTenants() {
|
||||
return tenants;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all stored devices.
|
||||
*/
|
||||
public void clear() {
|
||||
devices.values().forEach(device -> device.clean());
|
||||
devices.values().forEach(AbstractSimulatedDevice::clean);
|
||||
devices.clear();
|
||||
tenants.clear();
|
||||
}
|
||||
|
||||
private static final class DeviceKey {
|
||||
|
||||
@@ -122,8 +122,7 @@ public class AmqpConfiguration {
|
||||
*/
|
||||
@Bean
|
||||
public Queue receiverConnectorQueueFromHawkBit() {
|
||||
final Map<String, Object> arguments = getDeadLetterExchangeArgs();
|
||||
arguments.putAll(getTTLMaxArgs());
|
||||
final Map<String, Object> arguments = getTTLMaxArgs();
|
||||
|
||||
return QueueBuilder.nonDurable(amqpProperties.getReceiverConnectorQueueFromSp()).autoDelete()
|
||||
.withArguments(arguments).build();
|
||||
@@ -151,36 +150,6 @@ public class AmqpConfiguration {
|
||||
return BindingBuilder.bind(receiverConnectorQueueFromHawkBit()).to(exchangeQueueToConnector());
|
||||
}
|
||||
|
||||
/**
|
||||
* Create dead letter queue.
|
||||
*
|
||||
* @return the queue
|
||||
*/
|
||||
@Bean
|
||||
public Queue deadLetterQueue() {
|
||||
return QueueBuilder.nonDurable(amqpProperties.getDeadLetterQueue()).withArguments(getTTLMaxArgs()).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the dead letter fanout exchange.
|
||||
*
|
||||
* @return the fanout exchange
|
||||
*/
|
||||
@Bean
|
||||
public FanoutExchange exchangeDeadLetter() {
|
||||
return new FanoutExchange(amqpProperties.getDeadLetterExchange(), false, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the Binding deadLetterQueue to exchangeDeadLetter.
|
||||
*
|
||||
* @return the binding
|
||||
*/
|
||||
@Bean
|
||||
public Binding bindDeadLetterQueueToLwm2mExchange() {
|
||||
return BindingBuilder.bind(deadLetterQueue()).to(exchangeDeadLetter());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Listener factory.
|
||||
*
|
||||
@@ -198,12 +167,6 @@ public class AmqpConfiguration {
|
||||
return containerFactory;
|
||||
}
|
||||
|
||||
private Map<String, Object> getDeadLetterExchangeArgs() {
|
||||
final Map<String, Object> args = Maps.newHashMapWithExpectedSize(1);
|
||||
args.put("x-dead-letter-exchange", amqpProperties.getDeadLetterExchange());
|
||||
return args;
|
||||
}
|
||||
|
||||
private static Map<String, Object> getTTLMaxArgs() {
|
||||
final Map<String, Object> args = Maps.newHashMapWithExpectedSize(2);
|
||||
args.put("x-message-ttl", Duration.ofDays(1).toMillis());
|
||||
|
||||
@@ -30,6 +30,11 @@ public class AmqpProperties {
|
||||
*/
|
||||
private boolean enabled;
|
||||
|
||||
/**
|
||||
* Set to true for the simulator run DMF health check.
|
||||
*/
|
||||
private boolean checkDmfHealth = false;
|
||||
|
||||
/**
|
||||
* Queue for receiving DMF messages from update server.
|
||||
*/
|
||||
@@ -40,22 +45,20 @@ public class AmqpProperties {
|
||||
*/
|
||||
private String senderForSpExchange = "simulator.replyTo";
|
||||
|
||||
/**
|
||||
* Simulator dead letter queue.
|
||||
*/
|
||||
private String deadLetterQueue = "simulator_deadletter";
|
||||
|
||||
/**
|
||||
* Simulator dead letter exchange.
|
||||
*/
|
||||
private String deadLetterExchange = "simulator.deadletter";
|
||||
|
||||
/**
|
||||
* Message time to live (ttl) for the deadletter queue. Default ttl is 1
|
||||
* hour.
|
||||
*/
|
||||
private int deadLetterTtl = 60_000;
|
||||
|
||||
public boolean isCheckDmfHealth() {
|
||||
return checkDmfHealth;
|
||||
}
|
||||
|
||||
public void setCheckDmfHealth(final boolean checkDmfHealth) {
|
||||
this.checkDmfHealth = checkDmfHealth;
|
||||
}
|
||||
|
||||
public String getReceiverConnectorQueueFromSp() {
|
||||
return receiverConnectorQueueFromSp;
|
||||
}
|
||||
@@ -64,22 +67,6 @@ public class AmqpProperties {
|
||||
this.receiverConnectorQueueFromSp = receiverConnectorQueueFromSp;
|
||||
}
|
||||
|
||||
public String getDeadLetterExchange() {
|
||||
return deadLetterExchange;
|
||||
}
|
||||
|
||||
public void setDeadLetterExchange(final String deadLetterExchange) {
|
||||
this.deadLetterExchange = deadLetterExchange;
|
||||
}
|
||||
|
||||
public String getDeadLetterQueue() {
|
||||
return deadLetterQueue;
|
||||
}
|
||||
|
||||
public void setDeadLetterQueue(final String deadLetterQueue) {
|
||||
this.deadLetterQueue = deadLetterQueue;
|
||||
}
|
||||
|
||||
public String getSenderForSpExchange() {
|
||||
return senderForSpExchange;
|
||||
}
|
||||
|
||||
@@ -58,8 +58,12 @@ public abstract class SenderService extends MessageService {
|
||||
return;
|
||||
}
|
||||
message.getMessageProperties().getHeaders().remove(AbstractJavaTypeMapper.DEFAULT_CLASSID_FIELD_NAME);
|
||||
|
||||
final String correlationId = UUID.randomUUID().toString();
|
||||
message.getMessageProperties().setCorrelationId(correlationId.getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
if (isCorrelationIdEmpty(message)) {
|
||||
message.getMessageProperties().setCorrelationId(correlationId.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
if (LOGGER.isTraceEnabled()) {
|
||||
LOGGER.trace("Sending message {} to exchange {} with correlationId {}", message, address, correlationId);
|
||||
@@ -70,6 +74,11 @@ public abstract class SenderService extends MessageService {
|
||||
rabbitTemplate.send(address, null, message, new CorrelationData(correlationId));
|
||||
}
|
||||
|
||||
private static boolean isCorrelationIdEmpty(final Message message) {
|
||||
return message.getMessageProperties().getCorrelationId() == null
|
||||
|| message.getMessageProperties().getCorrelationId().length <= 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert object and message properties to message.
|
||||
*
|
||||
|
||||
@@ -8,8 +8,11 @@
|
||||
*/
|
||||
package org.eclipse.hawkbit.simulator.amqp;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.eclipse.hawkbit.dmf.amqp.api.EventTopic;
|
||||
import org.eclipse.hawkbit.dmf.amqp.api.MessageHeaderKey;
|
||||
@@ -17,6 +20,7 @@ import org.eclipse.hawkbit.dmf.amqp.api.MessageType;
|
||||
import org.eclipse.hawkbit.dmf.json.model.DmfDownloadAndUpdateRequest;
|
||||
import org.eclipse.hawkbit.simulator.DeviceSimulatorRepository;
|
||||
import org.eclipse.hawkbit.simulator.DeviceSimulatorUpdater;
|
||||
import org.eclipse.jetty.util.ConcurrentHashSet;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.amqp.core.Message;
|
||||
@@ -26,6 +30,7 @@ import org.springframework.amqp.rabbit.core.RabbitTemplate;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.messaging.handler.annotation.Header;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
@@ -43,6 +48,8 @@ public class SpReceiverService extends ReceiverService {
|
||||
|
||||
private final DeviceSimulatorRepository repository;
|
||||
|
||||
private final Set<String> openPings = new ConcurrentHashSet<>();
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
@@ -82,28 +89,60 @@ public class SpReceiverService extends ReceiverService {
|
||||
*/
|
||||
@RabbitListener(queues = "${hawkbit.device.simulator.amqp.receiverConnectorQueueFromSp}", containerFactory = "listenerContainerFactory")
|
||||
public void recieveMessageSp(final Message message, @Header(MessageHeaderKey.TYPE) final String type,
|
||||
@Header(MessageHeaderKey.THING_ID) final String thingId,
|
||||
@Header(name = MessageHeaderKey.THING_ID, required = false) final String thingId,
|
||||
@Header(MessageHeaderKey.TENANT) final String tenant) {
|
||||
checkContentTypeJson(message);
|
||||
delegateMessage(message, type, thingId, tenant);
|
||||
}
|
||||
|
||||
private void delegateMessage(final Message message, final String type, final String thingId, final String tenant) {
|
||||
final MessageType messageType = MessageType.valueOf(type);
|
||||
|
||||
if (MessageType.EVENT.equals(messageType)) {
|
||||
checkContentTypeJson(message);
|
||||
handleEventMessage(message, thingId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (MessageType.THING_DELETED.equals(messageType)) {
|
||||
checkContentTypeJson(message);
|
||||
repository.remove(tenant, thingId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (MessageType.PING_RESPONSE.equals(messageType)) {
|
||||
final String correlationId = new String(message.getMessageProperties().getCorrelationId(),
|
||||
StandardCharsets.UTF_8);
|
||||
if (!openPings.remove(correlationId)) {
|
||||
LOGGER.error("Unknown PING_RESPONSE received for correlationId: {}.", correlationId);
|
||||
}
|
||||
|
||||
if (LOGGER.isDebugEnabled()) {
|
||||
LOGGER.debug("Got ping response from tenant {} with correlationId {} with timestamp {}", tenant,
|
||||
correlationId, new String(message.getBody(), StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
LOGGER.info("No valid message type property.");
|
||||
}
|
||||
|
||||
@Scheduled(fixedDelay = 5_000, initialDelay = 5_000)
|
||||
void checkDmfHealth() {
|
||||
if (!amqpProperties.isCheckDmfHealth()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (openPings.size() > 5) {
|
||||
LOGGER.error("Currently {} open pings! DMF does not seem to be reachable.", openPings.size());
|
||||
} else {
|
||||
LOGGER.debug("Currently {} open pings", openPings.size());
|
||||
}
|
||||
|
||||
repository.getTenants().forEach(tenant -> {
|
||||
final String correlationId = UUID.randomUUID().toString();
|
||||
spSenderService.ping(tenant, correlationId);
|
||||
openPings.add(correlationId);
|
||||
LOGGER.debug("Ping tenant {} with correlationId {}", tenant, correlationId);
|
||||
});
|
||||
}
|
||||
|
||||
private void handleEventMessage(final Message message, final String thingId) {
|
||||
final Object eventHeader = message.getMessageProperties().getHeaders().get(MessageHeaderKey.TOPIC);
|
||||
if (eventHeader == null) {
|
||||
|
||||
@@ -57,6 +57,17 @@ public class SpSenderService extends SenderService {
|
||||
this.simulationProperties = simulationProperties;
|
||||
}
|
||||
|
||||
public void ping(final String tenant, final String correlationId) {
|
||||
final MessageProperties messageProperties = new MessageProperties();
|
||||
messageProperties.getHeaders().put(MessageHeaderKey.TENANT, tenant);
|
||||
messageProperties.getHeaders().put(MessageHeaderKey.TYPE, MessageType.PING.toString());
|
||||
messageProperties.setCorrelationId(correlationId.getBytes());
|
||||
messageProperties.setReplyTo(amqpProperties.getSenderForSpExchange());
|
||||
messageProperties.setContentType(MessageProperties.CONTENT_TYPE_TEXT_PLAIN);
|
||||
|
||||
sendMessage(spExchange, new Message(null, messageProperties));
|
||||
}
|
||||
|
||||
/**
|
||||
* Finish the update process. This will send a action status to SP.
|
||||
*
|
||||
@@ -96,7 +107,8 @@ public class SpSenderService extends SenderService {
|
||||
* the ID of the action for the error message
|
||||
*/
|
||||
public void sendErrorMessage(final String tenant, final List<String> updateResultMessages, final Long actionId) {
|
||||
final Message message = createActionStatusMessage(tenant, DmfActionStatus.ERROR, updateResultMessages, actionId);
|
||||
final Message message = createActionStatusMessage(tenant, DmfActionStatus.ERROR, updateResultMessages,
|
||||
actionId);
|
||||
sendMessage(spExchange, message);
|
||||
}
|
||||
|
||||
@@ -240,7 +252,8 @@ public class SpSenderService extends SenderService {
|
||||
final List<String> updateResultMessages) {
|
||||
final MessageProperties messageProperties = new MessageProperties();
|
||||
final Map<String, Object> headers = messageProperties.getHeaders();
|
||||
final DmfActionUpdateStatus actionUpdateStatus = new DmfActionUpdateStatus(cacheValue.getActionId(), actionStatus);
|
||||
final DmfActionUpdateStatus actionUpdateStatus = new DmfActionUpdateStatus(cacheValue.getActionId(),
|
||||
actionStatus);
|
||||
headers.put(MessageHeaderKey.TYPE, MessageType.EVENT.name());
|
||||
headers.put(MessageHeaderKey.TENANT, cacheValue.getTenant());
|
||||
headers.put(MessageHeaderKey.TOPIC, EventTopic.UPDATE_ACTION_STATUS.name());
|
||||
|
||||
Reference in New Issue
Block a user