This is an automated email from the ASF dual-hosted git repository.

liuhongyu pushed a commit to branch feat/upstream-manual-status
in repository https://gitbox.apache.org/repos/asf/shenyu.git

commit 911f043ee903cf29b13c7f05b9783fbc9f362298
Author: liuhy <[email protected]>
AuthorDate: Wed Apr 1 20:05:39 2026 +0800

    feat: add manual discovery upstream status
---
 .../plans/2026-04-01-upstream-manual-status.md     | 88 +++++++++++++++++++++
 .../2026-04-01-upstream-manual-status-design.md    | 92 ++++++++++++++++++++++
 .../admin/controller/UpstreamController.java       | 71 +++++++++++++++++
 .../admin/mapper/DiscoveryUpstreamMapper.java      | 11 +++
 .../admin/model/dto/DiscoveryUpstreamDTO.java      | 25 ++++++
 .../admin/model/dto/UpstreamManualStatusDTO.java   | 74 +++++++++++++++++
 .../admin/model/entity/DiscoveryUpstreamDO.java    | 42 ++++++++++
 .../shenyu/admin/model/vo/DiscoveryUpstreamVO.java | 23 ++++++
 .../admin/service/DiscoveryUpstreamService.java    | 10 +++
 .../service/impl/DiscoveryUpstreamServiceImpl.java | 60 +++++++++++++-
 .../shenyu/admin/transfer/DiscoveryTransfer.java   | 11 +++
 .../shenyu/admin/utils/CommonUpstreamUtils.java    |  2 +
 .../mappers/discovery-upstream-sqlmap.xml          | 21 ++++-
 .../src/main/resources/sql-script/h2/schema.sql    |  1 +
 .../service/DiscoveryUpstreamServiceTest.java      | 53 ++++++++++++-
 .../shenyu/common/dto/DiscoveryUpstreamData.java   | 43 +++++++++-
 .../dto/convert/selector/CommonUpstream.java       | 34 ++++++++
 .../common/dto/convert/selector/GrpcUpstream.java  | 17 ++++
 .../common/enums/UpstreamManualStatusEnum.java     | 60 ++++++++++++++
 .../shenyu/loadbalancer/entity/Upstream.java       | 51 ++++++++++++
 .../loadbalancer/factory/LoadBalancerFactory.java  | 13 ++-
 .../factory/LoadBalancerFactoryTest.java           | 26 ++++++
 .../divide/handler/DivideUpstreamDataHandler.java  |  1 +
 .../handler/DivideUpstreamDataHandlerTest.java     | 20 +++++
 .../handler/GrpcDiscoveryUpstreamDataHandler.java  |  1 +
 .../grpc/loadbalance/picker/ShenyuPicker.java      |  5 ++
 .../handler/WebSocketUpstreamDataHandler.java      |  1 +
 27 files changed, 850 insertions(+), 6 deletions(-)

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 0000000000..cc377a0e92
--- /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 0000000000..8678b76d98
--- /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 0000000000..2e3a6344b4
--- /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 ad049bcc79..2f17f256c4 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 dcceeb0bf4..b817dd4a9e 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 @@ package org.apache.shenyu.admin.model.dto;
 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 class DiscoveryUpstreamDTO implements Serializable {
     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 0000000000..8d1b3317b2
--- /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 93f899b8cc..68060754d5 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 class DiscoveryUpstreamDO extends BaseDO {
         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 class DiscoveryUpstreamDO extends BaseDO {
                     .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 class DiscoveryUpstreamDO extends BaseDO {
          */
         private String namespaceId;
 
+        /**
+         * manualStatus.
+         */
+        private String manualStatus = UpstreamManualStatusEnum.NONE.name();
+
         /**
          * id.
          *
@@ -446,6 +476,17 @@ public class DiscoveryUpstreamDO extends BaseDO {
             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 class DiscoveryUpstreamDO extends BaseDO {
             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 a6bfcd42f2..6c888f600c 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 class DiscoveryUpstreamVO {
     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 d808c194b6..55d2e836bc 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.model.vo.DiscoveryUpstreamVO;
 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 b626f314fa..ad4fa015b9 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 @@ package org.apache.shenyu.admin.service.impl;
 
 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.transfer.DiscoveryTransfer;
 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 class DiscoveryUpstreamServiceImpl implements 
DiscoveryUpstreamService {
                                         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 class DiscoveryUpstreamServiceImpl implements 
DiscoveryUpstreamService {
         this.selectorMapper = selectorMapper;
         this.proxySelectorMapper = proxySelectorMapper;
         this.pluginMapper = pluginMapper;
+        this.eventPublisher = eventPublisher;
     }
 
     /**
@@ -123,6 +132,13 @@ public class DiscoveryUpstreamServiceImpl implements 
DiscoveryUpstreamService {
     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.updateSelective(discoveryUpstreamDO);
         } else {
             DiscoveryUpstreamDO existingRecord = 
discoveryUpstreamMapper.selectByDiscoveryHandlerIdAndUrl(discoveryUpstreamDO.getDiscoveryHandlerId(),
 discoveryUpstreamDO.getUpstreamUrl());
@@ -217,6 +233,11 @@ public class DiscoveryUpstreamServiceImpl implements 
DiscoveryUpstreamService {
      */
     private String update(final DiscoveryUpstreamDTO discoveryUpstreamDTO) {
         DiscoveryUpstreamDO discoveryUpstreamDO = 
DiscoveryUpstreamDO.buildDiscoveryUpstreamDO(discoveryUpstreamDTO);
+        if (!StringUtils.hasLength(discoveryUpstreamDTO.getManualStatus())) {
+            
discoveryUpstreamMapper.selectByIds(Collections.singletonList(discoveryUpstreamDTO.getId())).stream()
+                    .findFirst()
+                    .ifPresent(existing -> 
discoveryUpstreamDO.setManualStatus(existing.getManualStatus()));
+        }
         discoveryUpstreamMapper.update(discoveryUpstreamDO);
         fetchAll(discoveryUpstreamDTO.getDiscoveryHandlerId());
         return ShenyuResultMessage.UPDATE_SUCCESS;
@@ -246,10 +267,27 @@ public class DiscoveryUpstreamServiceImpl implements 
DiscoveryUpstreamService {
     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<DiscoveryUpstreamDTO> 
discoveryUpstreamList) {
@@ -351,4 +389,24 @@ public class DiscoveryUpstreamServiceImpl implements 
DiscoveryUpstreamService {
         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<DiscoveryUpstreamData> 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 91f854cd5f..f233d8b85b 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.admin.utils.CommonUpstreamUtils;
 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 enum DiscoveryTransfer {
             .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 enum DiscoveryTransfer {
                     .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 enum DiscoveryTransfer {
             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 enum DiscoveryTransfer {
             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 enum DiscoveryTransfer {
             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 enum DiscoveryTransfer {
             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 enum DiscoveryTransfer {
                 .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 6a6cdc5c2e..2f91947cce 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 class CommonUpstreamUtils {
                             upstream.getUpstreamHost(), 
upstream.getUpstreamUrl(),
                             upstream.isStatus(), upstream.getTimestamp());
                     
commonUpstream.setHealthCheckEnabled(upstream.isHealthCheckEnabled());
+                    commonUpstream.setNamespaceId(upstream.getNamespaceId());
+                    commonUpstream.setManualStatus(upstream.getManualStatus());
                     return commonUpstream;
                 })
                 .collect(Collectors.toList());
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 7b17d713fd..616c25f3ef 100644
--- a/shenyu-admin/src/main/resources/mappers/discovery-upstream-sqlmap.xml
+++ b/shenyu-admin/src/main/resources/mappers/discovery-upstream-sqlmap.xml
@@ -27,6 +27,7 @@
         <result column="protocol" jdbcType="VARCHAR" property="protocol"/>
         <result column="upstream_url" jdbcType="VARCHAR" 
property="upstreamUrl"/>
         <result column="upstream_status" jdbcType="INTEGER" 
property="upstreamStatus"/>
+        <result column="manual_status" jdbcType="VARCHAR" 
property="manualStatus"/>
         <result column="weight" jdbcType="INTEGER" property="weight"/>
         <result column="props" jdbcType="VARCHAR" property="props"/>
     </resultMap>
@@ -40,6 +41,7 @@
         protocol,
         upstream_url,
         upstream_status,
+        manual_status,
         weight,
         props
     </sql>
@@ -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 @@
         <if test="upstreamStatus != null">
             upstream_status=#{upstreamStatus,jdbcType=INTEGER},
         </if>
+        <if test="manualStatus != null">
+            manual_status=#{manualStatus,jdbcType=VARCHAR},
+        </if>
         <if test="weight != null">
             weight=#{weight,jdbcType=INTEGER},
         </if>
@@ -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 @@
     </sql>
 
     <select id="selectByDiscoveryHandlerIdAndUrl"
-            
resultType="org.apache.shenyu.admin.model.entity.DiscoveryUpstreamDO">
+            resultMap="BaseResultMap">
         SELECT <include refid="Base_Column_List"/> from discovery_upstream 
where discovery_handler_id = #{discoveryHandlerId} and upstream_url = 
#{upstreamUrl} for update
     </select>
 
@@ -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>
 
+    <update id="updateManualStatusByUrl">
+        UPDATE discovery_upstream
+        SET manual_status = #{manualStatus}
+        WHERE discovery_handler_id = #{discoveryHandlerId} and upstream_url = 
#{upstreamUrl}
+    </update>
+
 </mapper>
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 16dd2dd278..8dfe5dd2c8 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 28adf2c609..6562af2b56 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.model.vo.DiscoveryUpstreamVO;
 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.junit.jupiter.api.extension.ExtendWith;
 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 java.util.List;
 
 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 final class DiscoveryUpstreamServiceTest {
                 discoveryRelMapper,
                 selectorMapper,
                 pluginMapper,
-                discoveryProcessorHolder
+                discoveryProcessorHolder,
+                eventPublisher
         );
     }
 
@@ -142,8 +152,11 @@ public final class DiscoveryUpstreamServiceTest {
         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<DiscoverySyncData> 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 final class DiscoveryUpstreamServiceTest {
         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 @@ public final class DiscoveryUpstreamServiceTest {
     }
 
     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 @@ public final class DiscoveryUpstreamServiceTest {
         SelectorDO selectorDO = new SelectorDO();
         selectorDO.setId("selector_1");
         selectorDO.setSelectorName("selector_1");
+        selectorDO.setNamespaceId("test");
         return selectorDO;
     }
 
@@ -293,4 +338,10 @@ public final class DiscoveryUpstreamServiceTest {
         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 20fcae9946..0567a0ae41 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 class DiscoveryUpstreamData {
         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 class DiscoveryUpstreamData {
                 && 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 class DiscoveryUpstreamData {
 
         private String namespaceId;
 
+        private String manualStatus = UpstreamManualStatusEnum.NONE.name();
+
         private Builder() {
         }
 
@@ -432,6 +459,17 @@ public class DiscoveryUpstreamData {
             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 class DiscoveryUpstreamData {
             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 13f10e7b1f..f81476b1ff 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 class CommonUpstream {
         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 8379e719e8..f35cf4fa39 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 @@ public final class GrpcUpstream extends CommonUpstream {
         setTimestamp(builder.timestamp);
         setNamespaceId(builder.namespaceId);
         setHealthCheckEnabled(builder.healthCheckEnabled);
+        setManualStatus(builder.manualStatus);
     }
 
     /**
@@ -169,6 +170,11 @@ public final class GrpcUpstream extends CommonUpstream {
          */
         private boolean healthCheckEnabled = true;
 
+        /**
+         * manual status.
+         */
+        private String manualStatus;
+
         /**
          * no args constructor.
          */
@@ -272,5 +278,16 @@ public final class GrpcUpstream extends CommonUpstream {
             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 0000000000..073c4caebb
--- /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 d832dc28ff..3363c574d1 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<String, String> metadata = new ConcurrentHashMap<>();
     
@@ -133,6 +139,7 @@ public final class Upstream {
         this.version = builder.version;
         this.gray = builder.gray;
         this.healthCheckEnabled = builder.healthCheckEnabled;
+        this.manualStatus = 
UpstreamManualStatusEnum.normalize(builder.manualStatus);
     }
     
     /**
@@ -251,6 +258,33 @@ public final class Upstream {
     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 final class Upstream {
                 + ", url='" + url
                 + ", weight=" + weight
                 + ", status=" + status
+                + ", manualStatus='" + manualStatus + '\''
                 + ", timestamp=" + timestamp
                 + ", warmup=" + warmup
                 + ", group='" + group
@@ -581,6 +616,11 @@ public final class Upstream {
          */
         private boolean healthCheckEnabled = true;
 
+        /**
+         * manual status.
+         */
+        private String manualStatus;
+
         /**
          * no args constructor.
          */
@@ -707,5 +747,16 @@ public final class Upstream {
             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 291039da43..a8702cc7a2 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.loadbalancer.spi.LoadBalancer;
 import org.apache.shenyu.spi.ExtensionLoader;
 
 import java.util.List;
+import java.util.Objects;
 
 /**
  * The type Load balance Factory.
@@ -41,7 +42,17 @@ public final class LoadBalancerFactory {
      * @return the upstream
      */
     public static Upstream selector(final List<Upstream> upstreamList, final 
String algorithm, final LoadBalanceData data) {
+        if (Objects.isNull(upstreamList)) {
+            return null;
+        }
+        List<Upstream> 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 e7b7f2f280..46bfd6fd77 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.IntStream;
 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 final class LoadBalancerFactoryTest {
         });
         assertEquals(12, countMap.get("upstream-10").intValue());
     }
+
+    @Test
+    public void selectorShouldIgnoreForceOfflineUpstream() {
+        List<Upstream> 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<Upstream> 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 a6196b46e4..c05b4b3ab0 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 @@ public class DivideUpstreamDataHandler implements 
DiscoveryUpstreamDataHandler {
                     .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 184ae75b61..225faf2d0b 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 @@ package org.apache.shenyu.plugin.divide.handler;
 
 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 class DivideUpstreamDataHandlerTest {
 
     @AfterEach
     public void tearDown() {
+        UpstreamCacheManager.getInstance().removeByKey("handler");
+        
UpstreamCacheManager.getInstance().removeByKey("manual-status-handler");
         mockCheckUtils.close();
     }
 
@@ -97,4 +100,21 @@ public class DivideUpstreamDataHandlerTest {
     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<Upstream> 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 d2e604e8a0..46b834cc3e 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 @@ public class GrpcDiscoveryUpstreamDataHandler implements 
DiscoveryUpstreamDataHa
                     .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 b8450f109f..f53140e6d9 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.context.GrpcConstants;
 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 @@ public class ShenyuPicker extends AbstractReadyPicker {
             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 @@ public class ShenyuPicker extends AbstractReadyPicker {
                 .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 923cdf67c5..7056be86e9 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 @@ public class WebSocketUpstreamDataHandler implements 
DiscoveryUpstreamDataHandle
                     .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();

Reply via email to