SouthwestAsiaFloat commented on issue #7996:
URL: https://github.com/apache/shenyu/issues/7996#issuecomment-6386043558

   **中文版**
   
   我想认领 #7996。目前有一个初步思路,可以把整个过程理解成:**请求沿着一张路线图前进,插件是沿途的处理站,执行器负责导航。** 
下面的类名和字段名暂时作为设计草案,具体再与相关契约对齐。
   
   首先,请求到来时,需要决定使用原来的插件链,还是新的图执行方式。为了表达这个选择,可以引入一个 
`ExecutionDispatcher`,把它理解成入口的分流员。它根据部署配置选择 LEGACY 或 GRAPH,默认仍然走 LEGACY。
   
   选定以后,本次请求的整套插件流程就交给对应的执行器负责,包括全局处理、认证、代理转发和响应处理。两个执行器可以复用现有的 `ShenyuPlugin` 
实现,各自管理自己的执行进度。
   
   这个选择需要在本次请求执行期间保持固定。就像出发时已经选好了路线,中途不能因为图更新或执行出错,又回到入口重新走一遍旧链。如果部署配置选择了 
GRAPH,却没有可用的图,就应该明确拒绝;发布一张新图本身也不应该自动切换执行模式。
   
   接下来,需要在程序里表示这张路线图,可以对接 #7995 提供的 `GraphPlan`。
   
   为了知道“是哪张图”,需要 `graphId`;为了区分更新前后的版本,需要 `revision`;为了知道从哪里出发,需要 
`entryNodeId`。图中的每个节点也需要自己的 `nodeId`,就像地图上的地点都有地址,执行器才能找到它。
   
   节点之间的连线表示下一步的方向。对于普通插件节点,可以把后继地址称为 
`nextNodeId`;对于条件节点,则保存判断条件和对应目标,像一个有指示牌的岔路口,根据当前请求选择其中一条路。具体字段名称再和图模型统一。
   
   
不过,图上写着插件名称,还不足以直接执行。就像地图标出了一个服务站,我们还需要找到真正提供服务的人。因此,可以在执行前增加一个绑定过程,把节点里的插件名称解析成已有的
 `ShenyuPlugin` 实例。为了区分这种“已经准备好、能够执行的地图”,暂且称它为 `BoundGraphPlan`。
   
   地图准备好后,需要有人带着请求看图前进。为了描述这一次执行,不妨定义一个 `GraphExecution`,把它当作导航员。
   
   每次请求的入口被订阅、真正开始执行时,都会创建自己的导航员,并拿定本次使用的地图。导航员通过类似下面的方法推进:
   
   ```java
   run(nodeId, exchange)
   ```
   
   这里的 `nodeId` 表示现在要去的处理站,`exchange` 
就是沿途携带的请求上下文。插件如果修改或装饰了请求,后面的步骤拿到的也应该是它传回来的那个上下文。
   
   到插件节点时,执行器保留原有的 `skip` 和 hooks 行为,再由插件按自己的 selector / rule 
机制判断是否处理请求。到条件节点时,则由执行器根据当前上下文选路。
   
   例如:
   
   ```text
   插件 A → 条件判断 → 插件 B 或插件 C → 公共后续流程
   ```
   
   一次执行在岔路口只选择其中一条路,经过 B 或 C 后再进入公共流程。这里的汇合,就是两条可选路线通向同一个地点,不需要等待两路并行任务。
   
   **最关键的问题是:现有插件只会调用 `chain.execute(exchange)`,怎么让它们用上这张图?**
   
   我的想法是,插件不必自己学会看图。导航员把请求交给它时,同时给它一个“继续按钮”,按钮背后已经记好了下一站在哪里。
   
   为了在代码里表达这个按钮,可以定义一个实现 `ShenyuPluginChain` 的 `GraphContinuation`。它首先需要保存两个信息:
   
   ```java
   GraphExecution execution; // 按下按钮后,把请求交给哪位导航员
   String nextNodeId;         // 这位导航员接下来应该前往哪里
   ```
   
   插件仍然照常调用:
   
   ```java
   chain.execute(exchange);
   ```
   
   这个调用就由 `GraphContinuation` 转交给:
   
   ```java
   execution.run(nextNodeId, exchange);
   ```
   
   因此,**插件眼里仍然是熟悉的插件链,执行器手里则是一张完整的图。** A 不需要知道后面有岔路,也不需要认识 
B、C,只需要把处理后的请求交回来,说一句“继续”。
   
   不过,这个按钮还有一个需要明确的地方:**它能不能被重复使用?**
   
   在这里,调用 `chain.execute()` 拿到一个 
`Mono`,可以理解成领取一张任务单;真正订阅它时,任务才启动。同一张任务单如果启动两次,简单地从 `nextNodeId` 
开始执行,就可能把后面的插件再跑一遍。
   
   旧链有一个不断向前移动的游标,像书里的书签;图 continuation 
记住的是固定地址,像“从第十页开始读”。第一次读完以后,再使用它们,结果就可能不同。
   
   为了先验证“一次性继续”的方案,可以给每个 `GraphContinuation` 增加一个运行状态:
   
   ```java
   AtomicBoolean consumed; // 这个按钮是否已经启动过后继执行
   ```
   
   
它就像按钮旁边的一盏灯,第一次真正启动任务时亮起来,后面再次启动就能发现重复使用。这个状态属于本次执行中的按钮,公共地图可以被多个请求共用,不应该保存某个请求是否按过按钮。
   
   **下面的伪代码展示的是“一次性消费、重复则报错”这个候选方案,最终是否采用,需要按照 #7994 / #8015 的兼容契约确定。**
   
   ```java
   class GraphContinuation implements ShenyuPluginChain {
   
       // 当前这次执行的导航员
       private final GraphExecution execution;
   
       // 地图上为当前插件标好的下一站
       private final String nextNodeId;
   
       // 当前按钮是否已经启动过后继执行
       private final AtomicBoolean consumed = new AtomicBoolean(false);
   
       GraphContinuation(GraphExecution execution, String nextNodeId) {
           this.execution = execution;
           this.nextNodeId = nextNodeId;
       }
   
       @Override
       public Mono<Void> execute(ServerWebExchange exchange) {
           return Mono.defer(() -> {
               // 真正订阅时才消费按钮
               if (!consumed.compareAndSet(false, true)) {
                   return Mono.error(
                       new IllegalStateException("Continuation already 
consumed")
                   );
               }
   
               // 带着插件传回来的上下文,前往下一站
               return execution.run(nextNodeId, exchange);
           });
       }
   
       // fork() 的职责在下文说明,此处省略实现
   }
   ```
   
   把 `consumed` 放在 continuation 对象上,是为了让同一个插件多次调用 `execute()`,或者重复订阅它返回的 
`Mono`,都检查同一盏灯。`AtomicBoolean` 则让“检查”和“点亮”成为一个不可拆开的动作,避免两个订阅同时通过。
   
   每次进入一个插件调用,都会为它创建自己的 continuation,所以 A 的按钮用过,不会影响 B 的按钮。
   
   走到插件节点时,交接关系可以简化表示为:
   
   ```java
   ShenyuPlugin plugin = node.plugin();
   
   ShenyuPluginChain next =
       new GraphContinuation(this, node.nextNodeId());
   
   return plugin.execute(exchange, next);
   ```
   
   这里省略了 `skip` 和 hooks 等外围逻辑,重点是说明插件拿到的仍然是它熟悉的 `exchange` 和 `chain`。整个交接过程就像:
   
   ```text
   导航员:到 A 站了,请 A 处理
       ↓
   插件 A:处理好了,拿这个请求继续吧
       ↓
   GraphContinuation:检查按钮状态,并提供下一站地址
       ↓
   导航员:带着当前请求,继续看图前进
   ```
   
   如果 A 缓存命中或鉴权失败,直接返回,没有调用 `chain.execute()`,按钮就没有被使用,后面的流程也不会启动。
   
   
插件也可以继续包住后续执行,观察成功、错误或取消。就像把请求交给下一站之后,它仍然保留了一条回程通道,后面的结果会经过它再往外返回。执行器需要保留这种包装关系,以及现有
 `before/after` hooks 的触发时机。走到图的终点时,也要让外围响应逻辑按原来的顺序收尾。
   
   
对于一次性保护,我希望先验证它对真实插件的影响,再确定最终行为。因为有些插件可能依赖重复调用或重复订阅,直接报错也会改变行为。网络请求内部重试、重新订阅整个入口,以及显式
 `fork()`,也需要分别定义和测试,不能用同一个标记全部限制住。
   
   现有接口里的 `fork()`,可以理解成:**派出另一个导航员,拿着同一版本的地图,带领子请求独立出发。**
   
   在这个初步方案里,子执行从图入口开始,拥有自己的运行状态和 continuation,继承父执行选定的执行器与计划,不再经过全局 
dispatcher。为了避免子调用不断产生新的子调用,还需要记录调用层数,例如 `forkDepth`,并设置上限。
   
   
这样,即使管理员在父请求执行期间发布了新图,父执行和它派出的子执行仍然拿着原来的地图;之后开始的新请求再使用新版本。这里固定的是图的结构和绑定,不代表把插件的所有动态配置也一起冻结。
   
   目前已有一个小型 Reactor 实验,验证了上述思路中的部分行为。正式确定运行时接口前,希望先和 #7994 / #7995 
对齐图模型与执行契约,尤其是 #8015 提出的重复调用、重复订阅和 `fork()` 边界。
   
   在这些设计完善期间,我可以先推进原型和旧链行为测试,再补齐入口分流、真实插件兼容,以及响应完成、错误、取消和 fork 等场景的验证。也愿意和 
@dmsolr 一起协调这些接口边界,麻烦将这个任务分配给我,谢谢!
   
   
[GraphSemanticsProbe.java](https://github.com/user-attachments/files/33907759/GraphSemanticsProbe.java)
   
   
   
   ---
   
   **English version**
   
   I’d like to take #7996. I have an initial approach in mind: **a request 
follows a route map, plugins are the processing stations along the route, and 
the executor acts as the navigator.** The class and field names below are 
provisional and would be aligned with the related contracts.
   
   First, when a request arrives, we need to decide whether to use the existing 
plugin chain or graph execution. To represent this choice, we could introduce 
an `ExecutionDispatcher` and think of it as a guide at the entrance. It selects 
LEGACY or GRAPH based on deployment configuration, with LEGACY as the default.
   
   Once selected, that executor owns the request’s complete plugin pipeline, 
including global processing, authentication, proxying, and response handling. 
Both executors can reuse existing `ShenyuPlugin` implementations while managing 
their execution progress independently.
   
   This choice must remain fixed throughout the request execution. Once the 
journey starts, a graph update or execution error must not send the request 
back to the entrance to run through the legacy chain again. If GRAPH is 
configured but no usable plan is available, execution should be rejected 
explicitly. Publishing a new graph should not switch the execution mode by 
itself.
   
   Next, we need a representation of the route map, integrating with the 
`GraphPlan` from #7995.
   
   To identify the map, we need a `graphId`; to distinguish its revisions, we 
need a `revision`; and to know where the journey begins, we need an 
`entryNodeId`. Each node also needs its own `nodeId`, much like a location on a 
map needs an address so the navigator can find it.
   
   Edges indicate where to go next. For an ordinary plugin node, we could call 
the successor address `nextNodeId`. A conditional node would hold conditions 
and their targets, like a signposted junction that selects one route based on 
the current request. The actual field names would be aligned with the graph 
model.
   
   However, having a plugin name on the map is not enough to execute it. A map 
may identify a service station, but we still need to find the person providing 
the service. We could therefore add a binding step before execution, resolving 
plugin names to existing `ShenyuPlugin` instances. To distinguish this 
ready-to-execute map, let’s provisionally call it a `BoundGraphPlan`.
   
   With the map ready, someone needs to guide the request through it. We could 
represent each execution with a `GraphExecution`, acting as the navigator.
   
   Whenever the request entry publisher is subscribed and execution actually 
begins, it creates its own navigator and pins the plan for that execution. The 
navigator advances through a method such as:
   
   ```java
   run(nodeId, exchange)
   ```
   
   Here, `nodeId` identifies the processing station to visit, and `exchange` 
carries the request context along the route. If a plugin modifies or decorates 
the exchange, subsequent steps should receive the exchange it passes onward.
   
   At a plugin node, the executor preserves the existing `skip` and hook 
behavior, and the plugin uses its own selector/rule mechanism to decide whether 
to process the request. At a conditional node, the executor chooses a route 
using the current exchange.
   
   For example:
   
   ```text
   Plugin A → Condition → Plugin B or Plugin C → Shared downstream flow
   ```
   
   An execution selects only one route at the junction, then reaches the shared 
flow through B or C. Convergence means that the alternative routes lead to the 
same destination; it does not require waiting for parallel branches.
   
   **The key question is how existing plugins, which call 
`chain.execute(exchange)`, can work with this graph.**
   
   My idea is that plugins do not need to learn how to read the map. When the 
navigator hands a request to a plugin, it also provides a “continue button” 
that already knows the next destination.
   
   We could represent this button with a `GraphContinuation` implementing 
`ShenyuPluginChain`. It initially needs two pieces of information:
   
   ```java
   GraphExecution execution; // Which navigator receives control
   String nextNodeId;         // Where that navigator should go next
   ```
   
   The plugin continues to call:
   
   ```java
   chain.execute(exchange);
   ```
   
   `GraphContinuation` forwards that call to:
   
   ```java
   execution.run(nextNodeId, exchange);
   ```
   
   This means **the plugin still sees a familiar plugin chain, while the 
executor holds the complete graph.** A does not need to know about the junction 
or recognize B and C. It simply passes back the processed exchange and says, 
“continue.”
   
   There is one more question about this button: **can it be used more than 
once?**
   
   Here, obtaining a `Mono` from `chain.execute()` is like collecting a work 
order; subscribing to it starts the work. If the same work order is started 
twice, simply executing from `nextNodeId` can run the downstream plugins again.
   
   The legacy chain has a cursor that advances, like a bookmark in a book. A 
graph continuation remembers a fixed address, like an instruction to “start 
reading from page ten.” Reusing them after the first traversal can therefore 
produce different results.
   
   To explore a one-shot continuation policy, we could add a runtime state 
field to each `GraphContinuation`:
   
   ```java
   AtomicBoolean consumed; // Whether successor execution has already started
   ```
   
   Think of it as an indicator beside the button: it lights up when execution 
first starts, allowing later attempts to detect reuse. This state belongs to 
the button in a particular execution. The shared map should not record whether 
an individual request has pressed its button.
   
   **The following pseudocode illustrates one candidate policy: consume once 
and reject subsequent attempts. Whether to adopt it must be determined by the 
compatibility contract in #7994 / #8015.**
   
   ```java
   class GraphContinuation implements ShenyuPluginChain {
   
       // The navigator for this execution
       private final GraphExecution execution;
   
       // The next destination assigned to the current plugin
       private final String nextNodeId;
   
       // Whether this button has already started successor execution
       private final AtomicBoolean consumed = new AtomicBoolean(false);
   
       GraphContinuation(GraphExecution execution, String nextNodeId) {
           this.execution = execution;
           this.nextNodeId = nextNodeId;
       }
   
       @Override
       public Mono<Void> execute(ServerWebExchange exchange) {
           return Mono.defer(() -> {
               // Consume the button only when subscribed
               if (!consumed.compareAndSet(false, true)) {
                   return Mono.error(
                       new IllegalStateException("Continuation already 
consumed")
                   );
               }
   
               // Continue with the exchange supplied by the plugin
               return execution.run(nextNodeId, exchange);
           });
       }
   
       // fork() is discussed below; its implementation is omitted here
   }
   ```
   
   Keeping `consumed` on the continuation object ensures that repeated calls to 
`execute()` and repeated subscriptions to its returned `Mono` check the same 
indicator. `AtomicBoolean` makes checking and setting that indicator a single 
atomic operation, preventing two subscriptions from both passing the check.
   
   Each plugin invocation receives its own continuation, so consuming A’s 
button does not affect B’s button.
   
   At a plugin node, the handoff can be illustrated as:
   
   ```java
   ShenyuPlugin plugin = node.plugin();
   
   ShenyuPluginChain next =
       new GraphContinuation(this, node.nextNodeId());
   
   return plugin.execute(exchange, next);
   ```
   
   This omits the surrounding `skip` and hook logic to focus on the handoff: 
the plugin still receives the familiar `exchange` and `chain`.
   
   ```text
   Navigator: We have reached A; please process this request
       ↓
   Plugin A: Processing is done; continue with this exchange
       ↓
   GraphContinuation: Check the button state and supply the next address
       ↓
   Navigator: Continue through the map with the current exchange
   ```
   
   If A returns directly after a cache hit or authentication failure without 
calling `chain.execute()`, the button remains unused and downstream execution 
does not begin.
   
   Plugins can also keep wrapping downstream execution to observe completion, 
errors, or cancellation. It is like keeping a return channel open after handing 
the request to the next station: downstream results still pass back through the 
plugin. The executor needs to preserve this wrapping relationship and the 
timing of the existing `before/after` hooks. Reaching the graph’s end should 
also allow the surrounding response logic to finish in its original order.
   
   Before settling on one-shot protection, I would validate its impact on 
actual plugins. Some may depend on repeated invocation or resubscription, and 
rejecting those operations would change their behavior. Transport-internal 
retries, resubscription at the execution entry, and explicit `fork()` calls 
also need separate definitions and tests; a single flag should not 
indiscriminately restrict all of them.
   
   The existing `fork()` interface can be understood as **sending out another 
navigator with the same edition of the map to guide a child request 
independently.**
   
   In this initial design, the child execution starts at the graph entry, owns 
its execution state and continuations, and inherits the parent’s selected 
executor and plan without consulting the global dispatcher again. To prevent 
child calls from recursively creating more child calls without bound, we would 
also track a nesting level, such as `forkDepth`, and enforce a limit.
   
   With this arrangement, publishing a new graph while a parent request is 
running does not change the map used by that execution or its forks. Newly 
started requests can use the new version. This pins the graph structure and 
bindings; it does not freeze all dynamic plugin configuration.
   
   I have a small Reactor probe covering some of these behaviors. Before 
finalizing the runtime interfaces, I would like to align with the graph model 
and execution contract in #7994 / #7995, particularly the invocation, 
resubscription, and `fork()` boundaries raised in #8015.
   
   While those designs are being refined, I can continue working on the 
prototype and tests that characterize the legacy chain, then complete the 
dispatcher integration and coverage for real plugin compatibility, response 
completion, errors, cancellation, and forks. I’m also happy to coordinate these 
interface boundaries with @dmsolr. Could you assign this task to me? Thanks!


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to