This is an automated email from the ASF dual-hosted git repository.
Aias00 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/shenyu.git
The following commit(s) were added to refs/heads/master by this push:
new ee52a6f948 fix(admin): persist the instance last heartbeat time (#7859)
ee52a6f948 is described below
commit ee52a6f94874ea99620641bd45c316e1db582b72
Author: SouthRiver <[email protected]>
AuthorDate: Tue Oct 20 14:47:10 2026 +0800
fix(admin): persist the instance last heartbeat time (#7859)
The instance mapper's column list, result map, insert and update
omitted last_heart_beat_time, so the last reported heartbeat never
reached the database: reads and restart restores saw 0, and the
create path never copied the VO value either.
Add the column (bigint NOT NULL DEFAULT 0) to the H2/MySQL/PG/Oracle
init schemas and the five 2.7.1->2.7.2 upgrade scripts, persist it
through the mapper XML, copy it in buildInstanceInfoDO, and add
mapper round-trip regression tests plus service-path tests.
Co-authored-by: nanhe17 <[email protected]>
---
db/init/mysql/schema.sql | 1 +
db/init/oracle/schema.sql | 2 ++
db/init/pg/create-table.sql | 2 ++
db/upgrade/2.7.1-upgrade-2.7.2-mysql.sql | 3 ++
db/upgrade/2.7.1-upgrade-2.7.2-ob.sql | 3 ++
db/upgrade/2.7.1-upgrade-2.7.2-og.sql | 4 +++
db/upgrade/2.7.1-upgrade-2.7.2-oracle.sql | 4 +++
db/upgrade/2.7.1-upgrade-2.7.2-pg.sql | 4 +++
db/upgrade/upgrade-guide.md | 8 +++++
.../shenyu/admin/model/entity/InstanceInfoDO.java | 25 ++++++++++---
.../resources/mappers/instance-info-sqlmap.xml | 11 ++++--
.../src/main/resources/sql-script/h2/schema.sql | 1 +
.../admin/mapper/InstanceInfoMapperTest.java | 41 ++++++++++++++++++++++
.../service/impl/InstanceInfoServiceImplTest.java | 34 ++++++++++++++++++
14 files changed, 135 insertions(+), 8 deletions(-)
diff --git a/db/init/mysql/schema.sql b/db/init/mysql/schema.sql
index 02eb27ffc6..6465f0246f 100644
--- a/db/init/mysql/schema.sql
+++ b/db/init/mysql/schema.sql
@@ -2378,6 +2378,7 @@ CREATE TABLE IF NOT EXISTS `instance_info` (
`instance_state` tinyint(4) NOT NULL COMMENT '0-unknown 1-online
2-offline',
`date_created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT
'date_created',
`date_updated` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE
CURRENT_TIMESTAMP COMMENT 'date_updated',
+ `last_heart_beat_time` bigint NOT NULL DEFAULT 0 COMMENT 'last heart
beat time',
PRIMARY KEY (`id`)
);
diff --git a/db/init/oracle/schema.sql b/db/init/oracle/schema.sql
index 0925fe5645..ec385b4c89 100644
--- a/db/init/oracle/schema.sql
+++ b/db/init/oracle/schema.sql
@@ -2951,6 +2951,7 @@ CREATE TABLE instance_info (
instance_state number NOT NULL,
date_created timestamp(3) DEFAULT SYSTIMESTAMP NOT NULL,
date_updated timestamp(3) DEFAULT SYSTIMESTAMP NOT NULL,
+ last_heart_beat_time number DEFAULT 0 NOT NULL,
CONSTRAINT instance_info_pk PRIMARY KEY (id)
);
@@ -2964,6 +2965,7 @@ COMMENT ON COLUMN instance_info.instance_info IS
'instance_info';
COMMENT ON COLUMN instance_info.instance_state IS 'instance_state';
COMMENT ON COLUMN instance_info.date_created IS 'create time';
COMMENT ON COLUMN instance_info.date_updated IS 'update time';
+COMMENT ON COLUMN instance_info.last_heart_beat_time IS 'last heart beat time';
INSERT /*+ IGNORE_ROW_ON_DUPKEY_INDEX(permission(id)) */ INTO permission (id,
object_id, resource_id, date_created, date_updated)
VALUES ('1697146860569642741', '1346358560427216896', '1844026099075554850',
to_timestamp('2023-08-31 06:59:01', 'YYYY-MM-DD HH24:MI:SS'),
to_timestamp('2023-08-31 06:59:01', 'YYYY-MM-DD HH24:MI:SS'));
diff --git a/db/init/pg/create-table.sql b/db/init/pg/create-table.sql
index d5d9cc00eb..d0aadfc4ff 100644
--- a/db/init/pg/create-table.sql
+++ b/db/init/pg/create-table.sql
@@ -2692,6 +2692,7 @@ CREATE TABLE "public"."instance_info" (
"instance_state" int2 NOT NULL,
"date_created" timestamp(3) NOT NULL DEFAULT timezone('UTC-8'::text,
(now())::timestamp(0) without time zone),
"date_updated" timestamp(3) NOT NULL DEFAULT timezone('UTC-8'::text,
(now())::timestamp(0) without time zone),
+ "last_heart_beat_time" int8 NOT NULL DEFAULT 0,
PRIMARY KEY ("id")
);
COMMENT ON COLUMN "public"."instance_info"."id" IS 'primary key';
@@ -2703,6 +2704,7 @@ COMMENT ON COLUMN
"public"."instance_info"."instance_info" IS 'instance_info';
COMMENT ON COLUMN "public"."instance_info"."instance_state" IS
'instance_state';
COMMENT ON COLUMN "public"."instance_info"."date_created" IS 'create time';
COMMENT ON COLUMN "public"."instance_info"."date_updated" IS 'update time';
+COMMENT ON COLUMN "public"."instance_info"."last_heart_beat_time" IS 'last
heart beat time';
INSERT INTO "public"."permission" VALUES ('1697146860569642741',
'1346358560427216896', '1844026099075554850', '2023-08-31 06:59:01',
'2023-08-31 06:59:01');
INSERT INTO "public"."permission" VALUES ('1697146860569642742',
'1346358560427216896', '1844026099075554851', '2023-08-31 07:22:07',
'2023-08-31 07:22:07');
diff --git a/db/upgrade/2.7.1-upgrade-2.7.2-mysql.sql
b/db/upgrade/2.7.1-upgrade-2.7.2-mysql.sql
index 6c2771ce99..2244d929ec 100644
--- a/db/upgrade/2.7.1-upgrade-2.7.2-mysql.sql
+++ b/db/upgrade/2.7.1-upgrade-2.7.2-mysql.sql
@@ -126,3 +126,6 @@ CREATE INDEX idx_namespace_user_ns_user ON
`namespace_user_rel` (namespace_id, u
CREATE INDEX idx_namespace_user_user_ns ON `namespace_user_rel` (user_id,
namespace_id);
CREATE INDEX idx_user_role_user_role ON `user_role` (user_id, role_id);
CREATE INDEX idx_resource_parent ON `resource` (parent_id);
+
+-- Add the durable last heart beat observation to instance records.
+ALTER TABLE `instance_info` ADD COLUMN `last_heart_beat_time` bigint NOT NULL
DEFAULT 0 COMMENT 'last heart beat time';
diff --git a/db/upgrade/2.7.1-upgrade-2.7.2-ob.sql
b/db/upgrade/2.7.1-upgrade-2.7.2-ob.sql
index 3f576e7f44..558aa3f9b4 100644
--- a/db/upgrade/2.7.1-upgrade-2.7.2-ob.sql
+++ b/db/upgrade/2.7.1-upgrade-2.7.2-ob.sql
@@ -99,3 +99,6 @@ JOIN data_permission retained_permission
AND duplicate_permission.data_id = retained_permission.data_id
AND duplicate_permission.id > retained_permission.id;
CREATE UNIQUE INDEX idx_data_permission_user_type ON data_permission (user_id,
data_type, data_id);
+
+-- Add the durable last heart beat observation to instance records.
+ALTER TABLE instance_info ADD COLUMN last_heart_beat_time bigint NOT NULL
DEFAULT 0 COMMENT 'last heart beat time';
diff --git a/db/upgrade/2.7.1-upgrade-2.7.2-og.sql
b/db/upgrade/2.7.1-upgrade-2.7.2-og.sql
index b2afcdc7b6..75984e8301 100644
--- a/db/upgrade/2.7.1-upgrade-2.7.2-og.sql
+++ b/db/upgrade/2.7.1-upgrade-2.7.2-og.sql
@@ -99,3 +99,7 @@ WHERE duplicate_permission.user_id =
retained_permission.user_id
AND duplicate_permission.data_id = retained_permission.data_id
AND duplicate_permission.id > retained_permission.id;
CREATE UNIQUE INDEX "idx_data_permission_user_type" ON
"public"."data_permission" USING btree ("user_id", "data_type", "data_id");
+
+-- Add the durable last heart beat observation to instance records.
+ALTER TABLE "public"."instance_info" ADD COLUMN "last_heart_beat_time" int8
NOT NULL DEFAULT 0;
+COMMENT ON COLUMN "public"."instance_info"."last_heart_beat_time" IS 'last
heart beat time';
diff --git a/db/upgrade/2.7.1-upgrade-2.7.2-oracle.sql
b/db/upgrade/2.7.1-upgrade-2.7.2-oracle.sql
index 136dc1b497..53c22d7f38 100644
--- a/db/upgrade/2.7.1-upgrade-2.7.2-oracle.sql
+++ b/db/upgrade/2.7.1-upgrade-2.7.2-oracle.sql
@@ -116,3 +116,7 @@ WHERE EXISTS (
AND duplicate_permission.id > retained_permission.id
);
CREATE UNIQUE INDEX idx_data_permission_user_type ON data_permission (user_id,
data_type, data_id);
+
+-- Add the durable last heart beat observation to instance records.
+ALTER TABLE instance_info ADD last_heart_beat_time NUMBER DEFAULT 0 NOT NULL;
+COMMENT ON COLUMN instance_info.last_heart_beat_time IS 'last heart beat time';
diff --git a/db/upgrade/2.7.1-upgrade-2.7.2-pg.sql
b/db/upgrade/2.7.1-upgrade-2.7.2-pg.sql
index 750b908564..18c0f7ad1b 100644
--- a/db/upgrade/2.7.1-upgrade-2.7.2-pg.sql
+++ b/db/upgrade/2.7.1-upgrade-2.7.2-pg.sql
@@ -99,3 +99,7 @@ WHERE duplicate_permission.user_id =
retained_permission.user_id
AND duplicate_permission.data_id = retained_permission.data_id
AND duplicate_permission.id > retained_permission.id;
CREATE UNIQUE INDEX "idx_data_permission_user_type" ON
"public"."data_permission" USING btree ("user_id", "data_type", "data_id");
+
+-- Add the durable last heart beat observation to instance records.
+ALTER TABLE "public"."instance_info" ADD COLUMN "last_heart_beat_time" int8
NOT NULL DEFAULT 0;
+COMMENT ON COLUMN "public"."instance_info"."last_heart_beat_time" IS 'last
heart beat time';
diff --git a/db/upgrade/upgrade-guide.md b/db/upgrade/upgrade-guide.md
index 68cbe57c5c..94d8249721 100644
--- a/db/upgrade/upgrade-guide.md
+++ b/db/upgrade/upgrade-guide.md
@@ -37,6 +37,14 @@ existing `id`, and creates the unique
`idx_data_permission_user_type` index.
Each distinct grant is preserved. Fresh installations create the same unique
index directly.
+### Durable instance heartbeat timestamps in 2.7.2
+
+The same `2.7.1-upgrade-2.7.2-<dialect>.sql` script adds a
+`last_heart_beat_time` column to the `instance_info` table, so the last
+reported heartbeat survives Admin restarts and master handover. Existing rows
+are backfilled with `0`, which means "no durable observation yet"; the real
+timestamp is stored on the next heartbeat report.
+
- 2.7.1-upgrade-2.7.2-mysql.sql
- 2.7.1-upgrade-2.7.2-ob.sql
diff --git
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/model/entity/InstanceInfoDO.java
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/model/entity/InstanceInfoDO.java
index c74417392e..a0c3ef5a3b 100644
---
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/model/entity/InstanceInfoDO.java
+++
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/model/entity/InstanceInfoDO.java
@@ -279,6 +279,7 @@ public final class InstanceInfoDO extends BaseDO {
.instanceInfo(instanceInfoVO.getInstanceInfo())
.instanceState(instanceInfoVO.getInstanceState())
.namespaceId(instanceInfoVO.getNamespaceId())
+ .lastHeartBeatTime(instanceInfoVO.getLastHeartBeatTime())
.dateCreated(currentTime)
.dateUpdated(currentTime)
.build();
@@ -301,11 +302,13 @@ public final class InstanceInfoDO extends BaseDO {
private String instanceInfo;
private Integer instanceState;
-
+
private String namespaceId;
-
+
+ private long lastHeartBeatTime;
+
private InstanceInfoDOBuilder() {
-
+
}
/**
@@ -406,8 +409,19 @@ public final class InstanceInfoDO extends BaseDO {
this.namespaceId = namespaceId;
return this;
}
-
-
+
+ /**
+ * lastHeartBeatTime.
+ *
+ * @param lastHeartBeatTime the lastHeartBeatTime
+ * @return InstanceInfoDOBuilder.
+ */
+ public InstanceInfoDOBuilder lastHeartBeatTime(final long
lastHeartBeatTime) {
+ this.lastHeartBeatTime = lastHeartBeatTime;
+ return this;
+ }
+
+
/**
* build InstanceInfoDO.
*
@@ -424,6 +438,7 @@ public final class InstanceInfoDO extends BaseDO {
instanceInfoDO.setInstanceInfo(instanceInfo);
instanceInfoDO.setInstanceState(instanceState);
instanceInfoDO.setNamespaceId(namespaceId);
+ instanceInfoDO.setLastHeartBeatTime(lastHeartBeatTime);
return instanceInfoDO;
}
}
diff --git a/shenyu-admin/src/main/resources/mappers/instance-info-sqlmap.xml
b/shenyu-admin/src/main/resources/mappers/instance-info-sqlmap.xml
index 0160e66e42..61e4379f0a 100644
--- a/shenyu-admin/src/main/resources/mappers/instance-info-sqlmap.xml
+++ b/shenyu-admin/src/main/resources/mappers/instance-info-sqlmap.xml
@@ -28,6 +28,7 @@
<result column="instance_info" jdbcType="VARCHAR"
property="instanceInfo"/>
<result column="instance_state" jdbcType="INTEGER"
property="instanceState"/>
<result column="namespace_id" jdbcType="VARCHAR"
property="namespaceId"/>
+ <result column="last_heart_beat_time" jdbcType="BIGINT"
property="lastHeartBeatTime"/>
</resultMap>
<sql id="Base_Column_List">
@@ -40,6 +41,7 @@
instance_info,
instance_state,
namespace_id,
+ last_heart_beat_time,
date_created,
date_updated
</sql>
@@ -53,7 +55,8 @@
instance_port = #{instancePort, jdbcType=VARCHAR},
instance_type = #{instanceType, jdbcType=VARCHAR},
instance_info = #{instanceInfo, jdbcType=VARCHAR},
- instance_state = #{instanceState, jdbcType=TINYINT}
+ instance_state = #{instanceState, jdbcType=TINYINT},
+ last_heart_beat_time = #{lastHeartBeatTime, jdbcType=BIGINT}
WHERE
id = #{id, jdbcType=VARCHAR}
</update>
@@ -164,7 +167,8 @@
instance_type,
instance_info,
instance_state,
- namespace_id)
+ namespace_id,
+ last_heart_beat_time)
VALUES
(#{id,jdbcType=VARCHAR},
#{dateCreated,jdbcType=TIMESTAMP},
@@ -174,7 +178,8 @@
#{instanceType,jdbcType=VARCHAR},
#{instanceInfo,jdbcType=VARCHAR},
#{instanceState,jdbcType=TINYINT},
- #{namespaceId,jdbcType=VARCHAR})
+ #{namespaceId,jdbcType=VARCHAR},
+ #{lastHeartBeatTime,jdbcType=BIGINT})
</insert>
<delete id="delete" parameterType="java.lang.String">
diff --git a/shenyu-admin/src/main/resources/sql-script/h2/schema.sql
b/shenyu-admin/src/main/resources/sql-script/h2/schema.sql
index 42b4a3d14c..23a4bdb726 100644
--- a/shenyu-admin/src/main/resources/sql-script/h2/schema.sql
+++ b/shenyu-admin/src/main/resources/sql-script/h2/schema.sql
@@ -1417,6 +1417,7 @@ CREATE TABLE IF NOT EXISTS instance_info
`instance_state` int NOT NULL COMMENT '0-unknown 1-online 2-offline',
`date_created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT
'date_created',
`date_updated` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE
CURRENT_TIMESTAMP COMMENT 'date_updated',
+ `last_heart_beat_time` bigint NOT NULL DEFAULT 0 COMMENT 'last heart
beat time',
PRIMARY KEY (`id`)
);
diff --git
a/shenyu-admin/src/test/java/org/apache/shenyu/admin/mapper/InstanceInfoMapperTest.java
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/mapper/InstanceInfoMapperTest.java
index a6148bebef..9b0520854b 100644
---
a/shenyu-admin/src/test/java/org/apache/shenyu/admin/mapper/InstanceInfoMapperTest.java
+++
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/mapper/InstanceInfoMapperTest.java
@@ -45,6 +45,8 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
*/
public final class InstanceInfoMapperTest extends
AbstractSpringIntegrationTest {
+ private static final long FIXED_HEART_BEAT_TIME = 1700000000000L;
+
@Resource
private InstanceInfoMapper instanceInfoMapper;
@@ -127,6 +129,45 @@ public final class InstanceInfoMapperTest extends
AbstractSpringIntegrationTest
assertEquals(1, deleted);
}
+ @Test
+ void insertRoundTripsLastHeartBeatTime() {
+ InstanceInfoDO data = buildInstance();
+ data.setLastHeartBeatTime(FIXED_HEART_BEAT_TIME);
+ assertEquals(1, instanceInfoMapper.insert(data));
+ try {
+ InstanceInfoDO found = instanceInfoMapper.selectById(data.getId());
+ assertNotNull(found);
+ assertEquals(FIXED_HEART_BEAT_TIME, found.getLastHeartBeatTime());
+
+ InstanceQuery query = new InstanceQuery();
+ query.setInstanceId(data.getId());
+ query.setNamespaceId(data.getNamespaceId());
+ query.setInstanceType(data.getInstanceType());
+ query.setInstanceIp(data.getInstanceIp());
+ query.setInstancePort(data.getInstancePort());
+ InstanceInfoDO one = instanceInfoMapper.selectOneByQuery(query);
+ assertNotNull(one);
+ assertEquals(FIXED_HEART_BEAT_TIME, one.getLastHeartBeatTime());
+
+ InstanceInfoDO viaResultMap =
instanceInfoMapper.findAllByNamespaceId(data.getNamespaceId())
+ .stream().filter(item ->
data.getId().equals(item.getId())).findFirst().orElse(null);
+ assertNotNull(viaResultMap);
+ assertEquals(FIXED_HEART_BEAT_TIME,
viaResultMap.getLastHeartBeatTime());
+ } finally {
+ instanceInfoMapper.delete(data.getId());
+ }
+ }
+
+ @Test
+ void updateRoundTripsLastHeartBeatTime() {
+ instance.setLastHeartBeatTime(FIXED_HEART_BEAT_TIME);
+ int updated = instanceInfoMapper.updateById(instance);
+ assertEquals(1, updated);
+ InstanceInfoDO found = instanceInfoMapper.selectById(instance.getId());
+ assertNotNull(found);
+ assertEquals(FIXED_HEART_BEAT_TIME, found.getLastHeartBeatTime());
+ }
+
@Test
void testLegacyInformationIsReadableWithoutRewritingTheStoredRow() {
String information = "{\"cpu\":4}";
diff --git
a/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/impl/InstanceInfoServiceImplTest.java
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/impl/InstanceInfoServiceImplTest.java
index 0a213be372..1bc42618ed 100644
---
a/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/impl/InstanceInfoServiceImplTest.java
+++
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/impl/InstanceInfoServiceImplTest.java
@@ -25,6 +25,7 @@ import org.apache.shenyu.admin.model.query.InstanceQuery;
import org.apache.shenyu.admin.model.vo.InstanceInfoVO;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
@@ -38,6 +39,7 @@ import static
org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
+import static org.mockito.Mockito.verify;
/**
* Test case for {@link InstanceInfoServiceImpl}.
@@ -46,6 +48,8 @@ import static org.mockito.BDDMockito.given;
@MockitoSettings(strictness = Strictness.LENIENT)
public final class InstanceInfoServiceImplTest {
+ private static final long FIXED_HEART_BEAT_TIME = 1700000000000L;
+
@InjectMocks
private InstanceInfoServiceImpl instanceInfoService;
@@ -74,6 +78,36 @@ public final class InstanceInfoServiceImplTest {
assertNotEquals(first.getId(), second.getId());
}
+ @Test
+ public void testCreatePersistsReportedHeartBeatTime() {
+
given(instanceInfoMapper.selectOneByQuery(any(InstanceQuery.class))).willReturn(null);
+ instanceInfoService.createOrUpdate(buildReport());
+ ArgumentCaptor<InstanceInfoDO> captor =
ArgumentCaptor.forClass(InstanceInfoDO.class);
+ verify(instanceInfoMapper).insert(captor.capture());
+ assertEquals(FIXED_HEART_BEAT_TIME,
captor.getValue().getLastHeartBeatTime());
+ }
+
+ @Test
+ public void testUpdatePersistsReportedHeartBeatTime() {
+
given(instanceInfoMapper.selectOneByQuery(any(InstanceQuery.class))).willReturn(buildDO("instance-1"));
+ instanceInfoService.createOrUpdate(buildReport());
+ ArgumentCaptor<InstanceInfoDO> captor =
ArgumentCaptor.forClass(InstanceInfoDO.class);
+ verify(instanceInfoMapper).updateById(captor.capture());
+ assertEquals(FIXED_HEART_BEAT_TIME,
captor.getValue().getLastHeartBeatTime());
+ }
+
+ private InstanceInfoVO buildReport() {
+ InstanceInfoVO report = new InstanceInfoVO();
+ report.setInstanceIp("127.0.0.1");
+ report.setInstancePort("8080");
+ report.setInstanceType("grpc");
+ report.setInstanceInfo("info");
+ report.setInstanceState(1);
+ report.setNamespaceId("ns");
+ report.setLastHeartBeatTime(FIXED_HEART_BEAT_TIME);
+ return report;
+ }
+
private InstanceInfoDO buildDO(final String id) {
return InstanceInfoDO.builder()
.id(id)