diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/AiModel.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/AiModel.java index ffca023b23..4d4fcfa58e 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/AiModel.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/AiModel.java @@ -46,7 +46,6 @@ public final class AiModel extends BaseData implements HasTenantId, H private static final long serialVersionUID = 9017108678716011604L; @Schema( - requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "JSON object representing the ID of the tenant associated with this AI model", example = "e3c4b7d2-5678-4a9b-0c1d-2e3f4a5b6c7d" @@ -54,7 +53,6 @@ public final class AiModel extends BaseData implements HasTenantId, H private TenantId tenantId; @Schema( - requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "Version of the AI model record; increments automatically whenever the record is changed", example = "7", diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChain.java b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChain.java index 4d8bb35919..f81bf841e5 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChain.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChain.java @@ -41,7 +41,7 @@ public class RuleChain extends BaseDataWithAdditionalInfo implement private static final long serialVersionUID = -5656679015121935465L; - @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "JSON object with Tenant Id.", accessMode = Schema.AccessMode.READ_ONLY) + @Schema(description = "JSON object with Tenant Id.", accessMode = Schema.AccessMode.READ_ONLY) private TenantId tenantId; @NoXss @Length(fieldName = "name") diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java index e8ade498db..b5384eada7 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java @@ -508,7 +508,9 @@ public class DefaultTransportService extends TransportActivityManager implements @Override public void process(TransportProtos.SessionInfoProto sessionInfo, TransportProtos.SessionEventMsg msg, TransportServiceCallback callback) { if (checkLimits(sessionInfo, msg, callback)) { - recordActivityInternal(sessionInfo); + if (msg.getEvent() != TransportProtos.SessionEvent.CLOSED) { + recordActivityInternal(sessionInfo); + } sendToDeviceActor(sessionInfo, TransportToDeviceActorMsg.newBuilder().setSessionInfo(sessionInfo) .setSessionEvent(msg).build(), callback); } diff --git a/common/transport/transport-api/src/test/java/org/thingsboard/server/common/transport/service/TransportActivityManagerTest.java b/common/transport/transport-api/src/test/java/org/thingsboard/server/common/transport/service/TransportActivityManagerTest.java index 3b3ce6e600..a656b22241 100644 --- a/common/transport/transport-api/src/test/java/org/thingsboard/server/common/transport/service/TransportActivityManagerTest.java +++ b/common/transport/transport-api/src/test/java/org/thingsboard/server/common/transport/service/TransportActivityManagerTest.java @@ -26,12 +26,14 @@ import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.test.util.ReflectionTestUtils; +import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.transport.SessionMsgListener; import org.thingsboard.server.common.transport.TransportServiceCallback; import org.thingsboard.server.common.transport.activity.ActivityReportCallback; import org.thingsboard.server.common.transport.activity.ActivityState; import org.thingsboard.server.common.transport.activity.strategy.ActivityStrategy; import org.thingsboard.server.common.transport.activity.strategy.ActivityStrategyType; +import org.thingsboard.server.common.transport.limits.TransportRateLimitService; import org.thingsboard.server.gen.transport.TransportProtos; import java.util.UUID; @@ -41,7 +43,12 @@ import java.util.stream.Stream; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.ArgumentMatchers.nullable; import static org.mockito.Mockito.doCallRealMethod; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; @@ -514,4 +521,55 @@ public class TransportActivityManagerTest { verify(transportServiceMock, never()).process(sessionInfo, SESSION_EVENT_MSG_CLOSED, null); } + @Test + void givenSessionClosedEvent_whenProcessingSessionEvent_thenShouldNotRecordActivity() { + // GIVEN - simulates the session-expiry path: session already removed from sessions map, + // then process(SESSION_CLOSED) is called. Activity must NOT be recorded to avoid active + // status update from false to true + var rateLimitServiceMock = mock(TransportRateLimitService.class); + ReflectionTestUtils.setField(transportServiceMock, "rateLimitService", rateLimitServiceMock); + when(rateLimitServiceMock.checkLimits(any(), nullable(DeviceId.class), any(), anyInt(), anyBoolean())).thenReturn(null); + + TransportProtos.SessionInfoProto sessionInfo = TransportProtos.SessionInfoProto.newBuilder() + .setSessionIdMSB(SESSION_ID.getMostSignificantBits()) + .setSessionIdLSB(SESSION_ID.getLeastSignificantBits()) + .build(); + doCallRealMethod().when(transportServiceMock).process(sessionInfo, SESSION_EVENT_MSG_CLOSED, null); + + // WHEN + transportServiceMock.process(sessionInfo, SESSION_EVENT_MSG_CLOSED, null); + + // THEN + verify(transportServiceMock, never()).onActivity(any(), any(), anyLong()); + verify(transportServiceMock).sendToDeviceActor(eq(sessionInfo), any(), isNull()); + } + + @Test + void givenSessionOpenEvent_whenProcessingSessionEvent_thenShouldRecordActivity() { + // GIVEN + var rateLimitServiceMock = mock(TransportRateLimitService.class); + ReflectionTestUtils.setField(transportServiceMock, "rateLimitService", rateLimitServiceMock); + when(rateLimitServiceMock.checkLimits(any(), nullable(DeviceId.class), any(), anyInt(), anyBoolean())).thenReturn(null); + + TransportProtos.SessionInfoProto sessionInfo = TransportProtos.SessionInfoProto.newBuilder() + .setSessionIdMSB(SESSION_ID.getMostSignificantBits()) + .setSessionIdLSB(SESSION_ID.getLeastSignificantBits()) + .build(); + var sessionOpenMsg = TransportProtos.SessionEventMsg.newBuilder() + .setSessionType(TransportProtos.SessionType.ASYNC) + .setEvent(TransportProtos.SessionEvent.OPEN) + .build(); + when(transportServiceMock.toSessionId(sessionInfo)).thenReturn(SESSION_ID); + long expectedActivityTime = 100L; + when(transportServiceMock.getCurrentTimeMillis()).thenReturn(expectedActivityTime); + doCallRealMethod().when(transportServiceMock).process(sessionInfo, sessionOpenMsg, null); + + // WHEN + transportServiceMock.process(sessionInfo, sessionOpenMsg, null); + + // THEN + verify(transportServiceMock).onActivity(SESSION_ID, sessionInfo, expectedActivityTime); + verify(transportServiceMock).sendToDeviceActor(eq(sessionInfo), any(), isNull()); + } + }