Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ public JdbcRegistryServer(JdbcRegistryDataRepository jdbcRegistryDataRepository,
}

@Override
public void start() {
public synchronized void start() {
if (jdbcRegistryServerState != JdbcRegistryServerState.INIT) {
// The server is already started or stopped, will not start again.
return;
Expand Down Expand Up @@ -167,7 +167,7 @@ public void deregisterClient(IJdbcRegistryClient jdbcRegistryClient) {
}

@Override
public JdbcRegistryServerState getServerState() {
public synchronized JdbcRegistryServerState getServerState() {
return jdbcRegistryServerState;
}

Expand Down Expand Up @@ -256,7 +256,9 @@ public void releaseJdbcRegistryLock(Long clientId, String lockKey) {

@Override
public void close() {
jdbcRegistryServerState = JdbcRegistryServerState.STOPPED;
synchronized (this) {
jdbcRegistryServerState = JdbcRegistryServerState.STOPPED;
}
Comment on lines +259 to +261

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
synchronized (this) {
jdbcRegistryServerState = JdbcRegistryServerState.STOPPED;
}
synchronized (this) {
if(jdbcRegistryServerState == JdbcRegistryServerState.STOPPED) {
log.warn("xx is already closed");
return
}
jdbcRegistryServerState = JdbcRegistryServerState.STOPPED;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Avoid duplicate close?

schedulerThreadExecutor.shutdown();
List<Long> clientIds = jdbcRegistryClients.stream()
.map(IJdbcRegistryClient::getJdbcRegistryClientIdentify)
Expand All @@ -269,7 +271,7 @@ public void close() {

private void purgeInvalidJdbcRegistryMetadata() {
final StopWatch stopWatch = StopWatch.createStarted();
if (jdbcRegistryServerState == JdbcRegistryServerState.STOPPED) {
if (getServerState() == JdbcRegistryServerState.STOPPED) {
return;
}
// remove the client which is already dead from the registry, and remove it's related data and lock.
Expand Down Expand Up @@ -321,8 +323,11 @@ private void refreshClientsHeartbeat() {
if (CollectionUtils.isEmpty(jdbcRegistryClients)) {
return;
}
if (jdbcRegistryServerState == JdbcRegistryServerState.STOPPED) {
log.warn("The JdbcRegistryServer is STOPPED, will not refresh clients: {} heartbeat.",
JdbcRegistryServerState currentState = getServerState();
if (currentState == JdbcRegistryServerState.STOPPED
|| currentState == JdbcRegistryServerState.DISCONNECTED) {
log.warn("The JdbcRegistryServer is {}, will not refresh clients: {} heartbeat.",
currentState,
CollectionUtils.collect(jdbcRegistryClients, IJdbcRegistryClient::getJdbcRegistryClientIdentify));
return;
}
Expand All @@ -341,27 +346,27 @@ private void refreshClientsHeartbeat() {
}
JdbcRegistryClientHeartbeatDTO clone = jdbcRegistryClientHeartbeatDTO.clone();
clone.setLastHeartbeatTime(now);
jdbcRegistryClientRepository.updateById(jdbcRegistryClientHeartbeatDTO);
if (!jdbcRegistryClientRepository.updateById(clone)) {
log.error("The client heartbeat has expired: {}", jdbcRegistryClientHeartbeatDTO.getId());
transitionToDisconnected();
return;
Comment on lines +350 to +352

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
log.error("The client heartbeat has expired: {}", jdbcRegistryClientHeartbeatDTO.getId());
transitionToDisconnected();
return;
throw xxException()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should consider session timeout.

}
jdbcRegistryClientHeartbeatDTO.setLastHeartbeatTime(clone.getLastHeartbeatTime());
}
if (jdbcRegistryServerState == JdbcRegistryServerState.SUSPENDED) {
jdbcRegistryServerState = JdbcRegistryServerState.STARTED;
doTriggerReconnectedListener();
}
transitionToStarted();
lastSuccessHeartbeat = now;
log.debug("Success refresh clients: {} heartbeat.",
CollectionUtils.collect(jdbcRegistryClients, IJdbcRegistryClient::getJdbcRegistryClientIdentify));
} catch (Exception ex) {
log.error("Failed to refresh the client's term", ex);
switch (jdbcRegistryServerState) {
switch (getServerState()) {
case STARTED:
jdbcRegistryServerState = JdbcRegistryServerState.SUSPENDED;
transitionToSuspended();
break;
case SUSPENDED:
if (System.currentTimeMillis() - lastSuccessHeartbeat > jdbcRegistryProperties.getSessionTimeout()
.toMillis()) {
jdbcRegistryServerState = JdbcRegistryServerState.DISCONNECTED;
doTriggerOnDisConnectedListener();
transitionToDisconnected();
}
break;
default:
Expand All @@ -370,6 +375,30 @@ private void refreshClientsHeartbeat() {
}
}

private synchronized void transitionToStarted() {
if (jdbcRegistryServerState != JdbcRegistryServerState.SUSPENDED) {
return;
}
jdbcRegistryServerState = JdbcRegistryServerState.STARTED;
doTriggerReconnectedListener();
}

private synchronized void transitionToSuspended() {
if (jdbcRegistryServerState != JdbcRegistryServerState.STARTED) {
return;
}
jdbcRegistryServerState = JdbcRegistryServerState.SUSPENDED;
}

private synchronized void transitionToDisconnected() {
if (jdbcRegistryServerState != JdbcRegistryServerState.STARTED
&& jdbcRegistryServerState != JdbcRegistryServerState.SUSPENDED) {
return;
}
jdbcRegistryServerState = JdbcRegistryServerState.DISCONNECTED;
doTriggerOnDisConnectedListener();
}
Comment on lines +378 to +400

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use compareAndSwap and volatile to reduce synchronized?


private void doTriggerReconnectedListener() {
log.info("Trigger:onReconnected listener.");
connectionStateListeners.forEach(listener -> {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.apache.dolphinscheduler.plugin.registry.jdbc.server;

import org.apache.dolphinscheduler.plugin.registry.jdbc.JdbcRegistryProperties;
import org.apache.dolphinscheduler.plugin.registry.jdbc.client.IJdbcRegistryClient;
import org.apache.dolphinscheduler.plugin.registry.jdbc.client.JdbcRegistryClientIdentify;
import org.apache.dolphinscheduler.plugin.registry.jdbc.model.DTO.JdbcRegistryClientHeartbeatDTO;
import org.apache.dolphinscheduler.plugin.registry.jdbc.repository.JdbcRegistryClientRepository;
import org.apache.dolphinscheduler.plugin.registry.jdbc.repository.JdbcRegistryDataChangeEventRepository;
import org.apache.dolphinscheduler.plugin.registry.jdbc.repository.JdbcRegistryDataRepository;
import org.apache.dolphinscheduler.plugin.registry.jdbc.repository.JdbcRegistryLockRepository;

import java.time.Duration;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.transaction.support.TransactionTemplate;

import com.google.common.truth.Truth;

@ExtendWith(MockitoExtension.class)
class JdbcRegistryServerTest {

private static final JdbcRegistryClientIdentify CLIENT_IDENTIFY =
new JdbcRegistryClientIdentify(1L, "test-client");

@Mock
private JdbcRegistryDataRepository jdbcRegistryDataRepository;

@Mock
private JdbcRegistryLockRepository jdbcRegistryLockRepository;

@Mock
private JdbcRegistryClientRepository jdbcRegistryClientRepository;

@Mock
private JdbcRegistryDataChangeEventRepository jdbcRegistryDataChangeEventRepository;

@Mock
private TransactionTemplate transactionTemplate;

@Mock
private IJdbcRegistryClient jdbcRegistryClient;

@Mock
private ConnectionStateListener connectionStateListener;

private JdbcRegistryServer jdbcRegistryServer;

@BeforeEach
void setUp() {
JdbcRegistryProperties jdbcRegistryProperties = new JdbcRegistryProperties();
jdbcRegistryProperties.setSessionTimeout(Duration.ofSeconds(1));
jdbcRegistryServer = new JdbcRegistryServer(
jdbcRegistryDataRepository,
jdbcRegistryLockRepository,
jdbcRegistryClientRepository,
jdbcRegistryDataChangeEventRepository,
jdbcRegistryProperties,
transactionTemplate);
Mockito.when(jdbcRegistryClient.getJdbcRegistryClientIdentify()).thenReturn(CLIENT_IDENTIFY);
jdbcRegistryServer.registerClient(jdbcRegistryClient);
jdbcRegistryServer.subscribeConnectionStateChange(connectionStateListener);
}

@AfterEach
void tearDown() {
jdbcRegistryServer.close();
}

@Test
void refreshClientsHeartbeat_shouldDisconnectWhenHeartbeatRecordWasPurged() {
ReflectionTestUtils.setField(jdbcRegistryServer, "jdbcRegistryServerState",
JdbcRegistryServerState.SUSPENDED);
ReflectionTestUtils.setField(jdbcRegistryServer, "lastSuccessHeartbeat", 0L);
Mockito.when(jdbcRegistryClientRepository.updateById(Mockito.any())).thenReturn(false);

ReflectionTestUtils.invokeMethod(jdbcRegistryServer, "refreshClientsHeartbeat");

Truth.assertThat(jdbcRegistryServer.getServerState()).isEqualTo(JdbcRegistryServerState.DISCONNECTED);
Mockito.verify(connectionStateListener).onDisConnected();
}

@Test
void refreshClientsHeartbeat_shouldDisconnectImmediatelyWhenStartedHeartbeatRecordWasPurged() {
ReflectionTestUtils.setField(jdbcRegistryServer, "jdbcRegistryServerState", JdbcRegistryServerState.STARTED);
Mockito.when(jdbcRegistryClientRepository.updateById(Mockito.any())).thenReturn(false);

ReflectionTestUtils.invokeMethod(jdbcRegistryServer, "refreshClientsHeartbeat");
ReflectionTestUtils.invokeMethod(jdbcRegistryServer, "refreshClientsHeartbeat");

Truth.assertThat(jdbcRegistryServer.getServerState()).isEqualTo(JdbcRegistryServerState.DISCONNECTED);
Mockito.verify(jdbcRegistryClientRepository).updateById(Mockito.any());
Mockito.verify(connectionStateListener).onDisConnected();
}

@Test
void refreshClientsHeartbeat_shouldNotDisconnectWhenCloseWinsRace() throws Exception {
ReflectionTestUtils.setField(jdbcRegistryServer, "jdbcRegistryServerState", JdbcRegistryServerState.STARTED);
CountDownLatch heartbeatUpdateStarted = new CountDownLatch(1);
CountDownLatch allowHeartbeatUpdateToFinish = new CountDownLatch(1);
Mockito.when(jdbcRegistryClientRepository.updateById(Mockito.any())).thenAnswer(invocation -> {
heartbeatUpdateStarted.countDown();
allowHeartbeatUpdateToFinish.await(5, TimeUnit.SECONDS);
return false;
});
ExecutorService heartbeatExecutor = Executors.newSingleThreadExecutor();
Future<?> heartbeatFuture = heartbeatExecutor.submit(() -> {
ReflectionTestUtils.invokeMethod(jdbcRegistryServer, "refreshClientsHeartbeat");
});

try {
Truth.assertThat(heartbeatUpdateStarted.await(5, TimeUnit.SECONDS)).isTrue();
jdbcRegistryServer.close();
allowHeartbeatUpdateToFinish.countDown();
heartbeatFuture.get(5, TimeUnit.SECONDS);
} finally {
allowHeartbeatUpdateToFinish.countDown();
heartbeatExecutor.shutdownNow();
}

Truth.assertThat(jdbcRegistryServer.getServerState()).isEqualTo(JdbcRegistryServerState.STOPPED);
Mockito.verify(connectionStateListener, Mockito.never()).onDisConnected();
}

@Test
void refreshClientsHeartbeat_shouldNotReconnectWhenCloseWinsSuccessfulHeartbeatRace() throws Exception {
ReflectionTestUtils.setField(jdbcRegistryServer, "jdbcRegistryServerState",
JdbcRegistryServerState.SUSPENDED);
CountDownLatch heartbeatUpdateStarted = new CountDownLatch(1);
CountDownLatch allowHeartbeatUpdateToFinish = new CountDownLatch(1);
Mockito.when(jdbcRegistryClientRepository.updateById(Mockito.any())).thenAnswer(invocation -> {
heartbeatUpdateStarted.countDown();
allowHeartbeatUpdateToFinish.await(5, TimeUnit.SECONDS);
return true;
});
ExecutorService heartbeatExecutor = Executors.newSingleThreadExecutor();
Future<?> heartbeatFuture = heartbeatExecutor.submit(() -> {
ReflectionTestUtils.invokeMethod(jdbcRegistryServer, "refreshClientsHeartbeat");
});

try {
Truth.assertThat(heartbeatUpdateStarted.await(5, TimeUnit.SECONDS)).isTrue();
jdbcRegistryServer.close();
allowHeartbeatUpdateToFinish.countDown();
heartbeatFuture.get(5, TimeUnit.SECONDS);
} finally {
allowHeartbeatUpdateToFinish.countDown();
heartbeatExecutor.shutdownNow();
}

Truth.assertThat(jdbcRegistryServer.getServerState()).isEqualTo(JdbcRegistryServerState.STOPPED);
Mockito.verify(connectionStateListener, Mockito.never()).onReconnected();
Mockito.verify(connectionStateListener, Mockito.never()).onDisConnected();
}

@Test
void refreshClientsHeartbeat_shouldNotSuspendWhenCloseWinsFailedHeartbeatRace() throws Exception {
ReflectionTestUtils.setField(jdbcRegistryServer, "jdbcRegistryServerState", JdbcRegistryServerState.STARTED);
CountDownLatch heartbeatUpdateStarted = new CountDownLatch(1);
CountDownLatch allowHeartbeatUpdateToFail = new CountDownLatch(1);
Mockito.when(jdbcRegistryClientRepository.updateById(Mockito.any())).thenAnswer(invocation -> {
heartbeatUpdateStarted.countDown();
allowHeartbeatUpdateToFail.await(5, TimeUnit.SECONDS);
throw new RuntimeException("Heartbeat update failed");
});
ExecutorService heartbeatExecutor = Executors.newSingleThreadExecutor();
Future<?> heartbeatFuture = heartbeatExecutor.submit(() -> {
ReflectionTestUtils.invokeMethod(jdbcRegistryServer, "refreshClientsHeartbeat");
});

try {
Truth.assertThat(heartbeatUpdateStarted.await(5, TimeUnit.SECONDS)).isTrue();
jdbcRegistryServer.close();
allowHeartbeatUpdateToFail.countDown();
heartbeatFuture.get(5, TimeUnit.SECONDS);
} finally {
allowHeartbeatUpdateToFail.countDown();
heartbeatExecutor.shutdownNow();
}

Truth.assertThat(jdbcRegistryServer.getServerState()).isEqualTo(JdbcRegistryServerState.STOPPED);
Mockito.verify(connectionStateListener, Mockito.never()).onReconnected();
Mockito.verify(connectionStateListener, Mockito.never()).onDisConnected();
}

@Test
void refreshClientsHeartbeat_shouldPersistCurrentHeartbeatTimestamp() {
ArgumentCaptor<JdbcRegistryClientHeartbeatDTO> registeredHeartbeat =
ArgumentCaptor.forClass(JdbcRegistryClientHeartbeatDTO.class);
Mockito.verify(jdbcRegistryClientRepository).insert(registeredHeartbeat.capture());
registeredHeartbeat.getValue().setLastHeartbeatTime(0L);
AtomicLong persistedHeartbeatTimestamp = new AtomicLong(-1L);
Mockito.when(jdbcRegistryClientRepository.updateById(Mockito.any())).thenAnswer(invocation -> {
JdbcRegistryClientHeartbeatDTO heartbeat = invocation.getArgument(0);
persistedHeartbeatTimestamp.set(heartbeat.getLastHeartbeatTime());
return true;
});

ReflectionTestUtils.invokeMethod(jdbcRegistryServer, "refreshClientsHeartbeat");

Truth.assertThat(persistedHeartbeatTimestamp.get()).isGreaterThan(0L);
}

@Test
void refreshClientsHeartbeat_shouldNotRefreshAfterDisconnected() {
ReflectionTestUtils.setField(jdbcRegistryServer, "jdbcRegistryServerState",
JdbcRegistryServerState.DISCONNECTED);

ReflectionTestUtils.invokeMethod(jdbcRegistryServer, "refreshClientsHeartbeat");

Mockito.verify(jdbcRegistryClientRepository, Mockito.never()).updateById(Mockito.any());
}
}
Loading