committed by
GitHub
5 changed files with 218 additions and 18 deletions
@ -0,0 +1,126 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2024 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.subscription; |
||||
|
|
||||
|
import ch.qos.logback.classic.Logger; |
||||
|
import ch.qos.logback.classic.spi.ILoggingEvent; |
||||
|
import ch.qos.logback.core.read.ListAppender; |
||||
|
import com.google.common.util.concurrent.Futures; |
||||
|
import com.google.common.util.concurrent.ListenableFuture; |
||||
|
import com.google.common.util.concurrent.ListeningExecutorService; |
||||
|
import com.google.common.util.concurrent.MoreExecutors; |
||||
|
import org.apache.commons.lang3.RandomStringUtils; |
||||
|
import org.junit.jupiter.api.AfterEach; |
||||
|
import org.junit.jupiter.api.BeforeEach; |
||||
|
import org.junit.jupiter.api.Test; |
||||
|
import org.slf4j.LoggerFactory; |
||||
|
import org.springframework.test.util.ReflectionTestUtils; |
||||
|
import org.thingsboard.server.cache.limits.RateLimitService; |
||||
|
import org.thingsboard.server.common.data.id.DeviceId; |
||||
|
import org.thingsboard.server.common.data.id.EntityId; |
||||
|
import org.thingsboard.server.common.data.id.TenantId; |
||||
|
import org.thingsboard.server.common.data.limit.LimitedApi; |
||||
|
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; |
||||
|
import org.thingsboard.server.gen.transport.TransportProtos; |
||||
|
import org.thingsboard.server.queue.discovery.PartitionService; |
||||
|
import org.thingsboard.server.service.ws.WebSocketSessionRef; |
||||
|
|
||||
|
import java.util.ArrayList; |
||||
|
import java.util.HashMap; |
||||
|
import java.util.List; |
||||
|
import java.util.Map; |
||||
|
import java.util.UUID; |
||||
|
import java.util.concurrent.Executors; |
||||
|
|
||||
|
import static org.junit.jupiter.api.Assertions.assertFalse; |
||||
|
import static org.mockito.ArgumentMatchers.any; |
||||
|
import static org.mockito.ArgumentMatchers.eq; |
||||
|
import static org.mockito.ArgumentMatchers.nullable; |
||||
|
import static org.mockito.Mockito.mock; |
||||
|
import static org.mockito.Mockito.when; |
||||
|
|
||||
|
public class DefaultTbLocalSubscriptionServiceTest { |
||||
|
|
||||
|
ListAppender<ILoggingEvent> testLogAppender; |
||||
|
TbLocalSubscriptionService subscriptionService; |
||||
|
|
||||
|
@BeforeEach |
||||
|
public void setUp() throws Exception { |
||||
|
Logger logger = (Logger) LoggerFactory.getLogger(DefaultTbLocalSubscriptionService.class); |
||||
|
testLogAppender = new ListAppender<>(); |
||||
|
testLogAppender.start(); |
||||
|
logger.addAppender(testLogAppender); |
||||
|
|
||||
|
RateLimitService rateLimitService = mock(); |
||||
|
when(rateLimitService.checkRateLimit(eq(LimitedApi.WS_SUBSCRIPTIONS), any(Object.class), nullable(String.class))).thenReturn(true); |
||||
|
PartitionService partitionService = mock(); |
||||
|
when(partitionService.resolve(any(), any(), any())).thenReturn(TopicPartitionInfo.builder().build()); |
||||
|
subscriptionService = new DefaultTbLocalSubscriptionService(mock(), mock(), mock(), partitionService, mock(), mock(), mock(), rateLimitService); |
||||
|
ReflectionTestUtils.setField(subscriptionService, "serviceId", "serviceId"); |
||||
|
} |
||||
|
|
||||
|
@AfterEach |
||||
|
public void tearDown() { |
||||
|
if (testLogAppender != null) { |
||||
|
testLogAppender.stop(); |
||||
|
Logger logger = (Logger) LoggerFactory.getLogger(DefaultTbLocalSubscriptionService.class); |
||||
|
logger.detachAppender(testLogAppender); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void addSubscriptionConcurrentModificationTest() throws Exception { |
||||
|
ListeningExecutorService executorService = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(10)); |
||||
|
TenantId tenantId = new TenantId(UUID.randomUUID()); |
||||
|
DeviceId deviceId = new DeviceId(UUID.randomUUID()); |
||||
|
WebSocketSessionRef sessionRef = mock(); |
||||
|
ReflectionTestUtils.setField(subscriptionService, "subscriptionUpdateExecutor", executorService); |
||||
|
|
||||
|
List<ListenableFuture<?>> futures = new ArrayList<>(); |
||||
|
|
||||
|
try { |
||||
|
subscriptionService.onCoreStartupMsg(TransportProtos.CoreStartupMsg.newBuilder().addAllPartitions(List.of(0)).getDefaultInstanceForType()); |
||||
|
for (int i = 0; i < 50; i++) { |
||||
|
futures.add(executorService.submit(() -> subscriptionService.addSubscription(createSubscription(tenantId, deviceId), sessionRef))); |
||||
|
} |
||||
|
Futures.allAsList(futures).get(); |
||||
|
} finally { |
||||
|
executorService.shutdownNow(); |
||||
|
} |
||||
|
|
||||
|
List<ILoggingEvent> logs = testLogAppender.list; |
||||
|
boolean exceptionLogged = logs.stream() |
||||
|
.filter(event -> event.getThrowableProxy() != null) |
||||
|
.map(event -> event.getThrowableProxy().getClassName()) |
||||
|
.anyMatch(log -> log.equals("java.util.ConcurrentModificationException")); |
||||
|
|
||||
|
assertFalse(exceptionLogged, "Detected ConcurrentModificationException!"); |
||||
|
} |
||||
|
|
||||
|
private TbSubscription<?> createSubscription(TenantId tenantId, EntityId entityId) { |
||||
|
Map<String, Long> keys = new HashMap<>(); |
||||
|
for (int i = 0; i < 50; i++) { |
||||
|
keys.put(RandomStringUtils.randomAlphanumeric(5), 1L); |
||||
|
} |
||||
|
return TbAttributeSubscription.builder() |
||||
|
.tenantId(tenantId) |
||||
|
.entityId(entityId) |
||||
|
.subscriptionId(1) |
||||
|
.sessionId(RandomStringUtils.randomAlphanumeric(5)) |
||||
|
.keyStates(keys) |
||||
|
.build(); |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue