-
Notifications
You must be signed in to change notification settings - Fork 5.1k
[Fix-18274][Registry] Handle expired JDBC heartbeat sessions #18416
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Changes from all commits
0a31733
2cf868b
cbdfd1d
79cc4a0
eed6305
93e0b2f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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; | ||||||||||
|
|
@@ -167,7 +167,7 @@ public void deregisterClient(IJdbcRegistryClient jdbcRegistryClient) { | |||||||||
| } | ||||||||||
|
|
||||||||||
| @Override | ||||||||||
| public JdbcRegistryServerState getServerState() { | ||||||||||
| public synchronized JdbcRegistryServerState getServerState() { | ||||||||||
| return jdbcRegistryServerState; | ||||||||||
| } | ||||||||||
|
|
||||||||||
|
|
@@ -256,7 +256,9 @@ public void releaseJdbcRegistryLock(Long clientId, String lockKey) { | |||||||||
|
|
||||||||||
| @Override | ||||||||||
| public void close() { | ||||||||||
| jdbcRegistryServerState = JdbcRegistryServerState.STOPPED; | ||||||||||
| synchronized (this) { | ||||||||||
| jdbcRegistryServerState = JdbcRegistryServerState.STOPPED; | ||||||||||
| } | ||||||||||
| schedulerThreadExecutor.shutdown(); | ||||||||||
| List<Long> clientIds = jdbcRegistryClients.stream() | ||||||||||
| .map(IJdbcRegistryClient::getJdbcRegistryClientIdentify) | ||||||||||
|
|
@@ -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. | ||||||||||
|
|
@@ -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; | ||||||||||
| } | ||||||||||
|
|
@@ -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
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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: | ||||||||||
|
|
@@ -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
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we use |
||||||||||
|
|
||||||||||
| private void doTriggerReconnectedListener() { | ||||||||||
| log.info("Trigger:onReconnected listener."); | ||||||||||
| connectionStateListeners.forEach(listener -> { | ||||||||||
|
|
||||||||||
| 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()); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Avoid duplicate
close?