Skip to content

feat: add manual discovery upstream status#6315

Open
Aias00 wants to merge 4 commits intomasterfrom
feat/upstream-manual-status
Open

feat: add manual discovery upstream status#6315
Aias00 wants to merge 4 commits intomasterfrom
feat/upstream-manual-status

Conversation

@Aias00
Copy link
Copy Markdown
Contributor

@Aias00 Aias00 commented Apr 1, 2026

Summary

  • add manualStatus to discovery upstream persistence, DTO/VO conversion, and sync payloads
  • add admin /upstream/offline and /upstream/online APIs to persist manual offline state and publish discovery sync events
  • short-circuit heartbeat recovery and gateway load-balancing when an upstream is manually forced offline

Testing

  • mvn -pl shenyu-admin,shenyu-loadbalancer,shenyu-plugin/shenyu-plugin-proxy/shenyu-plugin-divide -am -DskipITs -DfailIfNoTests=false -Dtest=DiscoveryUpstreamServiceTest,LoadBalancerFactoryTest,DivideUpstreamDataHandlerTest test
  • mvn -pl shenyu-plugin/shenyu-plugin-proxy/shenyu-plugin-websocket,shenyu-plugin/shenyu-plugin-proxy/shenyu-plugin-rpc/shenyu-plugin-grpc -am -DskipITs -DskipTests compile

Copilot AI review requested due to automatic review settings April 1, 2026 12:08
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 manualStatus across persistence/DTO/VO/sync payloads and map it into Gateway upstream cache objects.
  • Add Admin /upstream/offline and /upstream/online endpoints + 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.

Comment on lines +43 to +49
public static String normalize(final String manualStatus) {
return Arrays.stream(values())
.filter(value -> value.name().equalsIgnoreCase(manualStatus))
.findFirst()
.orElse(NONE)
.name();
}
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines 185 to 195
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
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines 199 to 202
<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>
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines 134 to 142
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);
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +30 to +36
@NotBlank(message = "selectorId can't be null")
private String selectorId;

/**
* upstream url.
*/
@NotBlank(message = "url can't be null")
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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.

Suggested change
@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")

Copilot uses AI. Check for mistakes.
Comment on lines +309 to +311
public void setManualStatus(final String manualStatus) {
this.manualStatus = Objects.isNull(manualStatus) ? null : UpstreamManualStatusEnum.normalize(manualStatus);
}
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Comment on lines 44 to 57
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);
}
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Aias00 and others added 2 commits April 1, 2026 21:52
…map.xml

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
…pstreamManualStatusDTO.java

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants