fix: inconsistency status occurred during plugin startup due to optimistic locking conflict (#4275)

#### 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 方法最后更新数据的地方也就是 834e37cf13/application/src/main/java/run/halo/app/core/extension/reconciler/PluginReconciler.java (L447) 处打断点,suspend 勾选为 Thread
	<img width="404" alt="image" src="https://github.com/halo-dev/halo/assets/38999863/ead0ad2c-65a9-41aa-b2b1-f4fdbc2d2edf">
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
修复插件启动成功但更新数据失败而导致插件状态不符合预期的问题
```
pull/4287/head
guqing 2023-07-24 16:22:42 +08:00 committed by GitHub
parent 84093d8db0
commit 9bea5ef1c9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 29 additions and 9 deletions

View File

@ -320,11 +320,12 @@ public class PluginReconciler implements Reconciler<Request> {
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()

View File

@ -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<PluginState>) 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<Plugin> doReconcileNeedRequeue() {
ArgumentCaptor<Plugin> pluginCaptor = ArgumentCaptor.forClass(Plugin.class);
doNothing().when(extensionClient).update(pluginCaptor.capture());