From 9bea5ef1c9ba23e66c0e3a7576a4110965234f52 Mon Sep 17 00:00:00 2001
From: guqing <38999863+guqing@users.noreply.github.com>
Date: Mon, 24 Jul 2023 16:22:42 +0800
Subject: [PATCH] fix: inconsistency status occurred during plugin startup due
to optimistic locking conflict (#4275)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
#### What type of PR is this?
/kind improvement
/area core
/area plugin
/milestone 2.7.x
#### What this PR does / why we need it:
修复插件启动成功但更新数据失败而导致插件状态不符合预期的问题
how to test it?
1. 安装一个带 console 页面的插件并停用它
2. 使用 IDEA 在 doStart 方法最后更新数据的地方也就是 https://github.com/halo-dev/halo/blob/834e37cf130f2487fab268c9bc7198555933f0dc/application/src/main/java/run/halo/app/core/extension/reconciler/PluginReconciler.java#L447 处打断点,suspend 勾选为 Thread
3. 启用插件,会执行到断点处
4. 使用如下命令更新数据将 status 删除以模拟乐观锁冲突并清除 status 状态排除干扰
```shell
curl -u admin:admin -X PUT http://localhost:8090/apis/plugin.halo.run/v1alpha1/plugins/{name} --data '替换为 plugin 的 json '
```
5. 放行端点
根据上述步骤先在 main 分支浮现然后在切换到此 PR 对比结果,期望插件的状态为启动成功且 status 数据示例如下:
conditions 有两条会因为乐观锁更新失败一次且entry和stylesheet都有值
```json
{
"phase": "STARTED",
"conditions": [
{
"type": "STARTED",
"status": "TRUE",
"lastTransitionTime": "2023-07-21T07:46:01.274211Z",
"message": "Started successfully",
"reason": "STARTED"
},
{
"type": "FAILED",
"status": "FALSE",
"lastTransitionTime": "2023-07-21T07:46:01.248001Z",
"message": "Failed to update table [extensions]; Version does not match for row with Id [/registry/plugin.halo.run/plugins/PluginBytemd]",
"reason": "UnexpectedState"
}
],
"lastStartTime": "2023-07-21T07:46:01.273625Z",
"entry": "/plugins/PluginBytemd/assets/console/main.js?version=1.1.0-SNAPSHOT",
"stylesheet": "/plugins/PluginBytemd/assets/console/style.css?version=1.1.0-SNAPSHOT",
"logo": "/plugins/PluginBytemd/assets/logo.png?version=1.1.0-SNAPSHOT",
"loadLocation": "file:///Users/guqing/Development/halo-sigs/plugin-bytemd/"
}
```
#### Which issue(s) this PR fixes:
Fixes #4273
#### Does this PR introduce a user-facing change?
```release-note
修复插件启动成功但更新数据失败而导致插件状态不符合预期的问题
```
---
.../reconciler/PluginReconciler.java | 9 +++---
.../reconciler/PluginReconcilerTest.java | 29 +++++++++++++++----
2 files changed, 29 insertions(+), 9 deletions(-)
diff --git a/application/src/main/java/run/halo/app/core/extension/reconciler/PluginReconciler.java b/application/src/main/java/run/halo/app/core/extension/reconciler/PluginReconciler.java
index 441e82a53..cb42c5f06 100644
--- a/application/src/main/java/run/halo/app/core/extension/reconciler/PluginReconciler.java
+++ b/application/src/main/java/run/halo/app/core/extension/reconciler/PluginReconciler.java
@@ -320,11 +320,12 @@ public class PluginReconciler implements Reconciler {
Plugin.PluginStatus status = plugin.statusNonNull();
PluginWrapper pluginWrapper = haloPluginManager.getPlugin(pluginName);
- PluginState pluginState = Optional.ofNullable(pluginWrapper)
- .map(PluginWrapper::getPluginState)
- .orElse(PluginState.FAILED);
+ if (pluginWrapper != null) {
+ pluginWrapper.setPluginState(PluginState.FAILED);
+ pluginWrapper.setFailedException(e);
+ }
- status.setPhase(pluginState);
+ status.setPhase(PluginState.FAILED);
Plugin.PluginStatus oldStatus = JsonUtils.deepCopy(status);
Condition condition = Condition.builder()
diff --git a/application/src/test/java/run/halo/app/core/extension/reconciler/PluginReconcilerTest.java b/application/src/test/java/run/halo/app/core/extension/reconciler/PluginReconcilerTest.java
index 9d6d72094..714cf9b60 100644
--- a/application/src/test/java/run/halo/app/core/extension/reconciler/PluginReconcilerTest.java
+++ b/application/src/test/java/run/halo/app/core/extension/reconciler/PluginReconcilerTest.java
@@ -115,11 +115,7 @@ class PluginReconcilerTest {
// mock start plugin failed
when(extensionClient.fetch(eq(Plugin.class), eq("apples"))).thenReturn(Optional.of(plugin));
- when(haloPluginManager.startPlugin(any())).thenAnswer((Answer) invocation -> {
- // mock plugin real state is started
- when(pluginWrapper.getPluginState()).thenReturn(PluginState.FAILED);
- return PluginState.FAILED;
- });
+ when(haloPluginManager.startPlugin(any())).thenReturn(PluginState.FAILED);
// mock plugin real state is started
when(pluginWrapper.getPluginState()).thenReturn(PluginState.STOPPED);
@@ -497,6 +493,29 @@ class PluginReconcilerTest {
}
}
+ @Test
+ void persistenceFailureStatus() {
+ String name = "fake-plugin";
+ Plugin plugin = new Plugin();
+ Plugin.PluginStatus status = new Plugin.PluginStatus();
+ plugin.setStatus(status);
+ when(extensionClient.fetch(eq(Plugin.class), eq(name)))
+ .thenReturn(Optional.of(plugin));
+ PluginWrapper pluginWrapper = mock(PluginWrapper.class);
+ when(haloPluginManager.getPlugin(eq(name)))
+ .thenReturn(pluginWrapper);
+ Throwable error = mock(Throwable.class);
+ pluginReconciler.persistenceFailureStatus(name, error);
+
+ assertThat(status.getPhase()).isEqualTo(PluginState.FAILED);
+ assertThat(status.getConditions()).hasSize(1);
+ assertThat(status.getConditions().peek().getType())
+ .isEqualTo(PluginState.FAILED.toString());
+
+ verify(pluginWrapper).setPluginState(eq(PluginState.FAILED));
+ verify(pluginWrapper).setFailedException(eq(error));
+ }
+
private ArgumentCaptor doReconcileNeedRequeue() {
ArgumentCaptor pluginCaptor = ArgumentCaptor.forClass(Plugin.class);
doNothing().when(extensionClient).update(pluginCaptor.capture());