Browse Source
Fixed notification requests and RPC cleanup timeout on large datasetspull/15195/merge
committed by
GitHub
14 changed files with 567 additions and 62 deletions
@ -0,0 +1,144 @@ |
|||
/** |
|||
* Copyright © 2016-2026 The Thingsboard 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 |
|||
* |
|||
* http://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.thingsboard.server.service.ttl; |
|||
|
|||
import org.junit.jupiter.api.BeforeEach; |
|||
import org.junit.jupiter.api.Test; |
|||
import org.junit.jupiter.api.extension.ExtendWith; |
|||
import org.mockito.Mock; |
|||
import org.mockito.junit.jupiter.MockitoExtension; |
|||
import org.springframework.test.util.ReflectionTestUtils; |
|||
import org.thingsboard.server.common.data.id.TenantId; |
|||
import org.thingsboard.server.common.data.page.PageData; |
|||
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; |
|||
import org.thingsboard.server.dao.notification.NotificationRequestDao; |
|||
import org.thingsboard.server.dao.sqlts.insert.sql.SqlPartitioningRepository; |
|||
import org.thingsboard.server.dao.tenant.TenantService; |
|||
import org.thingsboard.server.queue.discovery.PartitionService; |
|||
|
|||
import java.util.List; |
|||
import java.util.UUID; |
|||
|
|||
import static org.mockito.ArgumentMatchers.any; |
|||
import static org.mockito.ArgumentMatchers.anyInt; |
|||
import static org.mockito.ArgumentMatchers.anyLong; |
|||
import static org.mockito.ArgumentMatchers.anyString; |
|||
import static org.mockito.ArgumentMatchers.eq; |
|||
import static org.mockito.Mockito.never; |
|||
import static org.mockito.Mockito.times; |
|||
import static org.mockito.Mockito.verify; |
|||
import static org.mockito.Mockito.when; |
|||
|
|||
@ExtendWith(MockitoExtension.class) |
|||
public class NotificationsCleanUpServiceTest { |
|||
|
|||
@Mock |
|||
private PartitionService partitionService; |
|||
@Mock |
|||
private SqlPartitioningRepository partitioningRepository; |
|||
@Mock |
|||
private NotificationRequestDao notificationRequestDao; |
|||
@Mock |
|||
private TenantService tenantService; |
|||
|
|||
private NotificationsCleanUpService cleanUpService; |
|||
|
|||
private static final int BATCH_SIZE = 3; |
|||
|
|||
@BeforeEach |
|||
public void setUp() { |
|||
cleanUpService = new NotificationsCleanUpService(partitionService, partitioningRepository, notificationRequestDao, tenantService); |
|||
ReflectionTestUtils.setField(cleanUpService, "ttlInSec", 2592000L); |
|||
ReflectionTestUtils.setField(cleanUpService, "partitionSizeInHours", 168); |
|||
ReflectionTestUtils.setField(cleanUpService, "removalBatchSize", BATCH_SIZE); |
|||
} |
|||
|
|||
@Test |
|||
public void testBatchLoopCallsDaoMultipleTimes() { |
|||
TopicPartitionInfo myPartition = TopicPartitionInfo.builder().topic("tb_core").myPartition(true).build(); |
|||
when(partitionService.resolve(any(), any(), any())).thenReturn(myPartition); |
|||
when(partitioningRepository.dropPartitionsBefore(anyString(), anyLong(), anyLong())) |
|||
.thenReturn(System.currentTimeMillis()); |
|||
|
|||
TenantId tenantId = TenantId.fromUUID(UUID.randomUUID()); |
|||
when(tenantService.findTenantsIds(any())) |
|||
.thenReturn(new PageData<>(List.of(tenantId), 1, 1, false)); |
|||
|
|||
// Sysadmin: returns 3 (full batch), then 1 (partial) -> 2 calls
|
|||
when(notificationRequestDao.removeByTenantIdAndCreatedTimeBeforeBatch(eq(TenantId.SYS_TENANT_ID), anyLong(), eq(BATCH_SIZE))) |
|||
.thenReturn(BATCH_SIZE) |
|||
.thenReturn(1); |
|||
// Tenant: returns 3, 3, 0 -> 3 calls
|
|||
when(notificationRequestDao.removeByTenantIdAndCreatedTimeBeforeBatch(eq(tenantId), anyLong(), eq(BATCH_SIZE))) |
|||
.thenReturn(BATCH_SIZE) |
|||
.thenReturn(BATCH_SIZE) |
|||
.thenReturn(0); |
|||
|
|||
cleanUpService.cleanUp(); |
|||
|
|||
verify(notificationRequestDao, times(2)) |
|||
.removeByTenantIdAndCreatedTimeBeforeBatch(eq(TenantId.SYS_TENANT_ID), anyLong(), eq(BATCH_SIZE)); |
|||
verify(notificationRequestDao, times(3)) |
|||
.removeByTenantIdAndCreatedTimeBeforeBatch(eq(tenantId), anyLong(), eq(BATCH_SIZE)); |
|||
} |
|||
|
|||
@Test |
|||
public void testSkipsTenantNotOnMyPartition() { |
|||
TopicPartitionInfo myPartition = TopicPartitionInfo.builder().topic("tb_core").myPartition(true).build(); |
|||
TopicPartitionInfo notMyPartition = TopicPartitionInfo.builder().topic("tb_core").myPartition(false).build(); |
|||
when(partitionService.resolve(any(), eq(TenantId.SYS_TENANT_ID), eq(TenantId.SYS_TENANT_ID))) |
|||
.thenReturn(myPartition); |
|||
when(partitioningRepository.dropPartitionsBefore(anyString(), anyLong(), anyLong())) |
|||
.thenReturn(System.currentTimeMillis()); |
|||
|
|||
// Sysadmin: no records
|
|||
when(notificationRequestDao.removeByTenantIdAndCreatedTimeBeforeBatch(eq(TenantId.SYS_TENANT_ID), anyLong(), eq(BATCH_SIZE))) |
|||
.thenReturn(0); |
|||
|
|||
TenantId myTenant = TenantId.fromUUID(UUID.randomUUID()); |
|||
TenantId otherTenant = TenantId.fromUUID(UUID.randomUUID()); |
|||
when(tenantService.findTenantsIds(any())) |
|||
.thenReturn(new PageData<>(List.of(myTenant, otherTenant), 2, 1, false)); |
|||
when(partitionService.resolve(any(), eq(myTenant), eq(myTenant))).thenReturn(myPartition); |
|||
when(partitionService.resolve(any(), eq(otherTenant), eq(otherTenant))).thenReturn(notMyPartition); |
|||
|
|||
when(notificationRequestDao.removeByTenantIdAndCreatedTimeBeforeBatch(eq(myTenant), anyLong(), eq(BATCH_SIZE))) |
|||
.thenReturn(0); |
|||
|
|||
cleanUpService.cleanUp(); |
|||
|
|||
verify(notificationRequestDao).removeByTenantIdAndCreatedTimeBeforeBatch(eq(myTenant), anyLong(), eq(BATCH_SIZE)); |
|||
verify(notificationRequestDao, never()).removeByTenantIdAndCreatedTimeBeforeBatch(eq(otherTenant), anyLong(), anyInt()); |
|||
} |
|||
|
|||
@Test |
|||
public void testNoPartitionsDropped_stillCleansUpRequests() { |
|||
TopicPartitionInfo myPartition = TopicPartitionInfo.builder().topic("tb_core").myPartition(true).build(); |
|||
when(partitionService.resolve(any(), any(), any())).thenReturn(myPartition); |
|||
when(partitioningRepository.dropPartitionsBefore(anyString(), anyLong(), anyLong())) |
|||
.thenReturn(0L); |
|||
|
|||
when(notificationRequestDao.removeByTenantIdAndCreatedTimeBeforeBatch(eq(TenantId.SYS_TENANT_ID), anyLong(), eq(BATCH_SIZE))) |
|||
.thenReturn(0); |
|||
when(tenantService.findTenantsIds(any())) |
|||
.thenReturn(new PageData<>(List.of(), 0, 0, false)); |
|||
|
|||
cleanUpService.cleanUp(); |
|||
|
|||
verify(notificationRequestDao).removeByTenantIdAndCreatedTimeBeforeBatch(eq(TenantId.SYS_TENANT_ID), anyLong(), eq(BATCH_SIZE)); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,136 @@ |
|||
/** |
|||
* Copyright © 2016-2026 The Thingsboard 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 |
|||
* |
|||
* http://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.thingsboard.server.service.ttl.rpc; |
|||
|
|||
import org.junit.jupiter.api.BeforeEach; |
|||
import org.junit.jupiter.api.Test; |
|||
import org.junit.jupiter.api.extension.ExtendWith; |
|||
import org.mockito.Mock; |
|||
import org.mockito.junit.jupiter.MockitoExtension; |
|||
import org.springframework.test.util.ReflectionTestUtils; |
|||
import org.thingsboard.server.common.data.TenantProfile; |
|||
import org.thingsboard.server.common.data.id.TenantId; |
|||
import org.thingsboard.server.common.data.page.PageData; |
|||
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; |
|||
import org.thingsboard.server.common.data.tenant.profile.TenantProfileData; |
|||
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; |
|||
import org.thingsboard.server.dao.rpc.RpcDao; |
|||
import org.thingsboard.server.dao.tenant.TbTenantProfileCache; |
|||
import org.thingsboard.server.dao.tenant.TenantService; |
|||
import org.thingsboard.server.queue.discovery.PartitionService; |
|||
|
|||
import java.util.List; |
|||
import java.util.UUID; |
|||
|
|||
import static org.mockito.ArgumentMatchers.any; |
|||
import static org.mockito.ArgumentMatchers.anyInt; |
|||
import static org.mockito.ArgumentMatchers.anyLong; |
|||
import static org.mockito.ArgumentMatchers.eq; |
|||
import static org.mockito.Mockito.never; |
|||
import static org.mockito.Mockito.times; |
|||
import static org.mockito.Mockito.verify; |
|||
import static org.mockito.Mockito.when; |
|||
|
|||
@ExtendWith(MockitoExtension.class) |
|||
public class RpcCleanUpServiceTest { |
|||
|
|||
@Mock |
|||
private PartitionService partitionService; |
|||
@Mock |
|||
private RpcDao rpcDao; |
|||
@Mock |
|||
private TenantService tenantService; |
|||
@Mock |
|||
private TbTenantProfileCache tenantProfileCache; |
|||
|
|||
private RpcCleanUpService cleanUpService; |
|||
|
|||
private static final int BATCH_SIZE = 3; |
|||
|
|||
@BeforeEach |
|||
public void setUp() { |
|||
cleanUpService = new RpcCleanUpService(tenantService, partitionService, tenantProfileCache, rpcDao); |
|||
ReflectionTestUtils.setField(cleanUpService, "removalBatchSize", BATCH_SIZE); |
|||
} |
|||
|
|||
@Test |
|||
public void testBatchLoopCallsDaoMultipleTimes() { |
|||
TenantId tenantId = TenantId.fromUUID(UUID.randomUUID()); |
|||
setupTenant(tenantId, 7); |
|||
|
|||
// Returns 3 (full batch), 3 (full batch), 1 (partial) -> 3 calls
|
|||
when(rpcDao.deleteOutdatedRpcByTenantIdBatch(eq(tenantId), anyLong(), eq(BATCH_SIZE))) |
|||
.thenReturn(BATCH_SIZE) |
|||
.thenReturn(BATCH_SIZE) |
|||
.thenReturn(1); |
|||
|
|||
cleanUpService.cleanUp(); |
|||
|
|||
verify(rpcDao, times(3)).deleteOutdatedRpcByTenantIdBatch(eq(tenantId), anyLong(), eq(BATCH_SIZE)); |
|||
} |
|||
|
|||
@Test |
|||
public void testSkipsTenantNotOnMyPartition() { |
|||
TenantId myTenant = TenantId.fromUUID(UUID.randomUUID()); |
|||
TenantId otherTenant = TenantId.fromUUID(UUID.randomUUID()); |
|||
|
|||
TopicPartitionInfo myPartition = TopicPartitionInfo.builder().topic("tb_core").myPartition(true).build(); |
|||
TopicPartitionInfo notMyPartition = TopicPartitionInfo.builder().topic("tb_core").myPartition(false).build(); |
|||
|
|||
when(tenantService.findTenantsIds(any())) |
|||
.thenReturn(new PageData<>(List.of(myTenant, otherTenant), 2, 1, false)); |
|||
when(partitionService.resolve(any(), eq(myTenant), eq(myTenant))).thenReturn(myPartition); |
|||
when(partitionService.resolve(any(), eq(otherTenant), eq(otherTenant))).thenReturn(notMyPartition); |
|||
|
|||
setupTenantProfile(myTenant, 7); |
|||
when(rpcDao.deleteOutdatedRpcByTenantIdBatch(eq(myTenant), anyLong(), eq(BATCH_SIZE))) |
|||
.thenReturn(0); |
|||
|
|||
cleanUpService.cleanUp(); |
|||
|
|||
verify(rpcDao).deleteOutdatedRpcByTenantIdBatch(eq(myTenant), anyLong(), eq(BATCH_SIZE)); |
|||
verify(rpcDao, never()).deleteOutdatedRpcByTenantIdBatch(eq(otherTenant), anyLong(), anyInt()); |
|||
} |
|||
|
|||
@Test |
|||
public void testSkipsTenantWithZeroTtl() { |
|||
TenantId tenantId = TenantId.fromUUID(UUID.randomUUID()); |
|||
setupTenant(tenantId, 0); |
|||
|
|||
cleanUpService.cleanUp(); |
|||
|
|||
verify(rpcDao, never()).deleteOutdatedRpcByTenantIdBatch(any(), anyLong(), anyInt()); |
|||
} |
|||
|
|||
private void setupTenant(TenantId tenantId, int rpcTtlDays) { |
|||
TopicPartitionInfo myPartition = TopicPartitionInfo.builder().topic("tb_core").myPartition(true).build(); |
|||
when(partitionService.resolve(any(), eq(tenantId), eq(tenantId))).thenReturn(myPartition); |
|||
when(tenantService.findTenantsIds(any())) |
|||
.thenReturn(new PageData<>(List.of(tenantId), 1, 1, false)); |
|||
setupTenantProfile(tenantId, rpcTtlDays); |
|||
} |
|||
|
|||
private void setupTenantProfile(TenantId tenantId, int rpcTtlDays) { |
|||
TenantProfile profile = new TenantProfile(); |
|||
TenantProfileData profileData = new TenantProfileData(); |
|||
DefaultTenantProfileConfiguration config = new DefaultTenantProfileConfiguration(); |
|||
config.setRpcTtlDays(rpcTtlDays); |
|||
profileData.setConfiguration(config); |
|||
profile.setProfileData(profileData); |
|||
when(tenantProfileCache.get(tenantId)).thenReturn(profile); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,133 @@ |
|||
/** |
|||
* Copyright © 2016-2026 The Thingsboard 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 |
|||
* |
|||
* http://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.thingsboard.server.dao.sql.notification; |
|||
|
|||
import org.junit.After; |
|||
import org.junit.Test; |
|||
import org.springframework.beans.factory.annotation.Autowired; |
|||
import org.thingsboard.server.common.data.id.NotificationRequestId; |
|||
import org.thingsboard.server.common.data.id.TenantId; |
|||
import org.thingsboard.server.common.data.notification.NotificationRequest; |
|||
import org.thingsboard.server.common.data.notification.NotificationRequestStatus; |
|||
import org.thingsboard.server.dao.AbstractJpaDaoTest; |
|||
|
|||
import java.util.ArrayList; |
|||
import java.util.List; |
|||
import java.util.UUID; |
|||
import java.util.concurrent.TimeUnit; |
|||
|
|||
import static org.assertj.core.api.Assertions.assertThat; |
|||
|
|||
public class JpaNotificationRequestDaoTest extends AbstractJpaDaoTest { |
|||
|
|||
@Autowired |
|||
JpaNotificationRequestDao notificationRequestDao; |
|||
|
|||
private final List<NotificationRequest> createdRequests = new ArrayList<>(); |
|||
|
|||
@After |
|||
public void tearDown() { |
|||
for (NotificationRequest request : createdRequests) { |
|||
notificationRequestDao.removeById(request.getTenantId(), request.getId().getId()); |
|||
} |
|||
createdRequests.clear(); |
|||
} |
|||
|
|||
@Test |
|||
public void testBatchDeletion() { |
|||
TenantId sysTenantId = TenantId.SYS_TENANT_ID; |
|||
long now = System.currentTimeMillis(); |
|||
long oldTimestamp = now - TimeUnit.DAYS.toMillis(30); |
|||
|
|||
NotificationRequest oldRequest1 = createNotificationRequest(sysTenantId, oldTimestamp); |
|||
notificationRequestDao.save(sysTenantId, oldRequest1); |
|||
|
|||
NotificationRequest oldRequest2 = createNotificationRequest(sysTenantId, oldTimestamp); |
|||
notificationRequestDao.save(sysTenantId, oldRequest2); |
|||
|
|||
NotificationRequest freshRequest = createNotificationRequest(sysTenantId, now); |
|||
notificationRequestDao.save(sysTenantId, freshRequest); |
|||
|
|||
TenantId tenant2Id = TenantId.fromUUID(UUID.fromString("3d193a7a-774b-4c05-84d5-f7fdcf7a37cf")); |
|||
NotificationRequest tenant2Request = createNotificationRequest(tenant2Id, oldTimestamp); |
|||
notificationRequestDao.save(tenant2Id, tenant2Request); |
|||
|
|||
int batchSize = 10_000; |
|||
|
|||
assertThat(notificationRequestDao.removeByTenantIdAndCreatedTimeBeforeBatch(sysTenantId, oldTimestamp - 1, batchSize)).isEqualTo(0); |
|||
|
|||
long expirationTime = now - TimeUnit.DAYS.toMillis(15); |
|||
assertThat(notificationRequestDao.removeByTenantIdAndCreatedTimeBeforeBatch(sysTenantId, expirationTime, batchSize)).isEqualTo(2); |
|||
|
|||
assertThat(notificationRequestDao.findById(sysTenantId, freshRequest.getId().getId())).isNotNull(); |
|||
assertThat(notificationRequestDao.removeByTenantIdAndCreatedTimeBeforeBatch(tenant2Id, now + 1, batchSize)).isEqualTo(1); |
|||
} |
|||
|
|||
@Test |
|||
public void testBatchDeletionWithSmallBatchSize() { |
|||
TenantId tenantId = TenantId.SYS_TENANT_ID; |
|||
long oldTimestamp = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(30); |
|||
|
|||
for (int i = 0; i < 10; i++) { |
|||
NotificationRequest request = createNotificationRequest(tenantId, oldTimestamp); |
|||
notificationRequestDao.save(tenantId, request); |
|||
} |
|||
|
|||
int batchSize = 3; |
|||
long expirationTime = System.currentTimeMillis(); |
|||
|
|||
assertThat(notificationRequestDao.removeByTenantIdAndCreatedTimeBeforeBatch(tenantId, expirationTime, batchSize)).isEqualTo(3); |
|||
assertThat(notificationRequestDao.removeByTenantIdAndCreatedTimeBeforeBatch(tenantId, expirationTime, batchSize)).isEqualTo(3); |
|||
assertThat(notificationRequestDao.removeByTenantIdAndCreatedTimeBeforeBatch(tenantId, expirationTime, batchSize)).isEqualTo(3); |
|||
assertThat(notificationRequestDao.removeByTenantIdAndCreatedTimeBeforeBatch(tenantId, expirationTime, batchSize)).isEqualTo(1); |
|||
assertThat(notificationRequestDao.removeByTenantIdAndCreatedTimeBeforeBatch(tenantId, expirationTime, batchSize)).isEqualTo(0); |
|||
} |
|||
|
|||
@Test |
|||
public void testBatchDeletionIsolationBetweenTenants() { |
|||
TenantId tenant1 = TenantId.SYS_TENANT_ID; |
|||
TenantId tenant2 = TenantId.fromUUID(UUID.fromString("3d193a7a-774b-4c05-84d5-f7fdcf7a37cf")); |
|||
long oldTimestamp = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(30); |
|||
|
|||
for (int i = 0; i < 5; i++) { |
|||
NotificationRequest request = createNotificationRequest(tenant1, oldTimestamp); |
|||
notificationRequestDao.save(tenant1, request); |
|||
} |
|||
|
|||
for (int i = 0; i < 3; i++) { |
|||
NotificationRequest request = createNotificationRequest(tenant2, oldTimestamp); |
|||
notificationRequestDao.save(tenant2, request); |
|||
} |
|||
|
|||
int batchSize = 10_000; |
|||
long expirationTime = System.currentTimeMillis(); |
|||
|
|||
assertThat(notificationRequestDao.removeByTenantIdAndCreatedTimeBeforeBatch(tenant1, expirationTime, batchSize)).isEqualTo(5); |
|||
assertThat(notificationRequestDao.removeByTenantIdAndCreatedTimeBeforeBatch(tenant2, expirationTime, batchSize)).isEqualTo(3); |
|||
} |
|||
|
|||
private NotificationRequest createNotificationRequest(TenantId tenantId, long createdTime) { |
|||
NotificationRequest request = new NotificationRequest(); |
|||
request.setId(new NotificationRequestId(UUID.randomUUID())); |
|||
request.setTenantId(tenantId); |
|||
request.setCreatedTime(createdTime); |
|||
request.setTargets(List.of(UUID.randomUUID())); |
|||
request.setStatus(NotificationRequestStatus.SENT); |
|||
createdRequests.add(request); |
|||
return request; |
|||
} |
|||
|
|||
} |
|||
Loading…
Reference in new issue