Skip to content

Commit

Permalink
Issue #106: Reworked handling of matching validation result failure.
Browse files Browse the repository at this point in the history
* Only send 'AddConnectionLogEntry' to Connectivity shard if the live response came from Connectivity. Otherwise, log warning that connection ID of sender is unknown.
* Set detail message of response validation failure as description for timeout exception in case no valid live response arrived within the command's specified or default timeout.
* Extracted factory for creating a 'LogEntry' for failed command-response-round-trips to make it re-usable.

Signed-off-by: Juergen Fickel <juergen.fickel@bosch.io>
  • Loading branch information
Juergen Fickel committed Nov 12, 2021
1 parent 9625bb7 commit 878f960
Show file tree
Hide file tree
Showing 6 changed files with 515 additions and 72 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ public final class GatewayCommandTimeoutException extends DittoRuntimeException

private static final String MESSAGE_TEMPLATE = "The Command reached the specified timeout of {0}ms.";

private static final String DEFAULT_DESCRIPTION = "Try increasing the command timeout";
private static final String DEFAULT_DESCRIPTION = "Try increasing the command timeout.";

private static final long serialVersionUID = -3732435554989623073L;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
* Copyright (c) 2021 Contributors to the Eclipse Foundation
*
* See the NOTICE file(s) distributed with this work for additional
* information regarding copyright ownership.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.ditto.connectivity.api.messaging.monitoring.logs;

import static org.eclipse.ditto.internal.models.signal.SignalInformationPoint.getCorrelationId;
import static org.eclipse.ditto.internal.models.signal.SignalInformationPoint.getEntityId;

import java.time.Instant;
import java.util.function.Predicate;

import javax.annotation.concurrent.Immutable;

import org.eclipse.ditto.base.model.common.ConditionChecker;
import org.eclipse.ditto.base.model.signals.commands.Command;
import org.eclipse.ditto.base.model.signals.commands.CommandResponse;
import org.eclipse.ditto.connectivity.model.ConnectivityModelFactory;
import org.eclipse.ditto.connectivity.model.LogCategory;
import org.eclipse.ditto.connectivity.model.LogEntry;
import org.eclipse.ditto.connectivity.model.LogLevel;
import org.eclipse.ditto.connectivity.model.LogType;

/**
* Factory for creating instances of {@link LogEntry}.
*
* @since 2.2.0
*/
@Immutable
public final class LogEntryFactory {

private LogEntryFactory() {
throw new AssertionError();
}

/**
* Returns a {@code LogEntry} for a failed round-trip of the specified {@code Command} and {@code CommandResponse}.
* The failure is described by the specified detail message string argument.
*
* @param command the command of the round-trip.
* @param commandResponse the response of the round-trip.
* @param detailMessage describes the reason for the failed round-trip.
* @throws NullPointerException if any argument is {@code null}.
* @throws IllegalArgumentException if {@code detailMessage} is blank.
*/
public static LogEntry getLogEntryForFailedCommandResponseRoundTrip(final Command<?> command,
final CommandResponse<?> commandResponse,
final String detailMessage) {

ConditionChecker.checkNotNull(command, "command");
ConditionChecker.checkNotNull(commandResponse, "commandResponse");
ConditionChecker.checkArgument(ConditionChecker.checkNotNull(detailMessage, "detailMessage"),
Predicate.not(String::isBlank),
() -> "The detailMessage must not be blank.");

final var logEntryBuilder = ConnectivityModelFactory.newLogEntryBuilder(
getCorrelationId(command).or(() -> getCorrelationId(commandResponse)).orElse("n/a"),
Instant.now(),
LogCategory.RESPONSE,
LogType.DROPPED,
LogLevel.FAILURE,
detailMessage
);

getEntityId(command).or(() -> getEntityId(commandResponse)).ifPresent(logEntryBuilder::entityId);

return logEntryBuilder.build();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
/*
* Copyright (c) 2021 Contributors to the Eclipse Foundation
*
* See the NOTICE file(s) distributed with this work for additional
* information regarding copyright ownership.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.ditto.connectivity.api.messaging.monitoring.logs;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.assertj.core.api.Assertions.assertThatNullPointerException;
import static org.mutabilitydetector.unittesting.MutabilityAssert.assertInstancesOf;
import static org.mutabilitydetector.unittesting.MutabilityMatchers.areImmutable;

import java.time.Instant;

import org.eclipse.ditto.base.model.correlationid.TestNameCorrelationId;
import org.eclipse.ditto.base.model.entity.id.EntityId;
import org.eclipse.ditto.base.model.entity.id.WithEntityId;
import org.eclipse.ditto.base.model.headers.DittoHeaders;
import org.eclipse.ditto.base.model.signals.commands.Command;
import org.eclipse.ditto.base.model.signals.commands.CommandResponse;
import org.eclipse.ditto.connectivity.model.LogCategory;
import org.eclipse.ditto.connectivity.model.LogLevel;
import org.eclipse.ditto.connectivity.model.LogType;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnitRunner;

/**
* Unit test for {@link LogEntryFactory}.
*/
@RunWith(MockitoJUnitRunner.class)
public final class LogEntryFactoryTest {

private static final String DETAIL_MESSAGE_FAILURE = "This is the failure detail message.";

@Rule
public final TestNameCorrelationId testNameCorrelationId = TestNameCorrelationId.newInstance();

@Mock
private Command<?> command;

@Mock
private CommandResponse<?> commandResponse;

private DittoHeaders dittoHeadersWithCorrelationId;

@Before
public void before() {
dittoHeadersWithCorrelationId =
DittoHeaders.newBuilder().correlationId(testNameCorrelationId.getCorrelationId()).build();

final var emptyDittoHeaders = DittoHeaders.empty();
Mockito.when(command.getDittoHeaders()).thenReturn(emptyDittoHeaders);
Mockito.when(commandResponse.getDittoHeaders()).thenReturn(emptyDittoHeaders);
}

@Test
public void assertImmutability() {
assertInstancesOf(LogEntryFactory.class, areImmutable());
}

@Test
public void getLogEntryForFailedCommandResponseRoundTripWithNullCommandThrowsException() {
assertThatNullPointerException()
.isThrownBy(() -> LogEntryFactory.getLogEntryForFailedCommandResponseRoundTrip(null,
commandResponse,
DETAIL_MESSAGE_FAILURE))
.withMessage("The command must not be null!")
.withNoCause();
}

@Test
public void getLogEntryForFailedCommandResponseRoundTripWithNullCommandResponseThrowsException() {
assertThatNullPointerException()
.isThrownBy(() -> LogEntryFactory.getLogEntryForFailedCommandResponseRoundTrip(command,
null,
DETAIL_MESSAGE_FAILURE))
.withMessage("The commandResponse must not be null!")
.withNoCause();
}

@Test
public void getLogEntryForFailedCommandResponseRoundTripWithNullDetailMessageThrowsException() {
assertThatNullPointerException()
.isThrownBy(() -> LogEntryFactory.getLogEntryForFailedCommandResponseRoundTrip(command,
commandResponse,
null))
.withMessage("The detailMessage must not be null!")
.withNoCause();
}

@Test
public void getLogEntryForFailedCommandResponseRoundTripWithBlankDetailMessageThrowsException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> LogEntryFactory.getLogEntryForFailedCommandResponseRoundTrip(command,
commandResponse,
" "))
.withMessage("The detailMessage must not be blank.")
.withNoCause();
}

@Test
public void getLogEntryForFailedCommandResponseRoundTripWithNoCorrelationIdReturnsExpected() {
final var emptyDittoHeaders = DittoHeaders.empty();
Mockito.when(command.getDittoHeaders()).thenReturn(emptyDittoHeaders);
Mockito.when(commandResponse.getDittoHeaders()).thenReturn(emptyDittoHeaders);

final var logEntry = LogEntryFactory.getLogEntryForFailedCommandResponseRoundTrip(command,
commandResponse,
DETAIL_MESSAGE_FAILURE);

assertThat(logEntry.getCorrelationId()).isEqualTo("n/a");
}

@Test
public void getLogEntryForFailedCommandResponseRoundTripWithCommandCorrelationIdOnlyReturnsExpected() {
Mockito.when(command.getDittoHeaders()).thenReturn(dittoHeadersWithCorrelationId);

final var logEntry = LogEntryFactory.getLogEntryForFailedCommandResponseRoundTrip(command,
commandResponse,
DETAIL_MESSAGE_FAILURE);

assertThat(logEntry.getCorrelationId()).isEqualTo(testNameCorrelationId.getCorrelationId().toString());
}

@Test
public void getLogEntryForFailedCommandResponseRoundTripWithPrefersCommandCorrelationIdOnlyReturnsExpected() {
Mockito.when(command.getDittoHeaders()).thenReturn(dittoHeadersWithCorrelationId);
Mockito.lenient()
.when(commandResponse.getDittoHeaders())
.thenReturn(DittoHeaders.newBuilder().correlationId("anotherCorrelationId").build());

final var logEntry = LogEntryFactory.getLogEntryForFailedCommandResponseRoundTrip(command,
commandResponse,
DETAIL_MESSAGE_FAILURE);

assertThat(logEntry.getCorrelationId()).isEqualTo(testNameCorrelationId.getCorrelationId().toString());
}

@Test
public void getLogEntryForFailedCommandResponseRoundTripReturnsLogEntryWithTimestampCloseToNow() {
final var logEntry = LogEntryFactory.getLogEntryForFailedCommandResponseRoundTrip(command,
commandResponse,
DETAIL_MESSAGE_FAILURE);
final var instantNow = Instant.now();

assertThat(logEntry.getTimestamp()).isBetween(instantNow.minusMillis(500L), instantNow);
}

@Test
public void getLogEntryForFailedCommandResponseRoundTripReturnsLogEntryWithLogCategoryResponse() {
final var logEntry = LogEntryFactory.getLogEntryForFailedCommandResponseRoundTrip(command,
commandResponse,
DETAIL_MESSAGE_FAILURE);

assertThat(logEntry.getLogCategory()).isEqualTo(LogCategory.RESPONSE);
}

@Test
public void getLogEntryForFailedCommandResponseRoundTripReturnsLogEntryWithLogTypeDropped() {
final var logEntry = LogEntryFactory.getLogEntryForFailedCommandResponseRoundTrip(command,
commandResponse,
DETAIL_MESSAGE_FAILURE);

assertThat(logEntry.getLogType()).isEqualTo(LogType.DROPPED);
}

@Test
public void getLogEntryForFailedCommandResponseRoundTripReturnsLogEntryWithLogLevelFailure() {
final var logEntry = LogEntryFactory.getLogEntryForFailedCommandResponseRoundTrip(command,
commandResponse,
DETAIL_MESSAGE_FAILURE);

assertThat(logEntry.getLogLevel()).isEqualTo(LogLevel.FAILURE);
}

@Test
public void getLogEntryForFailedCommandResponseRoundTripReturnsLogEntryWithExpectedDetailMessage() {
final var logEntry = LogEntryFactory.getLogEntryForFailedCommandResponseRoundTrip(command,
commandResponse,
DETAIL_MESSAGE_FAILURE);

assertThat(logEntry.getMessage()).isEqualTo(DETAIL_MESSAGE_FAILURE);
}

@Test
public void getLogEntryForFailedCommandResponseRoundTripReturnsLogEntryWithoutEntityId() {
final var logEntry = LogEntryFactory.getLogEntryForFailedCommandResponseRoundTrip(command,
commandResponse,
DETAIL_MESSAGE_FAILURE);

assertThat(logEntry.getEntityId()).isEmpty();
}

@Test
public void getLogEntryForFailedCommandResponseRoundTripReturnsLogEntryWithEntityIdOfCommand() {
final var entityId = Mockito.mock(EntityId.class);
final Command<?> commandWithEntityId =
Mockito.mock(Command.class, Mockito.withSettings().extraInterfaces(WithEntityId.class));
Mockito.when(commandWithEntityId.getDittoHeaders()).thenReturn(dittoHeadersWithCorrelationId);
Mockito.when(((WithEntityId) commandWithEntityId).getEntityId()).thenReturn(entityId);

final var logEntry = LogEntryFactory.getLogEntryForFailedCommandResponseRoundTrip(commandWithEntityId,
commandResponse,
DETAIL_MESSAGE_FAILURE);

assertThat(logEntry.getEntityId()).hasValue(entityId);
}

@Test
public void getLogEntryForFailedCommandResponseRoundTripReturnsLogEntryWithEntityIdOfCommandResponse() {
final var entityId = Mockito.mock(EntityId.class);
final CommandResponse<?> commandResponseWithEntityId =
Mockito.mock(CommandResponse.class, Mockito.withSettings().extraInterfaces(WithEntityId.class));
Mockito.when(commandResponseWithEntityId.getDittoHeaders()).thenReturn(dittoHeadersWithCorrelationId);
Mockito.when(((WithEntityId) commandResponseWithEntityId).getEntityId()).thenReturn(entityId);

final var logEntry = LogEntryFactory.getLogEntryForFailedCommandResponseRoundTrip(command,
commandResponseWithEntityId,
DETAIL_MESSAGE_FAILURE);

assertThat(logEntry.getEntityId()).hasValue(entityId);
}

}

0 comments on commit 878f960

Please sign in to comment.