From 911f043ee903cf29b13c7f05b9783fbc9f362298 Mon Sep 17 00:00:00 2001 From: liuhy Date: Wed, 1 Apr 2026 20:05:39 +0800 Subject: [PATCH 1/5] feat: add manual discovery upstream status --- .../2026-04-01-upstream-manual-status.md | 88 ++++++++++++++++++ ...026-04-01-upstream-manual-status-design.md | 92 +++++++++++++++++++ .../admin/controller/UpstreamController.java | 71 ++++++++++++++ .../admin/mapper/DiscoveryUpstreamMapper.java | 11 +++ .../admin/model/dto/DiscoveryUpstreamDTO.java | 25 +++++ .../model/dto/UpstreamManualStatusDTO.java | 74 +++++++++++++++ .../model/entity/DiscoveryUpstreamDO.java | 42 +++++++++ .../admin/model/vo/DiscoveryUpstreamVO.java | 23 +++++ .../service/DiscoveryUpstreamService.java | 10 ++ .../impl/DiscoveryUpstreamServiceImpl.java | 60 +++++++++++- .../admin/transfer/DiscoveryTransfer.java | 11 +++ .../admin/utils/CommonUpstreamUtils.java | 2 + .../mappers/discovery-upstream-sqlmap.xml | 21 ++++- .../main/resources/sql-script/h2/schema.sql | 1 + .../service/DiscoveryUpstreamServiceTest.java | 53 ++++++++++- .../common/dto/DiscoveryUpstreamData.java | 43 ++++++++- .../dto/convert/selector/CommonUpstream.java | 34 +++++++ .../dto/convert/selector/GrpcUpstream.java | 17 ++++ .../enums/UpstreamManualStatusEnum.java | 60 ++++++++++++ .../shenyu/loadbalancer/entity/Upstream.java | 51 ++++++++++ .../factory/LoadBalancerFactory.java | 13 ++- .../factory/LoadBalancerFactoryTest.java | 26 ++++++ .../handler/DivideUpstreamDataHandler.java | 1 + .../DivideUpstreamDataHandlerTest.java | 20 ++++ .../GrpcDiscoveryUpstreamDataHandler.java | 1 + .../grpc/loadbalance/picker/ShenyuPicker.java | 5 + .../handler/WebSocketUpstreamDataHandler.java | 1 + 27 files changed, 850 insertions(+), 6 deletions(-) create mode 100644 docs/superpowers/plans/2026-04-01-upstream-manual-status.md create mode 100644 docs/superpowers/specs/2026-04-01-upstream-manual-status-design.md create mode 100644 shenyu-admin/src/main/java/org/apache/shenyu/admin/controller/UpstreamController.java create mode 100644 shenyu-admin/src/main/java/org/apache/shenyu/admin/model/dto/UpstreamManualStatusDTO.java create mode 100644 shenyu-common/src/main/java/org/apache/shenyu/common/enums/UpstreamManualStatusEnum.java diff --git a/docs/superpowers/plans/2026-04-01-upstream-manual-status.md b/docs/superpowers/plans/2026-04-01-upstream-manual-status.md new file mode 100644 index 000000000000..cc377a0e92ea --- /dev/null +++ b/docs/superpowers/plans/2026-04-01-upstream-manual-status.md @@ -0,0 +1,88 @@ +# Upstream Manual Status Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add persisted manual upstream offline control and make Admin, sync payloads, and Gateway selection honor it end to end. + +**Architecture:** Introduce a shared `UpstreamManualStatusEnum`, persist it on `discovery_upstream`, update Admin service/controller flows to publish sync events after manual changes, and carry the new field through sync DTOs into Gateway cache objects where load-balancer selection filters forced-offline upstreams. + +**Tech Stack:** Java, Spring MVC, MyBatis, Maven, JUnit 5, Mockito + +--- + +### Task 1: Add Failing Admin Tests + +**Files:** +- Modify: `shenyu-admin/src/test/java/org/apache/shenyu/admin/service/DiscoveryUpstreamServiceTest.java` +- Modify: `shenyu-admin/src/test/java/org/apache/shenyu/admin/service/SyncDataServiceTest.java` +- Test: `shenyu-admin/src/test/java/org/apache/shenyu/admin/service/DiscoveryUpstreamServiceTest.java` + +- [ ] **Step 1: Write failing tests for manual status update and status short-circuit** +- [ ] **Step 2: Write failing assertions that sync payload exposes `manualStatus`** +- [ ] **Step 3: Run admin tests to verify they fail for missing field and behavior** +- [ ] **Step 4: Keep failures focused on the new contract** + +### Task 2: Add Failing Gateway Tests + +**Files:** +- Modify: `shenyu-loadbalancer/src/test/java/org/apache/shenyu/loadbalancer/factory/LoadBalancerFactoryTest.java` +- Modify: `shenyu-plugin/shenyu-plugin-proxy/shenyu-plugin-divide/src/test/java/org/apache/shenyu/plugin/divide/handler/DivideUpstreamDataHandlerTest.java` +- Test: `shenyu-loadbalancer/src/test/java/org/apache/shenyu/loadbalancer/factory/LoadBalancerFactoryTest.java` + +- [ ] **Step 1: Add a failing load-balancer test that excludes `FORCE_OFFLINE` upstreams** +- [ ] **Step 2: Add a failing divide handler test that maps sync payload `manualStatus` into cached upstreams** +- [ ] **Step 3: Run targeted gateway tests to verify red state** + +### Task 3: Implement Shared Enum And DTO Changes + +**Files:** +- Create: `shenyu-common/src/main/java/org/apache/shenyu/common/enums/UpstreamManualStatusEnum.java` +- Modify: `shenyu-common/src/main/java/org/apache/shenyu/common/dto/DiscoveryUpstreamData.java` +- Modify: `shenyu-loadbalancer/src/main/java/org/apache/shenyu/loadbalancer/entity/Upstream.java` + +- [ ] **Step 1: Add the shared enum with `NONE` and `FORCE_OFFLINE`** +- [ ] **Step 2: Extend sync DTO and cached upstream entity with `manualStatus`** +- [ ] **Step 3: Keep defaults backward compatible with `NONE`** + +### Task 4: Implement Admin Persistence And API + +**Files:** +- Modify: `shenyu-admin/src/main/resources/sql-script/h2/schema.sql` +- Modify: `shenyu-admin/src/main/java/org/apache/shenyu/admin/model/entity/DiscoveryUpstreamDO.java` +- Modify: `shenyu-admin/src/main/java/org/apache/shenyu/admin/model/dto/DiscoveryUpstreamDTO.java` +- Modify: `shenyu-admin/src/main/java/org/apache/shenyu/admin/model/vo/DiscoveryUpstreamVO.java` +- Modify: `shenyu-admin/src/main/java/org/apache/shenyu/admin/mapper/DiscoveryUpstreamMapper.java` +- Modify: `shenyu-admin/src/main/resources/mappers/discovery-upstream-sqlmap.xml` +- Modify: `shenyu-admin/src/main/java/org/apache/shenyu/admin/transfer/DiscoveryTransfer.java` +- Modify: `shenyu-admin/src/main/java/org/apache/shenyu/admin/service/DiscoveryUpstreamService.java` +- Modify: `shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/DiscoveryUpstreamServiceImpl.java` +- Create: `shenyu-admin/src/main/java/org/apache/shenyu/admin/model/dto/UpstreamManualStatusDTO.java` +- Create: `shenyu-admin/src/main/java/org/apache/shenyu/admin/controller/UpstreamController.java` + +- [ ] **Step 1: Persist `manual_status` and map it through DO/DTO/VO/Mapper** +- [ ] **Step 2: Add service methods to change manual status and publish fresh discovery events** +- [ ] **Step 3: Add `/upstream/offline` and `/upstream/online` controller endpoints** + +### Task 5: Implement Heartbeat Short-Circuit And Gateway Filtering + +**Files:** +- Modify: `shenyu-admin/src/main/java/org/apache/shenyu/admin/service/register/AbstractShenyuClientRegisterServiceImpl.java` +- Modify: `shenyu-plugin/shenyu-plugin-proxy/shenyu-plugin-divide/src/main/java/org/apache/shenyu/plugin/divide/handler/DivideUpstreamDataHandler.java` +- Modify: `shenyu-plugin/shenyu-plugin-proxy/shenyu-plugin-websocket/src/main/java/org/apache/shenyu/plugin/websocket/handler/WebSocketUpstreamDataHandler.java` +- Modify: `shenyu-plugin/shenyu-plugin-proxy/shenyu-plugin-rpc/shenyu-plugin-grpc/src/main/java/org/apache/shenyu/plugin/grpc/handler/GrpcDiscoveryUpstreamDataHandler.java` +- Modify: `shenyu-loadbalancer/src/main/java/org/apache/shenyu/loadbalancer/factory/LoadBalancerFactory.java` + +- [ ] **Step 1: Prevent alive/status recovery when the DB record is `FORCE_OFFLINE`** +- [ ] **Step 2: Map synced `manualStatus` into plugin-specific upstream cache objects** +- [ ] **Step 3: Filter forced-offline upstreams before selection** + +### Task 6: Verify Green State + +**Files:** +- Modify: `docs/superpowers/specs/2026-04-01-upstream-manual-status-design.md` +- Modify: `docs/superpowers/plans/2026-04-01-upstream-manual-status.md` + +- [ ] **Step 1: Run targeted Maven tests for admin, loadbalancer, and divide modules** +- [ ] **Step 2: Run a focused compile if any cross-module breakage appears** +- [ ] **Step 3: Review git diff for unintended changes** +- [ ] **Step 4: Commit with one feature commit** diff --git a/docs/superpowers/specs/2026-04-01-upstream-manual-status-design.md b/docs/superpowers/specs/2026-04-01-upstream-manual-status-design.md new file mode 100644 index 000000000000..8678b76d9817 --- /dev/null +++ b/docs/superpowers/specs/2026-04-01-upstream-manual-status-design.md @@ -0,0 +1,92 @@ +# Upstream Manual Status Design + +## Goal + +Add a persisted manual upstream control flag that lets Admin force a discovery upstream offline without being overwritten by heartbeat recovery, and make Gateway honor that flag during upstream selection. + +## Background + +Today `discovery_upstream.upstream_status` is used for automatic liveness. Admin-triggered manual offline and automatic health recovery share the same status channel, so a heartbeat or recovery path can bring a manually disabled upstream back into traffic. + +## Chosen Approach + +Use a separate manual status field. + +- Persist `manual_status` on `discovery_upstream` with default `NONE`. +- Represent manual control with a shared enum `NONE` and `FORCE_OFFLINE`. +- Keep `upstream_status` for automatic health only. +- Make Admin manual APIs write only `manualStatus`. +- Let heartbeat or recovery logic skip `status=true` updates when `manualStatus == FORCE_OFFLINE`. +- Include `manualStatus` in discovery sync payloads and Gateway cache objects. +- Filter `FORCE_OFFLINE` upstreams before load-balancer selection. + +This keeps automatic and manual state independent and avoids hidden coupling. + +## Alternatives Considered + +### Reuse `upstream_status` + +Rejected because heartbeat and health check would continue to overwrite manual operations. + +### Keep manual state only in Gateway memory + +Rejected because it would not survive restarts or sync across Admin and Gateway nodes. + +## Data Model + +Add `manual_status varchar(32) not null default 'NONE'` to `discovery_upstream`. + +Shared enum: + +- `NONE` +- `FORCE_OFFLINE` + +`/upstream/online` resets the field to `NONE`. + +## Admin API + +Add a new Admin controller rooted at `/upstream` with: + +- `POST /upstream/offline` +- `POST /upstream/online` + +Request body will identify the upstream by `selectorId` and `url`. + +Behavior: + +- Look up the related discovery handler by selector id. +- Update only `manual_status`. +- Publish a fresh `DISCOVER_UPSTREAM` event built from current DB data so gateways receive the new flag immediately. + +## Status Update Rules + +Automatic writers keep their current responsibility for `upstream_status`. + +Additional rule: + +- If a write intends to mark an upstream alive (`status=true`) and the record is `FORCE_OFFLINE`, skip the status update. + +This protects the manual offline decision from heartbeat recovery without blocking automatic offline transitions. + +## Sync Contract + +Extend `DiscoveryUpstreamData` and all transfer paths to include `manualStatus`. + +Admin event producers and Gateway sync consumers will continue to use the same payload shape, now with one extra field. + +## Gateway Behavior + +Extend cached upstream objects with `manualStatus`. + +Gateway will filter out `FORCE_OFFLINE` upstreams before selection. This ensures: + +- Manually offline nodes are never chosen. +- Existing health-check metadata can still be retained. +- Re-enabling an upstream only requires Admin to push a new sync event with `manualStatus=NONE`. + +## Testing Strategy + +- Admin service tests for manual status update and status recovery short-circuit. +- Sync/transfer tests for `manualStatus` propagation. +- Load-balancer tests for filtering `FORCE_OFFLINE`. +- Divide discovery handler test for mapping sync payload to cached upstream manual status. diff --git a/shenyu-admin/src/main/java/org/apache/shenyu/admin/controller/UpstreamController.java b/shenyu-admin/src/main/java/org/apache/shenyu/admin/controller/UpstreamController.java new file mode 100644 index 000000000000..2e3a6344b453 --- /dev/null +++ b/shenyu-admin/src/main/java/org/apache/shenyu/admin/controller/UpstreamController.java @@ -0,0 +1,71 @@ +/* + * 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.shenyu.admin.controller; + +import jakarta.validation.Valid; +import org.apache.shenyu.admin.aspect.annotation.RestApi; +import org.apache.shenyu.admin.model.dto.UpstreamManualStatusDTO; +import org.apache.shenyu.admin.model.result.ShenyuAdminResult; +import org.apache.shenyu.admin.service.DiscoveryUpstreamService; +import org.apache.shenyu.admin.utils.ShenyuResultMessage; +import org.apache.shenyu.common.enums.UpstreamManualStatusEnum; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; + +/** + * Upstream controller. + */ +@RestApi("/upstream") +public class UpstreamController { + + private final DiscoveryUpstreamService discoveryUpstreamService; + + public UpstreamController(final DiscoveryUpstreamService discoveryUpstreamService) { + this.discoveryUpstreamService = discoveryUpstreamService; + } + + /** + * manual offline. + * + * @param upstreamManualStatusDTO upstream request + * @return result + */ + @PostMapping("/offline") + public ShenyuAdminResult offline(@Valid @RequestBody final UpstreamManualStatusDTO upstreamManualStatusDTO) { + discoveryUpstreamService.changeManualStatusBySelectorIdAndUrl( + upstreamManualStatusDTO.getSelectorId(), + upstreamManualStatusDTO.getUrl(), + UpstreamManualStatusEnum.FORCE_OFFLINE); + return ShenyuAdminResult.success(ShenyuResultMessage.UPDATE_SUCCESS); + } + + /** + * manual online. + * + * @param upstreamManualStatusDTO upstream request + * @return result + */ + @PostMapping("/online") + public ShenyuAdminResult online(@Valid @RequestBody final UpstreamManualStatusDTO upstreamManualStatusDTO) { + discoveryUpstreamService.changeManualStatusBySelectorIdAndUrl( + upstreamManualStatusDTO.getSelectorId(), + upstreamManualStatusDTO.getUrl(), + UpstreamManualStatusEnum.NONE); + return ShenyuAdminResult.success(ShenyuResultMessage.UPDATE_SUCCESS); + } +} diff --git a/shenyu-admin/src/main/java/org/apache/shenyu/admin/mapper/DiscoveryUpstreamMapper.java b/shenyu-admin/src/main/java/org/apache/shenyu/admin/mapper/DiscoveryUpstreamMapper.java index ad049bcc7962..2f17f256c433 100644 --- a/shenyu-admin/src/main/java/org/apache/shenyu/admin/mapper/DiscoveryUpstreamMapper.java +++ b/shenyu-admin/src/main/java/org/apache/shenyu/admin/mapper/DiscoveryUpstreamMapper.java @@ -170,4 +170,15 @@ public interface DiscoveryUpstreamMapper extends ExistProvider { */ int updateStatusByUrl(@Param("discoveryHandlerId") String discoveryHandlerId, @Param("upstreamUrl") String upstreamUrl, @Param("upstreamStatus") int upstreamStatus); + /** + * update manual status by url. + * + * @param discoveryHandlerId discoveryHandlerId + * @param upstreamUrl upstreamUrl + * @param manualStatus manualStatus + * @return effect + */ + int updateManualStatusByUrl(@Param("discoveryHandlerId") String discoveryHandlerId, @Param("upstreamUrl") String upstreamUrl, + @Param("manualStatus") String manualStatus); + } diff --git a/shenyu-admin/src/main/java/org/apache/shenyu/admin/model/dto/DiscoveryUpstreamDTO.java b/shenyu-admin/src/main/java/org/apache/shenyu/admin/model/dto/DiscoveryUpstreamDTO.java index dcceeb0bf4cc..b817dd4a9e10 100644 --- a/shenyu-admin/src/main/java/org/apache/shenyu/admin/model/dto/DiscoveryUpstreamDTO.java +++ b/shenyu-admin/src/main/java/org/apache/shenyu/admin/model/dto/DiscoveryUpstreamDTO.java @@ -20,11 +20,13 @@ import org.apache.shenyu.admin.mapper.DiscoveryUpstreamMapper; import org.apache.shenyu.admin.mapper.NamespaceMapper; import org.apache.shenyu.admin.validation.annotation.Existed; +import org.apache.shenyu.common.enums.UpstreamManualStatusEnum; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import java.io.Serializable; import java.sql.Timestamp; +import java.util.Objects; /** * discovery upstream dto. @@ -92,6 +94,11 @@ public class DiscoveryUpstreamDTO implements Serializable { */ private Timestamp dateUpdated; + /** + * manual status. + */ + private String manualStatus; + /** * getId. * @@ -284,4 +291,22 @@ public String getNamespaceId() { public void setNamespaceId(final String namespaceId) { this.namespaceId = namespaceId; } + + /** + * get manualStatus. + * + * @return manualStatus + */ + public String getManualStatus() { + return manualStatus; + } + + /** + * set manualStatus. + * + * @param manualStatus manualStatus + */ + public void setManualStatus(final String manualStatus) { + this.manualStatus = Objects.isNull(manualStatus) ? null : UpstreamManualStatusEnum.normalize(manualStatus); + } } diff --git a/shenyu-admin/src/main/java/org/apache/shenyu/admin/model/dto/UpstreamManualStatusDTO.java b/shenyu-admin/src/main/java/org/apache/shenyu/admin/model/dto/UpstreamManualStatusDTO.java new file mode 100644 index 000000000000..8d1b3317b27d --- /dev/null +++ b/shenyu-admin/src/main/java/org/apache/shenyu/admin/model/dto/UpstreamManualStatusDTO.java @@ -0,0 +1,74 @@ +/* + * 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.shenyu.admin.model.dto; + +import jakarta.validation.constraints.NotBlank; + +/** + * Manual upstream status request. + */ +public class UpstreamManualStatusDTO { + + /** + * selector id. + */ + @NotBlank(message = "selectorId can't be null") + private String selectorId; + + /** + * upstream url. + */ + @NotBlank(message = "url can't be null") + private String url; + + /** + * get selectorId. + * + * @return selectorId + */ + public String getSelectorId() { + return selectorId; + } + + /** + * set selectorId. + * + * @param selectorId selectorId + */ + public void setSelectorId(final String selectorId) { + this.selectorId = selectorId; + } + + /** + * get url. + * + * @return url + */ + public String getUrl() { + return url; + } + + /** + * set url. + * + * @param url url + */ + public void setUrl(final String url) { + this.url = url; + } +} diff --git a/shenyu-admin/src/main/java/org/apache/shenyu/admin/model/entity/DiscoveryUpstreamDO.java b/shenyu-admin/src/main/java/org/apache/shenyu/admin/model/entity/DiscoveryUpstreamDO.java index 93f899b8ccf6..68060754d55f 100644 --- a/shenyu-admin/src/main/java/org/apache/shenyu/admin/model/entity/DiscoveryUpstreamDO.java +++ b/shenyu-admin/src/main/java/org/apache/shenyu/admin/model/entity/DiscoveryUpstreamDO.java @@ -18,6 +18,7 @@ package org.apache.shenyu.admin.model.entity; import org.apache.shenyu.admin.model.dto.DiscoveryUpstreamDTO; +import org.apache.shenyu.common.enums.UpstreamManualStatusEnum; import org.apache.shenyu.common.utils.UUIDUtils; import org.springframework.util.StringUtils; @@ -66,6 +67,11 @@ public class DiscoveryUpstreamDO extends BaseDO { */ private String namespaceId; + /** + * manualStatus. + */ + private String manualStatus = UpstreamManualStatusEnum.NONE.name(); + /** * DiscoveryUpstreamDO. */ @@ -242,6 +248,24 @@ public void setNamespaceId(final String namespaceId) { this.namespaceId = namespaceId; } + /** + * get manualStatus. + * + * @return manualStatus + */ + public String getManualStatus() { + return manualStatus; + } + + /** + * set manualStatus. + * + * @param manualStatus manualStatus + */ + public void setManualStatus(final String manualStatus) { + this.manualStatus = UpstreamManualStatusEnum.normalize(manualStatus); + } + /** * buildDiscoveryUpstreamDO. * @@ -259,6 +283,7 @@ public static DiscoveryUpstreamDO buildDiscoveryUpstreamDO(final DiscoveryUpstre .weight(item.getWeight()) .props(item.getProps()) .url(item.getUrl()) + .manualStatus(item.getManualStatus()) .namespaceId(item.getNamespaceId()) .dateCreated(currentTime) .dateUpdated(currentTime).build(); @@ -327,6 +352,11 @@ public static final class DiscoveryUpstreamBuilder { */ private String namespaceId; + /** + * manualStatus. + */ + private String manualStatus = UpstreamManualStatusEnum.NONE.name(); + /** * id. * @@ -446,6 +476,17 @@ public DiscoveryUpstreamBuilder namespaceId(final String namespaceId) { return this; } + /** + * build manualStatus. + * + * @param manualStatus manualStatus + * @return this + */ + public DiscoveryUpstreamBuilder manualStatus(final String manualStatus) { + this.manualStatus = UpstreamManualStatusEnum.normalize(manualStatus); + return this; + } + /** * build. * @@ -462,6 +503,7 @@ public DiscoveryUpstreamDO build() { discoveryUpstreamDO.setWeight(this.weight); discoveryUpstreamDO.setProps(this.props); discoveryUpstreamDO.setNamespaceId(this.namespaceId); + discoveryUpstreamDO.setManualStatus(this.manualStatus); discoveryUpstreamDO.setDateCreated(this.dateCreated); discoveryUpstreamDO.setDateUpdated(this.dateUpdated); return discoveryUpstreamDO; diff --git a/shenyu-admin/src/main/java/org/apache/shenyu/admin/model/vo/DiscoveryUpstreamVO.java b/shenyu-admin/src/main/java/org/apache/shenyu/admin/model/vo/DiscoveryUpstreamVO.java index a6bfcd42f26c..6c888f600c86 100644 --- a/shenyu-admin/src/main/java/org/apache/shenyu/admin/model/vo/DiscoveryUpstreamVO.java +++ b/shenyu-admin/src/main/java/org/apache/shenyu/admin/model/vo/DiscoveryUpstreamVO.java @@ -59,6 +59,11 @@ public class DiscoveryUpstreamVO { */ private String startupTime; + /** + * manual status. + */ + private String manualStatus; + /** * getId. * @@ -202,4 +207,22 @@ public String getStartupTime() { public void setStartupTime(final String startupTime) { this.startupTime = startupTime; } + + /** + * get manualStatus. + * + * @return manualStatus + */ + public String getManualStatus() { + return manualStatus; + } + + /** + * set manualStatus. + * + * @param manualStatus manualStatus + */ + public void setManualStatus(final String manualStatus) { + this.manualStatus = manualStatus; + } } diff --git a/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/DiscoveryUpstreamService.java b/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/DiscoveryUpstreamService.java index d808c194b6d5..55d2e836bc2b 100644 --- a/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/DiscoveryUpstreamService.java +++ b/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/DiscoveryUpstreamService.java @@ -23,6 +23,7 @@ import org.apache.shenyu.admin.service.configs.ConfigsImportContext; import org.apache.shenyu.common.dto.DiscoverySyncData; import org.apache.shenyu.common.dto.DiscoveryUpstreamData; +import org.apache.shenyu.common.enums.UpstreamManualStatusEnum; import java.util.List; @@ -118,6 +119,15 @@ public interface DiscoveryUpstreamService { */ void changeStatusBySelectorIdAndUrl(String selectorId, String url, Boolean enabled); + /** + * changeManualStatusBySelectorIdAndUrl. + * + * @param selectorId selectorId + * @param url url + * @param manualStatus manualStatus + */ + void changeManualStatusBySelectorIdAndUrl(String selectorId, String url, UpstreamManualStatusEnum manualStatus); + /** * Import the discoveryUpstream data list. * diff --git a/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/DiscoveryUpstreamServiceImpl.java b/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/DiscoveryUpstreamServiceImpl.java index b626f314fa31..ad4fa015b99a 100644 --- a/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/DiscoveryUpstreamServiceImpl.java +++ b/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/DiscoveryUpstreamServiceImpl.java @@ -19,6 +19,7 @@ import com.google.common.collect.Lists; import org.apache.commons.collections4.CollectionUtils; +import org.apache.shenyu.admin.listener.DataChangedEvent; import org.apache.shenyu.admin.discovery.DiscoveryProcessor; import org.apache.shenyu.admin.discovery.DiscoveryProcessorHolder; import org.apache.shenyu.admin.mapper.DiscoveryHandlerMapper; @@ -44,6 +45,10 @@ import org.apache.shenyu.admin.utils.ShenyuResultMessage; import org.apache.shenyu.common.dto.DiscoverySyncData; import org.apache.shenyu.common.dto.DiscoveryUpstreamData; +import org.apache.shenyu.common.enums.ConfigGroupEnum; +import org.apache.shenyu.common.enums.DataEventTypeEnum; +import org.apache.shenyu.common.enums.UpstreamManualStatusEnum; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.StringUtils; @@ -74,6 +79,8 @@ public class DiscoveryUpstreamServiceImpl implements DiscoveryUpstreamService { private final DiscoveryProcessorHolder discoveryProcessorHolder; + private final ApplicationEventPublisher eventPublisher; + public DiscoveryUpstreamServiceImpl(final DiscoveryUpstreamMapper discoveryUpstreamMapper, final DiscoveryHandlerMapper discoveryHandlerMapper, final ProxySelectorMapper proxySelectorMapper, @@ -81,7 +88,8 @@ public DiscoveryUpstreamServiceImpl(final DiscoveryUpstreamMapper discoveryUpstr final DiscoveryRelMapper discoveryRelMapper, final SelectorMapper selectorMapper, final PluginMapper pluginMapper, - final DiscoveryProcessorHolder discoveryProcessorHolder) { + final DiscoveryProcessorHolder discoveryProcessorHolder, + final ApplicationEventPublisher eventPublisher) { this.discoveryUpstreamMapper = discoveryUpstreamMapper; this.discoveryProcessorHolder = discoveryProcessorHolder; this.discoveryHandlerMapper = discoveryHandlerMapper; @@ -90,6 +98,7 @@ public DiscoveryUpstreamServiceImpl(final DiscoveryUpstreamMapper discoveryUpstr this.selectorMapper = selectorMapper; this.proxySelectorMapper = proxySelectorMapper; this.pluginMapper = pluginMapper; + this.eventPublisher = eventPublisher; } /** @@ -123,6 +132,13 @@ public int updateBatch(final String discoveryHandlerId, final List discoveryUpstreamDO.setManualStatus(existing.getManualStatus())); + } discoveryUpstreamMapper.update(discoveryUpstreamDO); fetchAll(discoveryUpstreamDTO.getDiscoveryHandlerId()); return ShenyuResultMessage.UPDATE_SUCCESS; @@ -246,10 +267,27 @@ public void deleteBySelectorIdAndUrl(final String selectorId, final String url) public void changeStatusBySelectorIdAndUrl(final String selectorId, final String url, final Boolean enabled) { DiscoveryHandlerDO discoveryHandlerDO = discoveryHandlerMapper.selectBySelectorId(selectorId); if (Objects.nonNull(discoveryHandlerDO)) { + if (Boolean.TRUE.equals(enabled)) { + DiscoveryUpstreamDO existingRecord = discoveryUpstreamMapper.selectByDiscoveryHandlerIdAndUrl(discoveryHandlerDO.getId(), url); + if (Objects.nonNull(existingRecord) && UpstreamManualStatusEnum.isForceOffline(existingRecord.getManualStatus())) { + return; + } + } discoveryUpstreamMapper.updateStatusByUrl(discoveryHandlerDO.getId(), url, enabled ? 0 : 1); } } + @Override + @Transactional(rollbackFor = Exception.class) + public void changeManualStatusBySelectorIdAndUrl(final String selectorId, final String url, final UpstreamManualStatusEnum manualStatus) { + DiscoveryHandlerDO discoveryHandlerDO = discoveryHandlerMapper.selectBySelectorId(selectorId); + if (Objects.isNull(discoveryHandlerDO)) { + return; + } + discoveryUpstreamMapper.updateManualStatusByUrl(discoveryHandlerDO.getId(), url, manualStatus.name()); + publishDiscoverySyncEvent(selectorId, discoveryHandlerDO.getId()); + } + @Override @Transactional(rollbackFor = Exception.class) public ConfigImportResult importData(final List discoveryUpstreamList) { @@ -351,4 +389,24 @@ private void fetchAll(final String discoveryHandlerId) { discoveryProcessor.changeUpstream(proxySelectorDTO, collect); } + private void publishDiscoverySyncEvent(final String selectorId, final String discoveryHandlerId) { + DiscoveryRelDO discoveryRelDO = discoveryRelMapper.selectByDiscoveryHandlerId(discoveryHandlerId); + SelectorDO selectorDO = selectorMapper.selectById(selectorId); + if (Objects.isNull(discoveryRelDO) || Objects.isNull(selectorDO)) { + return; + } + DiscoverySyncData discoverySyncData = new DiscoverySyncData(); + discoverySyncData.setSelectorId(selectorId); + discoverySyncData.setSelectorName(selectorDO.getSelectorName()); + discoverySyncData.setPluginName(discoveryRelDO.getPluginName()); + discoverySyncData.setNamespaceId(selectorDO.getNamespaceId()); + discoverySyncData.setDiscoveryHandlerId(discoveryHandlerId); + List discoveryUpstreamDataList = discoveryUpstreamMapper.selectByDiscoveryHandlerId(discoveryHandlerId).stream() + .map(DiscoveryTransfer.INSTANCE::mapToData) + .collect(Collectors.toList()); + discoverySyncData.setUpstreamDataList(discoveryUpstreamDataList); + eventPublisher.publishEvent(new DataChangedEvent(ConfigGroupEnum.DISCOVER_UPSTREAM, + DataEventTypeEnum.UPDATE, Collections.singletonList(discoverySyncData))); + } + } diff --git a/shenyu-admin/src/main/java/org/apache/shenyu/admin/transfer/DiscoveryTransfer.java b/shenyu-admin/src/main/java/org/apache/shenyu/admin/transfer/DiscoveryTransfer.java index 91f854cd5ff1..f233d8b85b1f 100644 --- a/shenyu-admin/src/main/java/org/apache/shenyu/admin/transfer/DiscoveryTransfer.java +++ b/shenyu-admin/src/main/java/org/apache/shenyu/admin/transfer/DiscoveryTransfer.java @@ -35,6 +35,7 @@ import org.apache.shenyu.common.dto.DiscoveryUpstreamData; import org.apache.shenyu.common.dto.ProxySelectorData; import org.apache.shenyu.common.dto.convert.selector.CommonUpstream; +import org.apache.shenyu.common.enums.UpstreamManualStatusEnum; import org.apache.shenyu.common.utils.GsonUtils; import java.util.Optional; @@ -64,6 +65,7 @@ public DiscoveryUpstreamDO mapToDo(DiscoveryUpstreamData discoveryUpstreamData) .status(data.getStatus()) .weight(data.getWeight()) .props(data.getProps()) + .manualStatus(data.getManualStatus()) .url(data.getUrl()) .dateUpdated(data.getDateUpdated()) .dateCreated(data.getDateCreated()).build()).orElse(null); @@ -85,6 +87,8 @@ public CommonUpstream mapToCommonUpstream(DiscoveryUpstreamData discoveryUpstrea .orElse(new Properties()); commonUpstream .setHealthCheckEnabled(Boolean.parseBoolean(properties.getProperty("healthCheckEnabled", "true"))); + commonUpstream.setNamespaceId(data.getNamespaceId()); + commonUpstream.setManualStatus(data.getManualStatus()); return commonUpstream; }).orElse(null); } @@ -106,6 +110,7 @@ public DiscoveryUpstreamVO mapToVo(DiscoveryUpstreamDO discoveryUpstreamDO) { vo.setWeight(data.getWeight()); vo.setProps(data.getProps()); vo.setStartupTime(String.valueOf(data.getDateCreated().getTime())); + vo.setManualStatus(data.getManualStatus()); return vo; }).orElse(null); } @@ -193,6 +198,8 @@ public DiscoveryUpstreamData mapToData(DiscoveryUpstreamDO discoveryUpstreamDO) discoveryUpstreamData.setProps(data.getProps()); discoveryUpstreamData.setDateUpdated(data.getDateUpdated()); discoveryUpstreamData.setDateCreated(data.getDateCreated()); + discoveryUpstreamData.setNamespaceId(data.getNamespaceId()); + discoveryUpstreamData.setManualStatus(data.getManualStatus()); return discoveryUpstreamData; }).orElse(null); } @@ -216,6 +223,7 @@ public DiscoveryUpstreamData mapToData(DiscoveryUpstreamDTO discoveryUpstreamDTO discoveryUpstreamData.setNamespaceId(data.getNamespaceId()); discoveryUpstreamData.setDateCreated(data.getDateCreated()); discoveryUpstreamData.setDateUpdated(data.getDateUpdated()); + discoveryUpstreamData.setManualStatus(data.getManualStatus()); return discoveryUpstreamData; }).orElse(null); } @@ -337,6 +345,8 @@ public DiscoveryUpstreamDTO mapToDTO(DiscoveryUpstreamDO discoveryUpstreamDO) { discoveryUpstreamDTO.setWeight(data.getWeight()); discoveryUpstreamDTO.setDateCreated(data.getDateCreated()); discoveryUpstreamDTO.setDateUpdated(data.getDateUpdated()); + discoveryUpstreamDTO.setNamespaceId(data.getNamespaceId()); + discoveryUpstreamDTO.setManualStatus(data.getManualStatus()); return discoveryUpstreamDTO; }).orElse(null); } @@ -372,6 +382,7 @@ public DiscoveryUpstreamData mapToDiscoveryUpstreamData(CommonUpstream commonUps .orElse(new Properties()); properties.setProperty("healthCheckEnabled", String.valueOf(commonUpstream.isHealthCheckEnabled())); discoveryUpstreamDTO.setProps(GsonUtils.getInstance().toJson(properties)); + discoveryUpstreamDTO.setManualStatus(UpstreamManualStatusEnum.normalize(commonUpstream.getManualStatus())); return mapToData(discoveryUpstreamDTO); } } diff --git a/shenyu-admin/src/main/java/org/apache/shenyu/admin/utils/CommonUpstreamUtils.java b/shenyu-admin/src/main/java/org/apache/shenyu/admin/utils/CommonUpstreamUtils.java index 6a6cdc5c2e26..2f91947ccef2 100644 --- a/shenyu-admin/src/main/java/org/apache/shenyu/admin/utils/CommonUpstreamUtils.java +++ b/shenyu-admin/src/main/java/org/apache/shenyu/admin/utils/CommonUpstreamUtils.java @@ -251,6 +251,8 @@ public static List convertCommonUpstreamList(final List + @@ -40,6 +41,7 @@ protocol, upstream_url, upstream_status, + manual_status, weight, props @@ -58,6 +60,7 @@ protocol, upstream_url, upstream_status, + manual_status, weight, props, date_created, @@ -69,6 +72,7 @@ #{protocol,jdbcType=VARCHAR}, #{upstreamUrl,jdbcType=VARCHAR}, #{upstreamStatus,jdbcType=INTEGER}, + #{manualStatus,jdbcType=VARCHAR}, #{weight,jdbcType=INTEGER}, #{props,jdbcType=VARCHAR}, #{dateCreated, jdbcType=TIMESTAMP}, @@ -82,6 +86,7 @@ protocol=#{protocol,jdbcType=VARCHAR}, upstream_url=#{upstreamUrl,jdbcType=VARCHAR}, upstream_status=#{upstreamStatus,jdbcType=INTEGER}, + manual_status=#{manualStatus,jdbcType=VARCHAR}, weight=#{weight,jdbcType=INTEGER}, props=#{props,jdbcType=VARCHAR}, date_updated=#{dateUpdated, jdbcType=TIMESTAMP} @@ -103,6 +108,9 @@ upstream_status=#{upstreamStatus,jdbcType=INTEGER}, + + manual_status=#{manualStatus,jdbcType=VARCHAR}, + weight=#{weight,jdbcType=INTEGER}, @@ -120,6 +128,7 @@ SET protocol=#{protocol,jdbcType=VARCHAR}, upstream_status=#{upstreamStatus,jdbcType=INTEGER}, + manual_status=#{manualStatus,jdbcType=VARCHAR}, weight=#{weight,jdbcType=INTEGER}, props=#{props,jdbcType=VARCHAR}, date_updated=#{dateUpdated, jdbcType=TIMESTAMP} @@ -176,9 +185,11 @@ du.date_created, du.date_updated, du.discovery_handler_id, + du.namespace_id, du.protocol, du.upstream_url, du.upstream_status, + du.manual_status, du.weight, du.props FROM discovery_upstream du @@ -186,7 +197,7 @@ @@ -205,6 +216,7 @@ protocol, upstream_url, upstream_status, + manual_status, weight, props, date_created, @@ -217,6 +229,7 @@ #{item.protocol,jdbcType=VARCHAR}, #{item.upstreamUrl,jdbcType=VARCHAR}, #{item.upstreamStatus,jdbcType=INTEGER}, + #{item.manualStatus,jdbcType=VARCHAR}, #{item.weight,jdbcType=INTEGER}, #{item.props,jdbcType=VARCHAR}, #{item.dateCreated, jdbcType=TIMESTAMP}, @@ -239,4 +252,10 @@ WHERE discovery_handler_id = #{discoveryHandlerId} and upstream_url = #{upstreamUrl} + + UPDATE discovery_upstream + SET manual_status = #{manualStatus} + WHERE discovery_handler_id = #{discoveryHandlerId} and upstream_url = #{upstreamUrl} + + 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 16dd2dd278a1..8dfe5dd2c87f 100644 --- a/shenyu-admin/src/main/resources/sql-script/h2/schema.sql +++ b/shenyu-admin/src/main/resources/sql-script/h2/schema.sql @@ -1281,6 +1281,7 @@ CREATE TABLE IF NOT EXISTS `discovery_upstream` `protocol` varchar(64) COMMENT 'for http, https, tcp, ws', `upstream_url` varchar(64) NOT NULL COMMENT 'ip:port', `upstream_status` int(0) NOT NULL COMMENT 'type (0, healthy, 1 unhealthy)', + `manual_status` varchar(32) NOT NULL DEFAULT 'NONE' COMMENT 'manual status (NONE, FORCE_OFFLINE)', `weight` int(0) NOT NULL COMMENT 'the weight for lists', `props` text COMMENT 'the other field (json)', `date_created` timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT 'create time', diff --git a/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/DiscoveryUpstreamServiceTest.java b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/DiscoveryUpstreamServiceTest.java index 28adf2c60940..6562af2b562e 100644 --- a/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/DiscoveryUpstreamServiceTest.java +++ b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/DiscoveryUpstreamServiceTest.java @@ -17,6 +17,7 @@ package org.apache.shenyu.admin.service; +import org.apache.shenyu.admin.listener.DataChangedEvent; import org.apache.shenyu.admin.discovery.DiscoveryProcessor; import org.apache.shenyu.admin.discovery.DiscoveryProcessorHolder; import org.apache.shenyu.admin.mapper.DiscoveryHandlerMapper; @@ -39,6 +40,7 @@ import org.apache.shenyu.admin.service.impl.DiscoveryUpstreamServiceImpl; import org.apache.shenyu.admin.utils.ShenyuResultMessage; import org.apache.shenyu.common.dto.DiscoverySyncData; +import org.apache.shenyu.common.enums.UpstreamManualStatusEnum; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -46,6 +48,7 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.context.ApplicationEventPublisher; import java.sql.Timestamp; import java.time.LocalDateTime; @@ -54,9 +57,12 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; /** @@ -95,6 +101,9 @@ public final class DiscoveryUpstreamServiceTest { @Mock private DiscoveryProcessor discoveryProcessor; + @Mock + private ApplicationEventPublisher eventPublisher; + @BeforeEach public void setUp() { @@ -105,7 +114,8 @@ public void setUp() { discoveryRelMapper, selectorMapper, pluginMapper, - discoveryProcessorHolder + discoveryProcessorHolder, + eventPublisher ); } @@ -142,8 +152,11 @@ public void testListAll() { when(discoveryHandlerMapper.selectAll()).thenReturn(list); when(discoveryRelMapper.selectByDiscoveryHandlerId(any())).thenReturn(buildDiscoveryRelDO()); when(proxySelectorMapper.selectById(any())).thenReturn(buildProxySelectorDO()); + when(discoveryUpstreamMapper.selectByDiscoveryHandlerId(any())).thenReturn( + Collections.singletonList(buildDiscoveryUpstreamDO("", "123", "url1", UpstreamManualStatusEnum.FORCE_OFFLINE))); List dataList = discoveryUpstreamService.listAll(); assertEquals(dataList.size(), list.size()); + assertEquals(UpstreamManualStatusEnum.FORCE_OFFLINE.name(), dataList.get(0).getUpstreamDataList().get(0).getManualStatus()); } @Test @@ -184,6 +197,31 @@ public void testUpdateBatch() { discoveryUpstreamService.updateBatch("123", Collections.singletonList(buildDiscoveryUpstreamDTO(""))); } + @Test + public void testChangeManualStatusBySelectorIdAndUrl() { + when(discoveryHandlerMapper.selectBySelectorId("selector_1")).thenReturn(buildDiscoveryHandlerDO()); + when(selectorMapper.selectById("selector_1")).thenReturn(buildSelectorDO()); + when(discoveryRelMapper.selectByDiscoveryHandlerId("123")).thenReturn(buildDiscoveryRelDOWithSelector()); + when(discoveryUpstreamMapper.selectByDiscoveryHandlerId("123")).thenReturn( + Collections.singletonList(buildDiscoveryUpstreamDO("", "123", "url1", UpstreamManualStatusEnum.FORCE_OFFLINE))); + + discoveryUpstreamService.changeManualStatusBySelectorIdAndUrl("selector_1", "url1", UpstreamManualStatusEnum.FORCE_OFFLINE); + + verify(discoveryUpstreamMapper).updateManualStatusByUrl("123", "url1", UpstreamManualStatusEnum.FORCE_OFFLINE.name()); + verify(eventPublisher).publishEvent(any(DataChangedEvent.class)); + } + + @Test + public void testChangeStatusBySelectorIdAndUrlShouldSkipAliveUpdateWhenForceOffline() { + when(discoveryHandlerMapper.selectBySelectorId("selector_1")).thenReturn(buildDiscoveryHandlerDO()); + when(discoveryUpstreamMapper.selectByDiscoveryHandlerIdAndUrl("123", "url1")) + .thenReturn(buildDiscoveryUpstreamDO("", "123", "url1", UpstreamManualStatusEnum.FORCE_OFFLINE)); + + discoveryUpstreamService.changeStatusBySelectorIdAndUrl("selector_1", "url1", Boolean.TRUE); + + verify(discoveryUpstreamMapper, never()).updateStatusByUrl(anyString(), anyString(), anyInt()); + } + private void testUpdate() { when(discoveryUpstreamMapper.update(any())).thenReturn(1); DiscoveryUpstreamDTO discoveryUpstreamDTO = buildDiscoveryUpstreamDTO("123"); @@ -233,12 +271,18 @@ private DiscoveryUpstreamDO buildDiscoveryUpstreamDO(final String id) { } private DiscoveryUpstreamDO buildDiscoveryUpstreamDO(final String id, final String discoveryHandlerId, final String url) { + return buildDiscoveryUpstreamDO(id, discoveryHandlerId, url, UpstreamManualStatusEnum.NONE); + } + + private DiscoveryUpstreamDO buildDiscoveryUpstreamDO(final String id, final String discoveryHandlerId, final String url, + final UpstreamManualStatusEnum manualStatus) { DiscoveryUpstreamDO discoveryUpstreamDO = new DiscoveryUpstreamDO(); discoveryUpstreamDO.setId(id); discoveryUpstreamDO.setUpstreamStatus(1); discoveryUpstreamDO.setWeight(50); discoveryUpstreamDO.setUpstreamUrl(url); discoveryUpstreamDO.setDiscoveryHandlerId(discoveryHandlerId); + discoveryUpstreamDO.setManualStatus(manualStatus.name()); Timestamp now = Timestamp.valueOf(LocalDateTime.now()); discoveryUpstreamDO.setDateCreated(now); discoveryUpstreamDO.setDateUpdated(now); @@ -276,6 +320,7 @@ private SelectorDO buildSelectorDO() { SelectorDO selectorDO = new SelectorDO(); selectorDO.setId("selector_1"); selectorDO.setSelectorName("selector_1"); + selectorDO.setNamespaceId("test"); return selectorDO; } @@ -293,4 +338,10 @@ private DiscoveryRelDO buildDiscoveryRelDO() { return discoveryRelDO; } + private DiscoveryRelDO buildDiscoveryRelDOWithSelector() { + DiscoveryRelDO discoveryRelDO = buildDiscoveryRelDO(); + discoveryRelDO.setSelectorId("selector_1"); + return discoveryRelDO; + } + } diff --git a/shenyu-common/src/main/java/org/apache/shenyu/common/dto/DiscoveryUpstreamData.java b/shenyu-common/src/main/java/org/apache/shenyu/common/dto/DiscoveryUpstreamData.java index 20fcae9946a8..0567a0ae41d3 100644 --- a/shenyu-common/src/main/java/org/apache/shenyu/common/dto/DiscoveryUpstreamData.java +++ b/shenyu-common/src/main/java/org/apache/shenyu/common/dto/DiscoveryUpstreamData.java @@ -18,6 +18,7 @@ package org.apache.shenyu.common.dto; import com.fasterxml.jackson.annotation.JsonFormat; +import org.apache.shenyu.common.enums.UpstreamManualStatusEnum; import java.sql.Timestamp; import java.util.Objects; @@ -77,6 +78,11 @@ public class DiscoveryUpstreamData { */ private String namespaceId; + /** + * manualStatus. + */ + private String manualStatus = UpstreamManualStatusEnum.NONE.name(); + /** * getDiscoveryHandlerId. @@ -258,6 +264,24 @@ public void setNamespaceId(final String namespaceId) { this.namespaceId = namespaceId; } + /** + * get manualStatus. + * + * @return manualStatus + */ + public String getManualStatus() { + return manualStatus; + } + + /** + * set manualStatus. + * + * @param manualStatus manualStatus + */ + public void setManualStatus(final String manualStatus) { + this.manualStatus = UpstreamManualStatusEnum.normalize(manualStatus); + } + @Override public boolean equals(final Object o) { if (this == o) { @@ -271,12 +295,13 @@ public boolean equals(final Object o) { && Objects.equals(dateCreated, that.dateCreated) && Objects.equals(dateUpdated, that.dateUpdated) && Objects.equals(discoveryHandlerId, that.discoveryHandlerId) && Objects.equals(protocol, that.protocol) && Objects.equals(url, that.url) && Objects.equals(props, that.props) - && Objects.equals(namespaceId, that.namespaceId); + && Objects.equals(namespaceId, that.namespaceId) + && Objects.equals(manualStatus, that.manualStatus); } @Override public int hashCode() { - return Objects.hash(id, dateCreated, dateUpdated, discoveryHandlerId, protocol, url, status, weight, props, namespaceId); + return Objects.hash(id, dateCreated, dateUpdated, discoveryHandlerId, protocol, url, status, weight, props, namespaceId, manualStatus); } /** @@ -310,6 +335,8 @@ public static final class Builder { private String namespaceId; + private String manualStatus = UpstreamManualStatusEnum.NONE.name(); + private Builder() { } @@ -432,6 +459,17 @@ public Builder namespaceId(final String namespaceId) { return this; } + /** + * build manualStatus. + * + * @param manualStatus manualStatus + * @return this + */ + public Builder manualStatus(final String manualStatus) { + this.manualStatus = UpstreamManualStatusEnum.normalize(manualStatus); + return this; + } + /** * build new Object. * @@ -449,6 +487,7 @@ public DiscoveryUpstreamData build() { discoveryUpstreamData.setWeight(weight); discoveryUpstreamData.setProps(props); discoveryUpstreamData.setNamespaceId(namespaceId); + discoveryUpstreamData.setManualStatus(manualStatus); return discoveryUpstreamData; } } diff --git a/shenyu-common/src/main/java/org/apache/shenyu/common/dto/convert/selector/CommonUpstream.java b/shenyu-common/src/main/java/org/apache/shenyu/common/dto/convert/selector/CommonUpstream.java index 13f10e7b1fe9..f81476b1ffac 100644 --- a/shenyu-common/src/main/java/org/apache/shenyu/common/dto/convert/selector/CommonUpstream.java +++ b/shenyu-common/src/main/java/org/apache/shenyu/common/dto/convert/selector/CommonUpstream.java @@ -17,6 +17,8 @@ package org.apache.shenyu.common.dto.convert.selector; +import org.apache.shenyu.common.enums.UpstreamManualStatusEnum; + import java.util.Objects; /** @@ -64,6 +66,11 @@ public class CommonUpstream { */ private boolean healthCheckEnabled = true; + /** + * manualStatus. + */ + private String manualStatus = UpstreamManualStatusEnum.NONE.name(); + /** * Instantiates a new Common upstream. */ @@ -214,6 +221,33 @@ public void setGray(final boolean gray) { this.gray = gray; } + /** + * get manualStatus. + * + * @return manualStatus + */ + public String getManualStatus() { + return manualStatus; + } + + /** + * set manualStatus. + * + * @param manualStatus manualStatus + */ + public void setManualStatus(final String manualStatus) { + this.manualStatus = UpstreamManualStatusEnum.normalize(manualStatus); + } + + /** + * whether manual offline. + * + * @return true if manual offline + */ + public boolean isManualOffline() { + return UpstreamManualStatusEnum.isForceOffline(manualStatus); + } + /** * get healthCheckEnabled. * diff --git a/shenyu-common/src/main/java/org/apache/shenyu/common/dto/convert/selector/GrpcUpstream.java b/shenyu-common/src/main/java/org/apache/shenyu/common/dto/convert/selector/GrpcUpstream.java index 8379e719e8b8..f35cf4fa3967 100644 --- a/shenyu-common/src/main/java/org/apache/shenyu/common/dto/convert/selector/GrpcUpstream.java +++ b/shenyu-common/src/main/java/org/apache/shenyu/common/dto/convert/selector/GrpcUpstream.java @@ -47,6 +47,7 @@ private GrpcUpstream(final Builder builder) { setTimestamp(builder.timestamp); setNamespaceId(builder.namespaceId); setHealthCheckEnabled(builder.healthCheckEnabled); + setManualStatus(builder.manualStatus); } /** @@ -169,6 +170,11 @@ public static final class Builder { */ private boolean healthCheckEnabled = true; + /** + * manual status. + */ + private String manualStatus; + /** * no args constructor. */ @@ -272,5 +278,16 @@ public Builder healthCheckEnabled(final boolean healthCheckEnabled) { this.healthCheckEnabled = healthCheckEnabled; return this; } + + /** + * build manualStatus. + * + * @param manualStatus manualStatus + * @return this + */ + public Builder manualStatus(final String manualStatus) { + this.manualStatus = manualStatus; + return this; + } } } diff --git a/shenyu-common/src/main/java/org/apache/shenyu/common/enums/UpstreamManualStatusEnum.java b/shenyu-common/src/main/java/org/apache/shenyu/common/enums/UpstreamManualStatusEnum.java new file mode 100644 index 000000000000..073c4caebb20 --- /dev/null +++ b/shenyu-common/src/main/java/org/apache/shenyu/common/enums/UpstreamManualStatusEnum.java @@ -0,0 +1,60 @@ +/* + * 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.shenyu.common.enums; + +import java.util.Arrays; + +/** + * Manual upstream status. + */ +public enum UpstreamManualStatusEnum { + + /** + * No manual override. + */ + NONE, + + /** + * Force upstream offline manually. + */ + FORCE_OFFLINE; + + /** + * Normalize status value. + * + * @param manualStatus status value + * @return normalized enum name + */ + public static String normalize(final String manualStatus) { + return Arrays.stream(values()) + .filter(value -> value.name().equalsIgnoreCase(manualStatus)) + .findFirst() + .orElse(NONE) + .name(); + } + + /** + * Whether force offline. + * + * @param manualStatus status value + * @return true if force offline + */ + public static boolean isForceOffline(final String manualStatus) { + return FORCE_OFFLINE.name().equalsIgnoreCase(normalize(manualStatus)); + } +} diff --git a/shenyu-loadbalancer/src/main/java/org/apache/shenyu/loadbalancer/entity/Upstream.java b/shenyu-loadbalancer/src/main/java/org/apache/shenyu/loadbalancer/entity/Upstream.java index d832dc28ff00..3363c574d16b 100644 --- a/shenyu-loadbalancer/src/main/java/org/apache/shenyu/loadbalancer/entity/Upstream.java +++ b/shenyu-loadbalancer/src/main/java/org/apache/shenyu/loadbalancer/entity/Upstream.java @@ -18,6 +18,7 @@ package org.apache.shenyu.loadbalancer.entity; import org.apache.commons.lang3.StringUtils; +import org.apache.shenyu.common.enums.UpstreamManualStatusEnum; import java.util.Map; import java.util.Objects; @@ -109,6 +110,11 @@ public final class Upstream { * health check enabled. */ private boolean healthCheckEnabled = true; + + /** + * manualStatus. + */ + private String manualStatus = UpstreamManualStatusEnum.NONE.name(); private Map metadata = new ConcurrentHashMap<>(); @@ -133,6 +139,7 @@ private Upstream(final Builder builder) { this.version = builder.version; this.gray = builder.gray; this.healthCheckEnabled = builder.healthCheckEnabled; + this.manualStatus = UpstreamManualStatusEnum.normalize(builder.manualStatus); } /** @@ -251,6 +258,33 @@ public boolean isHealthCheckEnabled() { public void setHealthCheckEnabled(final boolean healthCheckEnabled) { this.healthCheckEnabled = healthCheckEnabled; } + + /** + * Gets manual status. + * + * @return manual status + */ + public String getManualStatus() { + return manualStatus; + } + + /** + * Sets manual status. + * + * @param manualStatus manual status + */ + public void setManualStatus(final String manualStatus) { + this.manualStatus = UpstreamManualStatusEnum.normalize(manualStatus); + } + + /** + * Is manual offline. + * + * @return true when manual offline + */ + public boolean isManualOffline() { + return UpstreamManualStatusEnum.isForceOffline(manualStatus); + } /** * Gets last health timestamp. @@ -519,6 +553,7 @@ public String toString() { + ", url='" + url + ", weight=" + weight + ", status=" + status + + ", manualStatus='" + manualStatus + '\'' + ", timestamp=" + timestamp + ", warmup=" + warmup + ", group='" + group @@ -581,6 +616,11 @@ public static final class Builder { */ private boolean healthCheckEnabled = true; + /** + * manual status. + */ + private String manualStatus; + /** * no args constructor. */ @@ -707,5 +747,16 @@ public Builder healthCheckEnabled(final boolean healthCheckEnabled) { return this; } + /** + * build manualStatus. + * + * @param manualStatus manualStatus + * @return this builder + */ + public Builder manualStatus(final String manualStatus) { + this.manualStatus = manualStatus; + return this; + } + } } diff --git a/shenyu-loadbalancer/src/main/java/org/apache/shenyu/loadbalancer/factory/LoadBalancerFactory.java b/shenyu-loadbalancer/src/main/java/org/apache/shenyu/loadbalancer/factory/LoadBalancerFactory.java index 291039da43ea..a8702cc7a20b 100644 --- a/shenyu-loadbalancer/src/main/java/org/apache/shenyu/loadbalancer/factory/LoadBalancerFactory.java +++ b/shenyu-loadbalancer/src/main/java/org/apache/shenyu/loadbalancer/factory/LoadBalancerFactory.java @@ -23,6 +23,7 @@ import org.apache.shenyu.spi.ExtensionLoader; import java.util.List; +import java.util.Objects; /** * The type Load balance Factory. @@ -41,7 +42,17 @@ private LoadBalancerFactory() { * @return the upstream */ public static Upstream selector(final List upstreamList, final String algorithm, final LoadBalanceData data) { + if (Objects.isNull(upstreamList)) { + return null; + } + List availableUpstreamList = upstreamList.stream() + .filter(Objects::nonNull) + .filter(upstream -> !upstream.isManualOffline()) + .toList(); + if (availableUpstreamList.isEmpty()) { + return null; + } LoadBalancer loadBalance = ExtensionLoader.getExtensionLoader(LoadBalancer.class).getJoin(algorithm); - return loadBalance.select(upstreamList, data); + return loadBalance.select(availableUpstreamList, data); } } diff --git a/shenyu-loadbalancer/src/test/java/org/apache/shenyu/loadbalancer/factory/LoadBalancerFactoryTest.java b/shenyu-loadbalancer/src/test/java/org/apache/shenyu/loadbalancer/factory/LoadBalancerFactoryTest.java index e7b7f2f280f9..46bfd6fd7793 100644 --- a/shenyu-loadbalancer/src/test/java/org/apache/shenyu/loadbalancer/factory/LoadBalancerFactoryTest.java +++ b/shenyu-loadbalancer/src/test/java/org/apache/shenyu/loadbalancer/factory/LoadBalancerFactoryTest.java @@ -18,6 +18,7 @@ package org.apache.shenyu.loadbalancer.factory; import org.apache.shenyu.common.enums.LoadBalanceEnum; +import org.apache.shenyu.common.enums.UpstreamManualStatusEnum; import org.apache.shenyu.loadbalancer.entity.LoadBalanceData; import org.apache.shenyu.loadbalancer.entity.Upstream; import org.junit.jupiter.api.Test; @@ -30,6 +31,7 @@ import java.util.stream.Stream; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; /** * The type loadBalance utils test. @@ -92,4 +94,28 @@ public void loadBalanceUtilsReversedWeightTest() { }); assertEquals(12, countMap.get("upstream-10").intValue()); } + + @Test + public void selectorShouldIgnoreForceOfflineUpstream() { + List upstreamList = List.of( + Upstream.builder().url("upstream-offline").weight(100).manualStatus(UpstreamManualStatusEnum.FORCE_OFFLINE.name()).build(), + Upstream.builder().url("upstream-online").weight(1).manualStatus(UpstreamManualStatusEnum.NONE.name()).build() + ); + + Upstream result = LoadBalancerFactory.selector(upstreamList, LoadBalanceEnum.ROUND_ROBIN.getName(), new LoadBalanceData()); + + assertEquals("upstream-online", result.getUrl()); + } + + @Test + public void selectorShouldReturnNullWhenAllUpstreamsAreForceOffline() { + List upstreamList = List.of( + Upstream.builder().url("upstream-offline-1").manualStatus(UpstreamManualStatusEnum.FORCE_OFFLINE.name()).build(), + Upstream.builder().url("upstream-offline-2").manualStatus(UpstreamManualStatusEnum.FORCE_OFFLINE.name()).build() + ); + + Upstream result = LoadBalancerFactory.selector(upstreamList, LoadBalanceEnum.ROUND_ROBIN.getName(), new LoadBalanceData()); + + assertNull(result); + } } diff --git a/shenyu-plugin/shenyu-plugin-proxy/shenyu-plugin-divide/src/main/java/org/apache/shenyu/plugin/divide/handler/DivideUpstreamDataHandler.java b/shenyu-plugin/shenyu-plugin-proxy/shenyu-plugin-divide/src/main/java/org/apache/shenyu/plugin/divide/handler/DivideUpstreamDataHandler.java index a6196b46e4da..c05b4b3ab023 100644 --- a/shenyu-plugin/shenyu-plugin-proxy/shenyu-plugin-divide/src/main/java/org/apache/shenyu/plugin/divide/handler/DivideUpstreamDataHandler.java +++ b/shenyu-plugin/shenyu-plugin-proxy/shenyu-plugin-divide/src/main/java/org/apache/shenyu/plugin/divide/handler/DivideUpstreamDataHandler.java @@ -76,6 +76,7 @@ private List convertUpstreamList(final List ups .warmup(Integer.parseInt(properties.getProperty("warmup", "10"))) .gray(Boolean.parseBoolean(properties.getProperty("gray", "false"))) .healthCheckEnabled(Boolean.parseBoolean(properties.getProperty("healthCheckEnabled", "true"))) + .manualStatus(u.getManualStatus()) .status(0 == u.getStatus()) .timestamp(Optional.ofNullable(u.getDateCreated()).map(Timestamp::getTime).orElse(System.currentTimeMillis())) .build(); diff --git a/shenyu-plugin/shenyu-plugin-proxy/shenyu-plugin-divide/src/test/java/org/apache/shenyu/plugin/divide/handler/DivideUpstreamDataHandlerTest.java b/shenyu-plugin/shenyu-plugin-proxy/shenyu-plugin-divide/src/test/java/org/apache/shenyu/plugin/divide/handler/DivideUpstreamDataHandlerTest.java index 184ae75b6185..225faf2d0b7f 100644 --- a/shenyu-plugin/shenyu-plugin-proxy/shenyu-plugin-divide/src/test/java/org/apache/shenyu/plugin/divide/handler/DivideUpstreamDataHandlerTest.java +++ b/shenyu-plugin/shenyu-plugin-proxy/shenyu-plugin-divide/src/test/java/org/apache/shenyu/plugin/divide/handler/DivideUpstreamDataHandlerTest.java @@ -19,6 +19,7 @@ import org.apache.shenyu.common.dto.DiscoverySyncData; import org.apache.shenyu.common.dto.DiscoveryUpstreamData; +import org.apache.shenyu.common.enums.UpstreamManualStatusEnum; import org.apache.shenyu.common.enums.PluginEnum; import org.apache.shenyu.common.utils.UpstreamCheckUtils; import org.apache.shenyu.loadbalancer.cache.UpstreamCacheManager; @@ -74,6 +75,8 @@ public void setUp() { @AfterEach public void tearDown() { + UpstreamCacheManager.getInstance().removeByKey("handler"); + UpstreamCacheManager.getInstance().removeByKey("manual-status-handler"); mockCheckUtils.close(); } @@ -97,4 +100,21 @@ public void handlerDiscoveryUpstreamDataTest() { public void pluginNamedTest() { assertEquals(divideUpstreamDataHandler.pluginName(), PluginEnum.DIVIDE.getName()); } + + @Test + public void handlerDiscoveryUpstreamDataShouldCarryManualStatus() { + DiscoveryUpstreamData upstreamData = DiscoveryUpstreamData.builder() + .url("manual-status-upstream") + .manualStatus(UpstreamManualStatusEnum.FORCE_OFFLINE.name()) + .dateUpdated(new Timestamp(System.currentTimeMillis())) + .build(); + DiscoverySyncData syncData = new DiscoverySyncData(); + syncData.setSelectorId("manual-status-handler"); + syncData.setUpstreamDataList(List.of(upstreamData)); + + divideUpstreamDataHandler.handlerDiscoveryUpstreamData(syncData); + + List result = UpstreamCacheManager.getInstance().findUpstreamListBySelectorId("manual-status-handler"); + assertEquals(UpstreamManualStatusEnum.FORCE_OFFLINE.name(), result.get(0).getManualStatus()); + } } diff --git a/shenyu-plugin/shenyu-plugin-proxy/shenyu-plugin-rpc/shenyu-plugin-grpc/src/main/java/org/apache/shenyu/plugin/grpc/handler/GrpcDiscoveryUpstreamDataHandler.java b/shenyu-plugin/shenyu-plugin-proxy/shenyu-plugin-rpc/shenyu-plugin-grpc/src/main/java/org/apache/shenyu/plugin/grpc/handler/GrpcDiscoveryUpstreamDataHandler.java index d2e604e8a0ca..46b834cc3ecc 100644 --- a/shenyu-plugin/shenyu-plugin-proxy/shenyu-plugin-rpc/shenyu-plugin-grpc/src/main/java/org/apache/shenyu/plugin/grpc/handler/GrpcDiscoveryUpstreamDataHandler.java +++ b/shenyu-plugin/shenyu-plugin-proxy/shenyu-plugin-rpc/shenyu-plugin-grpc/src/main/java/org/apache/shenyu/plugin/grpc/handler/GrpcDiscoveryUpstreamDataHandler.java @@ -74,6 +74,7 @@ private List convertUpstreamList(final List .weight(u.getWeight()) .status(0 == u.getStatus()) .timestamp(Optional.ofNullable(u.getDateCreated()).map(Timestamp::getTime).orElse(System.currentTimeMillis())) + .manualStatus(u.getManualStatus()) .healthCheckEnabled(Boolean.parseBoolean(properties.getProperty("healthCheckEnabled", "true"))) .build(); }).collect(Collectors.toList()); diff --git a/shenyu-plugin/shenyu-plugin-proxy/shenyu-plugin-rpc/shenyu-plugin-grpc/src/main/java/org/apache/shenyu/plugin/grpc/loadbalance/picker/ShenyuPicker.java b/shenyu-plugin/shenyu-plugin-proxy/shenyu-plugin-rpc/shenyu-plugin-grpc/src/main/java/org/apache/shenyu/plugin/grpc/loadbalance/picker/ShenyuPicker.java index b8450f109fb6..f53140e6d9a8 100644 --- a/shenyu-plugin/shenyu-plugin-proxy/shenyu-plugin-rpc/shenyu-plugin-grpc/src/main/java/org/apache/shenyu/plugin/grpc/loadbalance/picker/ShenyuPicker.java +++ b/shenyu-plugin/shenyu-plugin-proxy/shenyu-plugin-rpc/shenyu-plugin-grpc/src/main/java/org/apache/shenyu/plugin/grpc/loadbalance/picker/ShenyuPicker.java @@ -30,6 +30,7 @@ import org.apache.shenyu.plugin.grpc.loadbalance.SubChannelCopy; import java.util.List; +import java.util.Objects; import java.util.stream.Collectors; /** @@ -55,6 +56,9 @@ protected SubChannelCopy pick(final List list) { LoadBalanceData data = new LoadBalanceData(); data.setIp(remoteAddressIp); Upstream upstream = LoadBalancerFactory.selector(convertUpstreamList(grpcUpstreams), cacheRuleHandle.getLoadBalance(), data); + if (Objects.isNull(upstream)) { + return null; + } if (StringUtils.isBlank(upstream.getUrl()) && StringUtils.isBlank(upstream.getGroup()) && StringUtils.isBlank(upstream.getVersion())) { return randomPicker.pick(list); } @@ -71,6 +75,7 @@ private List convertUpstreamList(final List grpcUpstream .weight(u.getWeight()) .status(u.isStatus()) .timestamp(u.getTimestamp()) + .manualStatus(u.getManualStatus()) .healthCheckEnabled(u.isHealthCheckEnabled()) .build()).collect(Collectors.toList()); } diff --git a/shenyu-plugin/shenyu-plugin-proxy/shenyu-plugin-websocket/src/main/java/org/apache/shenyu/plugin/websocket/handler/WebSocketUpstreamDataHandler.java b/shenyu-plugin/shenyu-plugin-proxy/shenyu-plugin-websocket/src/main/java/org/apache/shenyu/plugin/websocket/handler/WebSocketUpstreamDataHandler.java index 923cdf67c5f5..7056be86e908 100644 --- a/shenyu-plugin/shenyu-plugin-proxy/shenyu-plugin-websocket/src/main/java/org/apache/shenyu/plugin/websocket/handler/WebSocketUpstreamDataHandler.java +++ b/shenyu-plugin/shenyu-plugin-proxy/shenyu-plugin-websocket/src/main/java/org/apache/shenyu/plugin/websocket/handler/WebSocketUpstreamDataHandler.java @@ -68,6 +68,7 @@ private List convertUpstreamList(final List ups .weight(u.getWeight()) .warmup(Integer.parseInt(properties.getProperty("warmup", "10"))) .healthCheckEnabled(Boolean.parseBoolean(properties.getProperty("healthCheckEnabled", "true"))) + .manualStatus(u.getManualStatus()) .status(0 == u.getStatus()) .timestamp(Optional.ofNullable(u.getDateCreated()).map(Timestamp::getTime).orElse(System.currentTimeMillis())) .build(); From 1041025bb599d93159f904224db5f6585343fa08 Mon Sep 17 00:00:00 2001 From: aias00 Date: Wed, 1 Apr 2026 21:52:42 +0800 Subject: [PATCH 2/5] Update shenyu-admin/src/main/resources/mappers/discovery-upstream-sqlmap.xml Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../src/main/resources/mappers/discovery-upstream-sqlmap.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shenyu-admin/src/main/resources/mappers/discovery-upstream-sqlmap.xml b/shenyu-admin/src/main/resources/mappers/discovery-upstream-sqlmap.xml index 616c25f3efcf..1bf75f839a66 100644 --- a/shenyu-admin/src/main/resources/mappers/discovery-upstream-sqlmap.xml +++ b/shenyu-admin/src/main/resources/mappers/discovery-upstream-sqlmap.xml @@ -254,7 +254,7 @@ UPDATE discovery_upstream - SET manual_status = #{manualStatus} + SET manual_status = #{manualStatus}, date_updated = CURRENT_TIMESTAMP WHERE discovery_handler_id = #{discoveryHandlerId} and upstream_url = #{upstreamUrl} From a0e40b23aec6fbfa5b05b83b94f3a15cb5907aae Mon Sep 17 00:00:00 2001 From: aias00 Date: Wed, 1 Apr 2026 21:52:54 +0800 Subject: [PATCH 3/5] Update shenyu-admin/src/main/java/org/apache/shenyu/admin/model/dto/UpstreamManualStatusDTO.java Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../shenyu/admin/model/dto/UpstreamManualStatusDTO.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/shenyu-admin/src/main/java/org/apache/shenyu/admin/model/dto/UpstreamManualStatusDTO.java b/shenyu-admin/src/main/java/org/apache/shenyu/admin/model/dto/UpstreamManualStatusDTO.java index 8d1b3317b27d..a7cbb6b6923c 100644 --- a/shenyu-admin/src/main/java/org/apache/shenyu/admin/model/dto/UpstreamManualStatusDTO.java +++ b/shenyu-admin/src/main/java/org/apache/shenyu/admin/model/dto/UpstreamManualStatusDTO.java @@ -27,13 +27,13 @@ public class UpstreamManualStatusDTO { /** * selector id. */ - @NotBlank(message = "selectorId can't be null") + @NotBlank(message = "selectorId can't be blank") private String selectorId; /** * upstream url. */ - @NotBlank(message = "url can't be null") + @NotBlank(message = "url can't be blank") private String url; /** From e74205b87527d8398bfd0c71d2d4960ccfb1b222 Mon Sep 17 00:00:00 2001 From: liuhy Date: Thu, 2 Apr 2026 09:19:42 +0800 Subject: [PATCH 4/5] fix: add discovery upstream manual status ddl --- db/init/mysql/schema.sql | 1 + db/init/ob/schema.sql | 1 + db/init/og/create-table.sql | 2 ++ db/init/oracle/schema.sql | 5 ++++- db/init/pg/create-table.sql | 2 ++ db/upgrade/2.7.1-upgrade-2.7.2-mysql.sql | 20 ++++++++++++++++++++ db/upgrade/2.7.1-upgrade-2.7.2-ob.sql | 20 ++++++++++++++++++++ db/upgrade/2.7.1-upgrade-2.7.2-og.sql | 21 +++++++++++++++++++++ db/upgrade/2.7.1-upgrade-2.7.2-oracle.sql | 21 +++++++++++++++++++++ db/upgrade/2.7.1-upgrade-2.7.2-pg.sql | 21 +++++++++++++++++++++ db/upgrade/upgrade-guide.md | 12 ++++++++++++ 11 files changed, 125 insertions(+), 1 deletion(-) create mode 100644 db/upgrade/2.7.1-upgrade-2.7.2-mysql.sql create mode 100644 db/upgrade/2.7.1-upgrade-2.7.2-ob.sql create mode 100644 db/upgrade/2.7.1-upgrade-2.7.2-og.sql create mode 100644 db/upgrade/2.7.1-upgrade-2.7.2-oracle.sql create mode 100644 db/upgrade/2.7.1-upgrade-2.7.2-pg.sql diff --git a/db/init/mysql/schema.sql b/db/init/mysql/schema.sql index c838022633b8..f2dc28fe7129 100644 --- a/db/init/mysql/schema.sql +++ b/db/init/mysql/schema.sql @@ -2444,6 +2444,7 @@ CREATE TABLE `discovery_upstream` `protocol` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT 'for http, https, tcp, ws', `upstream_url` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'ip:port', `upstream_status` int(0) NOT NULL COMMENT 'type (0, healthy, 1 unhealthy)', + `manual_status` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'NONE' COMMENT 'manual status (NONE, FORCE_OFFLINE)', `weight` int(0) NOT NULL COMMENT 'the weight for lists', `props` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT 'the other field (json)', `date_created` timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT 'create time', diff --git a/db/init/ob/schema.sql b/db/init/ob/schema.sql index 906323b20b10..406c5f14daf8 100644 --- a/db/init/ob/schema.sql +++ b/db/init/ob/schema.sql @@ -2361,6 +2361,7 @@ CREATE TABLE `discovery_upstream` `protocol` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT 'for http, https, tcp, ws', `upstream_url` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'ip:port', `upstream_status` int(0) NOT NULL COMMENT 'type (0, healthy, 1 unhealthy)', + `manual_status` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'NONE' COMMENT 'manual status (NONE, FORCE_OFFLINE)', `weight` int(0) NOT NULL COMMENT 'the weight for lists', `props` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT 'the other field (json)', `date_created` timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT 'create time', diff --git a/db/init/og/create-table.sql b/db/init/og/create-table.sql index 932dd7b449f8..31e81b3fc3ec 100644 --- a/db/init/og/create-table.sql +++ b/db/init/og/create-table.sql @@ -2542,6 +2542,7 @@ CREATE TABLE "public"."discovery_upstream" "protocol" varchar(128) COLLATE "pg_catalog"."default" NOT NULL, "upstream_url" varchar(128) COLLATE "pg_catalog"."default", "upstream_status" int4 NOT NULL, + "manual_status" varchar(32) COLLATE "pg_catalog"."default" NOT NULL DEFAULT 'NONE', "weight" int4 NOT NULL, "props" text COLLATE "pg_catalog"."default", "date_created" timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, @@ -2554,6 +2555,7 @@ COMMENT ON COLUMN "public"."discovery_upstream"."namespace_id" IS 'namespace id' COMMENT ON COLUMN "public"."discovery_upstream"."protocol" IS 'for http, https, tcp, ws'; COMMENT ON COLUMN "public"."discovery_upstream"."upstream_url" IS 'ip:port'; COMMENT ON COLUMN "public"."discovery_upstream"."upstream_status" IS 'type (0, healthy, 1 unhealthy)'; +COMMENT ON COLUMN "public"."discovery_upstream"."manual_status" IS 'manual status (NONE, FORCE_OFFLINE)'; COMMENT ON COLUMN "public"."discovery_upstream"."weight" IS 'the weight for lists'; COMMENT ON COLUMN "public"."discovery_upstream"."props" IS 'the other field (json)'; COMMENT ON COLUMN "public"."discovery_upstream"."date_created" IS 'create time'; diff --git a/db/init/oracle/schema.sql b/db/init/oracle/schema.sql index 7aeb47b46c50..a743089bec1a 100644 --- a/db/init/oracle/schema.sql +++ b/db/init/oracle/schema.sql @@ -2733,6 +2733,7 @@ create table discovery_upstream protocol VARCHAR2(64), upstream_url VARCHAR2(64) not null, upstream_status NUMBER(10) not null, + manual_status VARCHAR2(32) default 'NONE' not null, weight NUMBER(10) not null, props CLOB, date_created timestamp(3) default SYSDATE not null, @@ -2754,6 +2755,8 @@ comment on column DISCOVERY_UPSTREAM.upstream_url is 'ip:port'; comment on column DISCOVERY_UPSTREAM.upstream_status is 'type (0, healthy, 1 unhealthy)'; +comment on column DISCOVERY_UPSTREAM.manual_status + is 'manual status (NONE, FORCE_OFFLINE)'; comment on column DISCOVERY_UPSTREAM.weight is 'the weight for lists'; comment on column DISCOVERY_UPSTREAM.props @@ -3815,4 +3818,4 @@ INSERT INTO permission (id, object_id, resource_id, date_created, date_updated) INSERT INTO permission (id, object_id, resource_id, date_created, date_updated) VALUES ('1953049887387303902', '1346358560427216896', '1953048313980116901', sysdate, sysdate); INSERT INTO permission (id, object_id, resource_id, date_created, date_updated) VALUES ('1953049887387303903', '1346358560427216896', '1953048313980116902', sysdate, sysdate); INSERT INTO permission (id, object_id, resource_id, date_created, date_updated) VALUES ('1953049887387303904', '1346358560427216896', '1953048313980116903', sysdate, sysdate); -INSERT INTO permission (id, object_id, resource_id, date_created, date_updated) VALUES ('1953049887387303905', '1346358560427216896', '1953048313980116904', sysdate, sysdate); \ No newline at end of file +INSERT INTO permission (id, object_id, resource_id, date_created, date_updated) VALUES ('1953049887387303905', '1346358560427216896', '1953048313980116904', sysdate, sysdate); diff --git a/db/init/pg/create-table.sql b/db/init/pg/create-table.sql index d1e0130fd33d..617e49fbe506 100644 --- a/db/init/pg/create-table.sql +++ b/db/init/pg/create-table.sql @@ -2665,6 +2665,7 @@ CREATE TABLE "public"."discovery_upstream" "protocol" varchar(128) COLLATE "pg_catalog"."default" NOT NULL, "upstream_url" varchar(128) COLLATE "pg_catalog"."default", "upstream_status" int4 NOT NULL, + "manual_status" varchar(32) COLLATE "pg_catalog"."default" NOT NULL DEFAULT 'NONE', "weight" int4 NOT NULL, "props" text COLLATE "pg_catalog"."default", "date_created" timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, @@ -2677,6 +2678,7 @@ COMMENT ON COLUMN "public"."discovery_upstream"."namespace_id" IS 'the namespace COMMENT ON COLUMN "public"."discovery_upstream"."protocol" IS 'for http, https, tcp, ws'; COMMENT ON COLUMN "public"."discovery_upstream"."upstream_url" IS 'ip:port'; COMMENT ON COLUMN "public"."discovery_upstream"."upstream_status" IS 'type (0, healthy, 1 unhealthy)'; +COMMENT ON COLUMN "public"."discovery_upstream"."manual_status" IS 'manual status (NONE, FORCE_OFFLINE)'; COMMENT ON COLUMN "public"."discovery_upstream"."weight" IS 'the weight for lists'; COMMENT ON COLUMN "public"."discovery_upstream"."props" IS 'the other field (json)'; COMMENT ON COLUMN "public"."discovery_upstream"."date_created" IS 'create time'; 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 new file mode 100644 index 000000000000..5b790606103d --- /dev/null +++ b/db/upgrade/2.7.1-upgrade-2.7.2-mysql.sql @@ -0,0 +1,20 @@ +-- 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. + +-- this file works for MySQL. +ALTER TABLE `discovery_upstream` + ADD COLUMN `manual_status` varchar(32) NOT NULL DEFAULT 'NONE' COMMENT 'manual status (NONE, FORCE_OFFLINE)' + AFTER `upstream_status`; 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 new file mode 100644 index 000000000000..3bce16fdf621 --- /dev/null +++ b/db/upgrade/2.7.1-upgrade-2.7.2-ob.sql @@ -0,0 +1,20 @@ +-- 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. + +-- this file works for OceanBase. +ALTER TABLE `discovery_upstream` + ADD COLUMN `manual_status` varchar(32) NOT NULL DEFAULT 'NONE' COMMENT 'manual status (NONE, FORCE_OFFLINE)' + AFTER `upstream_status`; 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 new file mode 100644 index 000000000000..1dfe7f00e3fa --- /dev/null +++ b/db/upgrade/2.7.1-upgrade-2.7.2-og.sql @@ -0,0 +1,21 @@ +-- 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. + +-- this file works for og. +ALTER TABLE "public"."discovery_upstream" + ADD COLUMN "manual_status" VARCHAR(32) NOT NULL DEFAULT 'NONE'; + +COMMENT ON COLUMN "public"."discovery_upstream"."manual_status" IS 'manual status (NONE, FORCE_OFFLINE)'; 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 new file mode 100644 index 000000000000..62b9a808e3a3 --- /dev/null +++ b/db/upgrade/2.7.1-upgrade-2.7.2-oracle.sql @@ -0,0 +1,21 @@ +-- 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. + +-- this file works for Oracle, can not use "`" syntax. +ALTER TABLE discovery_upstream + ADD manual_status VARCHAR2(32) DEFAULT 'NONE' NOT NULL; + +COMMENT ON COLUMN discovery_upstream.manual_status IS 'manual status (NONE, FORCE_OFFLINE)'; 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 new file mode 100644 index 000000000000..9a65d9735e6c --- /dev/null +++ b/db/upgrade/2.7.1-upgrade-2.7.2-pg.sql @@ -0,0 +1,21 @@ +-- 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. + +-- this file works for PostgreSQL, can not use "`" syntax. +ALTER TABLE "public"."discovery_upstream" + ADD COLUMN "manual_status" VARCHAR(32) NOT NULL DEFAULT 'NONE'; + +COMMENT ON COLUMN "public"."discovery_upstream"."manual_status" IS 'manual status (NONE, FORCE_OFFLINE)'; diff --git a/db/upgrade/upgrade-guide.md b/db/upgrade/upgrade-guide.md index 1ac558b906ec..0d51073f9312 100644 --- a/db/upgrade/upgrade-guide.md +++ b/db/upgrade/upgrade-guide.md @@ -4,6 +4,18 @@ ## To Shenyu Users +- 2.7.1-upgrade-2.7.2-mysql.sql + +- 2.7.1-upgrade-2.7.2-ob.sql + +- 2.7.1-upgrade-2.7.2-og.sql + +- 2.7.1-upgrade-2.7.2-oracle.sql + +- 2.7.1-upgrade-2.7.2-pg.sql + + > this file is the Shenyu upgrade sql from v2.7.1 to v2.7.2 + - 2.7.0-upgrade-2.7.1-mysql.sql - 2.7.0-upgrade-2.7.1-og.sql From 315cec2ff8c7108f11d3e2a06ba4e91ac017a781 Mon Sep 17 00:00:00 2001 From: liuhy Date: Thu, 2 Apr 2026 10:25:18 +0800 Subject: [PATCH 5/5] fix discovery upstream manual status fallback --- .../admin/model/dto/DiscoveryUpstreamDTO.java | 4 +- .../impl/DiscoveryUpstreamServiceImpl.java | 8 ++-- .../model/dto/DiscoveryUpstreamDTOTest.java | 37 +++++++++++++++++++ .../service/DiscoveryUpstreamServiceTest.java | 15 ++++++++ 4 files changed, 57 insertions(+), 7 deletions(-) create mode 100644 shenyu-admin/src/test/java/org/apache/shenyu/admin/model/dto/DiscoveryUpstreamDTOTest.java diff --git a/shenyu-admin/src/main/java/org/apache/shenyu/admin/model/dto/DiscoveryUpstreamDTO.java b/shenyu-admin/src/main/java/org/apache/shenyu/admin/model/dto/DiscoveryUpstreamDTO.java index b817dd4a9e10..62bbbe4361cc 100644 --- a/shenyu-admin/src/main/java/org/apache/shenyu/admin/model/dto/DiscoveryUpstreamDTO.java +++ b/shenyu-admin/src/main/java/org/apache/shenyu/admin/model/dto/DiscoveryUpstreamDTO.java @@ -21,12 +21,12 @@ import org.apache.shenyu.admin.mapper.NamespaceMapper; import org.apache.shenyu.admin.validation.annotation.Existed; import org.apache.shenyu.common.enums.UpstreamManualStatusEnum; +import org.springframework.util.StringUtils; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import java.io.Serializable; import java.sql.Timestamp; -import java.util.Objects; /** * discovery upstream dto. @@ -307,6 +307,6 @@ public String getManualStatus() { * @param manualStatus manualStatus */ public void setManualStatus(final String manualStatus) { - this.manualStatus = Objects.isNull(manualStatus) ? null : UpstreamManualStatusEnum.normalize(manualStatus); + this.manualStatus = StringUtils.hasLength(manualStatus) ? UpstreamManualStatusEnum.normalize(manualStatus) : null; } } diff --git a/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/DiscoveryUpstreamServiceImpl.java b/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/DiscoveryUpstreamServiceImpl.java index ad4fa015b99a..1de679e22ed3 100644 --- a/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/DiscoveryUpstreamServiceImpl.java +++ b/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/DiscoveryUpstreamServiceImpl.java @@ -133,11 +133,9 @@ public void nativeCreateOrUpdate(final DiscoveryUpstreamDTO discoveryUpstreamDTO DiscoveryUpstreamDO discoveryUpstreamDO = DiscoveryUpstreamDO.buildDiscoveryUpstreamDO(discoveryUpstreamDTO); if (StringUtils.hasLength(discoveryUpstreamDTO.getId())) { if (!StringUtils.hasLength(discoveryUpstreamDTO.getManualStatus())) { - DiscoveryUpstreamDO existingRecord = discoveryUpstreamMapper.selectByDiscoveryHandlerIdAndUrl( - discoveryUpstreamDO.getDiscoveryHandlerId(), discoveryUpstreamDO.getUpstreamUrl()); - if (Objects.nonNull(existingRecord)) { - discoveryUpstreamDO.setManualStatus(existingRecord.getManualStatus()); - } + discoveryUpstreamMapper.selectByIds(Collections.singletonList(discoveryUpstreamDTO.getId())).stream() + .findFirst() + .ifPresent(existing -> discoveryUpstreamDO.setManualStatus(existing.getManualStatus())); } discoveryUpstreamMapper.updateSelective(discoveryUpstreamDO); } else { diff --git a/shenyu-admin/src/test/java/org/apache/shenyu/admin/model/dto/DiscoveryUpstreamDTOTest.java b/shenyu-admin/src/test/java/org/apache/shenyu/admin/model/dto/DiscoveryUpstreamDTOTest.java new file mode 100644 index 000000000000..c4032e61e1b8 --- /dev/null +++ b/shenyu-admin/src/test/java/org/apache/shenyu/admin/model/dto/DiscoveryUpstreamDTOTest.java @@ -0,0 +1,37 @@ +/* + * 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.shenyu.admin.model.dto; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertNull; + +/** + * Test cases for {@link DiscoveryUpstreamDTO}. + */ +public final class DiscoveryUpstreamDTOTest { + + @Test + public void setManualStatusShouldTreatEmptyStringAsNull() { + DiscoveryUpstreamDTO discoveryUpstreamDTO = new DiscoveryUpstreamDTO(); + + discoveryUpstreamDTO.setManualStatus(""); + + assertNull(discoveryUpstreamDTO.getManualStatus()); + } +} diff --git a/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/DiscoveryUpstreamServiceTest.java b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/DiscoveryUpstreamServiceTest.java index 6562af2b562e..1733fafec6d9 100644 --- a/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/DiscoveryUpstreamServiceTest.java +++ b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/DiscoveryUpstreamServiceTest.java @@ -45,6 +45,7 @@ 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.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; @@ -222,6 +223,20 @@ public void testChangeStatusBySelectorIdAndUrlShouldSkipAliveUpdateWhenForceOffl verify(discoveryUpstreamMapper, never()).updateStatusByUrl(anyString(), anyString(), anyInt()); } + @Test + public void testNativeCreateOrUpdateShouldPreserveManualStatusWhenUrlChanges() { + when(discoveryUpstreamMapper.updateSelective(any())).thenReturn(1); + when(discoveryUpstreamMapper.selectByIds(Collections.singletonList("123"))) + .thenReturn(Collections.singletonList(buildDiscoveryUpstreamDO("123", "123", "url1", UpstreamManualStatusEnum.FORCE_OFFLINE))); + DiscoveryUpstreamDTO discoveryUpstreamDTO = buildDiscoveryUpstreamDTO("123", "123", "url2"); + + discoveryUpstreamService.nativeCreateOrUpdate(discoveryUpstreamDTO); + + ArgumentCaptor captor = ArgumentCaptor.forClass(DiscoveryUpstreamDO.class); + verify(discoveryUpstreamMapper).updateSelective(captor.capture()); + assertEquals(UpstreamManualStatusEnum.FORCE_OFFLINE.name(), captor.getValue().getManualStatus()); + } + private void testUpdate() { when(discoveryUpstreamMapper.update(any())).thenReturn(1); DiscoveryUpstreamDTO discoveryUpstreamDTO = buildDiscoveryUpstreamDTO("123");