Conversation
There was a problem hiding this comment.
Pull request overview
Adds a persisted “manual upstream status” flag (e.g., FORCE_OFFLINE) to discovery upstream data, exposes Admin endpoints to toggle it, and ensures Gateway/load-balancing honors the manual override during selection and recovery.
Changes:
- Introduce
manualStatusacross persistence/DTO/VO/sync payloads and map it into Gateway upstream cache objects. - Add Admin
/upstream/offlineand/upstream/onlineendpoints + service logic to persist and publish refresh sync events. - Filter manually forced-offline upstreams out of load-balancer selection and add targeted tests.
Reviewed changes
Copilot reviewed 27 out of 27 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| shenyu-plugin/shenyu-plugin-proxy/shenyu-plugin-websocket/src/main/java/org/apache/shenyu/plugin/websocket/handler/WebSocketUpstreamDataHandler.java | Propagates manualStatus into WebSocket upstream cache objects. |
| shenyu-plugin/shenyu-plugin-proxy/shenyu-plugin-rpc/shenyu-plugin-grpc/src/main/java/org/apache/shenyu/plugin/grpc/loadbalance/picker/ShenyuPicker.java | Handles selector() possibly returning null; maps manualStatus into Upstream. |
| shenyu-plugin/shenyu-plugin-proxy/shenyu-plugin-rpc/shenyu-plugin-grpc/src/main/java/org/apache/shenyu/plugin/grpc/handler/GrpcDiscoveryUpstreamDataHandler.java | Propagates manualStatus into gRPC upstream cache objects. |
| shenyu-plugin/shenyu-plugin-proxy/shenyu-plugin-divide/src/test/java/org/apache/shenyu/plugin/divide/handler/DivideUpstreamDataHandlerTest.java | Adds test asserting sync payload carries manualStatus into cached upstreams. |
| shenyu-plugin/shenyu-plugin-proxy/shenyu-plugin-divide/src/main/java/org/apache/shenyu/plugin/divide/handler/DivideUpstreamDataHandler.java | Propagates manualStatus into Divide upstream cache objects. |
| shenyu-loadbalancer/src/test/java/org/apache/shenyu/loadbalancer/factory/LoadBalancerFactoryTest.java | Adds tests ensuring forced-offline upstreams are ignored and null is returned if all are offline. |
| shenyu-loadbalancer/src/main/java/org/apache/shenyu/loadbalancer/factory/LoadBalancerFactory.java | Filters FORCE_OFFLINE upstreams before delegating to the load-balancer. |
| shenyu-loadbalancer/src/main/java/org/apache/shenyu/loadbalancer/entity/Upstream.java | Adds manualStatus to upstream entity + helper to detect manual offline. |
| shenyu-common/src/main/java/org/apache/shenyu/common/enums/UpstreamManualStatusEnum.java | Introduces shared enum + normalization helpers. |
| shenyu-common/src/main/java/org/apache/shenyu/common/dto/convert/selector/GrpcUpstream.java | Adds builder support for manualStatus. |
| shenyu-common/src/main/java/org/apache/shenyu/common/dto/convert/selector/CommonUpstream.java | Adds manualStatus field + normalization and convenience check. |
| shenyu-common/src/main/java/org/apache/shenyu/common/dto/DiscoveryUpstreamData.java | Adds manualStatus to discovery sync DTO and includes it in equals/hashCode/builder. |
| shenyu-admin/src/test/java/org/apache/shenyu/admin/service/DiscoveryUpstreamServiceTest.java | Adds tests for manual status update event publishing and heartbeat recovery short-circuit. |
| shenyu-admin/src/main/resources/sql-script/h2/schema.sql | Adds manual_status column to discovery_upstream schema. |
| shenyu-admin/src/main/resources/mappers/discovery-upstream-sqlmap.xml | Maps manual_status, adds update statement, and adjusts select mapping. |
| shenyu-admin/src/main/java/org/apache/shenyu/admin/utils/CommonUpstreamUtils.java | Ensures manualStatus and namespaceId are set in converted upstreams. |
| shenyu-admin/src/main/java/org/apache/shenyu/admin/transfer/DiscoveryTransfer.java | Maps manualStatus across DO/DTO/VO and sync payload conversions. |
| shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/DiscoveryUpstreamServiceImpl.java | Persists manual status and publishes DISCOVER_UPSTREAM refresh events; short-circuits alive updates when forced offline. |
| shenyu-admin/src/main/java/org/apache/shenyu/admin/service/DiscoveryUpstreamService.java | Adds service API to change manual status. |
| shenyu-admin/src/main/java/org/apache/shenyu/admin/model/vo/DiscoveryUpstreamVO.java | Exposes manualStatus in Admin VO. |
| shenyu-admin/src/main/java/org/apache/shenyu/admin/model/entity/DiscoveryUpstreamDO.java | Persists manualStatus on DO and normalizes values. |
| shenyu-admin/src/main/java/org/apache/shenyu/admin/model/dto/UpstreamManualStatusDTO.java | Adds request DTO for manual online/offline endpoints. |
| shenyu-admin/src/main/java/org/apache/shenyu/admin/model/dto/DiscoveryUpstreamDTO.java | Adds manualStatus to DTO with normalization. |
| shenyu-admin/src/main/java/org/apache/shenyu/admin/mapper/DiscoveryUpstreamMapper.java | Adds mapper method to update manual status by URL. |
| shenyu-admin/src/main/java/org/apache/shenyu/admin/controller/UpstreamController.java | Adds /upstream/offline and /upstream/online Admin endpoints. |
| docs/superpowers/specs/2026-04-01-upstream-manual-status-design.md | Documents design rationale and behavior contract for manual upstream status. |
| docs/superpowers/plans/2026-04-01-upstream-manual-status.md | Documents implementation plan and testing strategy. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| public static String normalize(final String manualStatus) { | ||
| return Arrays.stream(values()) | ||
| .filter(value -> value.name().equalsIgnoreCase(manualStatus)) | ||
| .findFirst() | ||
| .orElse(NONE) | ||
| .name(); | ||
| } |
There was a problem hiding this comment.
normalize will throw a NullPointerException when manualStatus is null (because equalsIgnoreCase is called on a null argument). This is reachable via multiple call sites in this PR (e.g., new Upstream.Builder() where builder.manualStatus is unset). Make normalize null/blank-safe (e.g., treat null/blank as NONE) to prevent construction-time NPEs and make isForceOffline safe.
| 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 |
There was a problem hiding this comment.
Base_Column_List appears to include a full FROM discovery_upstream du clause, but selectByDiscoveryHandlerIdAndUrl appends from discovery_upstream again. This will generate invalid SQL (duplicate FROM / unexpected alias usage). Fix by making Base_Column_List columns-only (no FROM) and alias consistently, or remove the extra from discovery_upstream and reuse the same alias (du) in the WHERE clause.
| <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> |
There was a problem hiding this comment.
Base_Column_List appears to include a full FROM discovery_upstream du clause, but selectByDiscoveryHandlerIdAndUrl appends from discovery_upstream again. This will generate invalid SQL (duplicate FROM / unexpected alias usage). Fix by making Base_Column_List columns-only (no FROM) and alias consistently, or remove the extra from discovery_upstream and reuse the same alias (du) in the WHERE clause.
shenyu-admin/src/main/resources/mappers/discovery-upstream-sqlmap.xml
Outdated
Show resolved
Hide resolved
| 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); |
There was a problem hiding this comment.
In the update-by-id path (hasLength(id)), the fallback lookup for preserving manualStatus uses (discoveryHandlerId, upstreamUrl) instead of the primary key id. If the update changes upstreamUrl (or if there are duplicates/edge cases), this can fail to find the existing record and inadvertently reset/alter manualStatus. Prefer selecting the existing row by id in this branch when preserving fields.
| @NotBlank(message = "selectorId can't be null") | ||
| private String selectorId; | ||
|
|
||
| /** | ||
| * upstream url. | ||
| */ | ||
| @NotBlank(message = "url can't be null") |
There was a problem hiding this comment.
@NotBlank validates both null and blank/whitespace. The message currently says "can't be null", which is misleading for empty or whitespace values. Consider changing the messages to "can't be blank" (or "can't be null or blank") to match the actual constraint behavior.
| @NotBlank(message = "selectorId can't be null") | |
| private String selectorId; | |
| /** | |
| * upstream url. | |
| */ | |
| @NotBlank(message = "url can't be null") | |
| @NotBlank(message = "selectorId can't be null or blank") | |
| private String selectorId; | |
| /** | |
| * upstream url. | |
| */ | |
| @NotBlank(message = "url can't be null or blank") |
shenyu-admin/src/main/java/org/apache/shenyu/admin/model/dto/UpstreamManualStatusDTO.java
Outdated
Show resolved
Hide resolved
| public void setManualStatus(final String manualStatus) { | ||
| this.manualStatus = Objects.isNull(manualStatus) ? null : UpstreamManualStatusEnum.normalize(manualStatus); | ||
| } |
There was a problem hiding this comment.
This setter treats an empty string as a provided value (it will be normalized, likely to NONE), while other parts of the service use StringUtils.hasLength(...) to detect 'not provided' and preserve existing DB state. To keep request semantics consistent, consider treating blank/empty as null here as well (so callers can omit the field without accidentally overwriting to NONE).
| 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); | ||
| } |
There was a problem hiding this comment.
Using Stream#toList() requires newer JDKs (Java 16+). If this project/module targets Java 8/11 (common for libraries), this will not compile. Prefer collect(Collectors.toList()) (or a project-standard collector) for broader compatibility.
…map.xml Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
…pstreamManualStatusDTO.java Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Summary
manualStatusto discovery upstream persistence, DTO/VO conversion, and sync payloads/upstream/offlineand/upstream/onlineAPIs to persist manual offline state and publish discovery sync eventsTesting