fix: unique index conflict issue after backup restoration preventing startup (#6701)

#### What type of PR is this?
/kind bug
/area core
/milestone 2.20.x
/sig docs

#### What this PR does / why we need it:
修复恢复备份后可能会因为与之前的数据冲突导致无法启动的问题

如果恢复时发生不可预知的错误,需要重启之后重新初始化再进行恢复

#### Which issue(s) this PR fixes:
Fixes #6672

#### Does this PR introduce a user-facing change?
```release-note
修复恢复备份后可能会因为与恢复之前存在的数据冲突导致无法启动的问题
```
pull/6771/head^2
guqing 2024-10-07 17:20:50 +08:00 committed by GitHub
parent ea491f2386
commit 04e195f034
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 23 additions and 20 deletions

View File

@ -141,7 +141,14 @@ public class MigrationServiceImpl implements MigrationService, InitializingBean
return Mono.usingWhen( return Mono.usingWhen(
createTempDir("halo-restore-", scheduler), createTempDir("halo-restore-", scheduler),
tempDir -> unpackBackup(content, tempDir) tempDir -> unpackBackup(content, tempDir)
.then(Mono.defer(() -> restoreExtensions(tempDir))) .then(Mono.defer(() ->
// This step skips index verification such as unique index.
// In order to avoid index conflicts after recovery or
// OptimisticLockingFailureException when updating the same record,
// so we need to truncate all extension stores before saving(create or update).
repository.deleteAll()
.then(restoreExtensions(tempDir)))
)
.then(Mono.defer(() -> restoreWorkdir(tempDir))), .then(Mono.defer(() -> restoreWorkdir(tempDir))),
tempDir -> deleteRecursivelyAndSilently(tempDir, scheduler) tempDir -> deleteRecursivelyAndSilently(tempDir, scheduler)
); );
@ -241,13 +248,9 @@ public class MigrationServiceImpl implements MigrationService, InitializingBean
sink.complete(); sink.complete();
}) })
// reset version // reset version
.doOnNext(extensionStore -> extensionStore.setVersion(null)).buffer(100) .doOnNext(extensionStore -> extensionStore.setVersion(null))
// We might encounter OptimisticLockingFailureException when saving extension .buffer(100)
// store, .flatMap(repository::saveAll)
// So we have to delete all extension stores before saving.
.flatMap(extensionStores -> repository.deleteAll(extensionStores)
.thenMany(repository.saveAll(extensionStores))
)
.doOnNext(extensionStore -> log.info("Restored extension store: {}", .doOnNext(extensionStore -> log.info("Restored extension store: {}",
extensionStore.getName())) extensionStore.getName()))
.then(), .then(),

View File

@ -120,7 +120,7 @@ class MigrationServiceImplTest {
expectStore.setVersion(null); expectStore.setVersion(null);
when(haloProperties.getWorkDir()).thenReturn(workdir); when(haloProperties.getWorkDir()).thenReturn(workdir);
when(repository.deleteAll(List.of(expectStore))).thenReturn(Mono.empty()); when(repository.deleteAll()).thenReturn(Mono.empty());
when(repository.saveAll(List.of(expectStore))).thenReturn(Flux.empty()); when(repository.saveAll(List.of(expectStore))).thenReturn(Flux.empty());
var content = DataBufferUtils.read(backupFile, var content = DataBufferUtils.read(backupFile,
@ -132,7 +132,7 @@ class MigrationServiceImplTest {
verify(haloProperties).getWorkDir(); verify(haloProperties).getWorkDir();
verify(repository).deleteAll(List.of(expectStore)); verify(repository).deleteAll();
verify(repository).saveAll(List.of(expectStore)); verify(repository).saveAll(List.of(expectStore));
// make sure the workdir is recovered. // make sure the workdir is recovered.

View File

@ -114,7 +114,9 @@ useQuery({
<ul> <ul>
<li>{{ $t("core.backup.restore.tips.first") }}</li> <li>{{ $t("core.backup.restore.tips.first") }}</li>
<li> <li>
<strong>
{{ $t("core.backup.restore.tips.second") }} {{ $t("core.backup.restore.tips.second") }}
</strong>
</li> </li>
<li> <li>
{{ $t("core.backup.restore.tips.third") }} {{ $t("core.backup.restore.tips.third") }}

View File

@ -86,7 +86,7 @@ const handleClose = () => {
<VModal <VModal
:visible="visible" :visible="visible"
:width="450" :width="450"
:layer-closable="true" :layer-closable="false"
:data-unique-id="uniqueId" :data-unique-id="uniqueId"
@close="handleCancel()" @close="handleCancel()"
> >

View File

@ -1402,14 +1402,12 @@ core:
first: >- first: >-
1. The restore process may last for a long time, please do not refresh 1. The restore process may last for a long time, please do not refresh
the page during this period. the page during this period.
second: >- second: 2. Before performing the restore, all existing data will be cleared. Please ensure that there is no data that needs to be retained.
2. During the restore process, although the existing data will not be
cleaned up, if there is a conflict, the data will be overwritten.
third: >- third: >-
3. After the restore is completed, you need to restart Halo to load 3. After the restore is completed, you need to restart Halo to load
the system resources normally. the system resources normally.
complete: Restore completed, waiting for restart... complete: Restore completed, waiting for restart...
start: Start restore start: I have read the above instructions, start restore
tabs: tabs:
local: local:
label: Upload label: Upload

View File

@ -1302,10 +1302,10 @@ core:
restore: restore:
tips: tips:
first: 1. 恢复过程可能会持续较长时间,期间请勿刷新页面。 first: 1. 恢复过程可能会持续较长时间,期间请勿刷新页面。
second: 2. 在恢复的过程中,虽然已有的数据不会被清理掉,但如果有冲突的数据将被覆盖 second: 2. 在执行恢复之前,会清空现有的所有数据,请确保当前没有需要保留的数据
third: 3. 恢复完成之后需要重启 Halo 才能够正常加载系统资源。 third: 3. 恢复完成之后需要重启 Halo 才能够正常加载系统资源。
complete: 恢复完成,等待重启... complete: 恢复完成,等待重启...
start: 开始恢复 start: 我已阅读上方提示,开始恢复
tabs: tabs:
local: local:
label: 上传 label: 上传

View File

@ -1281,10 +1281,10 @@ core:
restore: restore:
tips: tips:
first: 1. 還原過程可能需要較長時間,期間請勿重新整理頁面。 first: 1. 還原過程可能需要較長時間,期間請勿重新整理頁面。
second: 2. 在還原過程中,雖然已有的資料不會被清除,但若有衝突的資料將被覆蓋 second: 2. 在執行恢復之前,會清空現有的所有數據,請確保當前沒有需要保留的數據
third: 3. 還原完成後需要重新啟動 Halo 才能正常載入系統資源。 third: 3. 還原完成後需要重新啟動 Halo 才能正常載入系統資源。
complete: 還原完成,等待重啟... complete: 還原完成,等待重啟...
start: 開始還原 start: 我已閱讀上方提示,開始恢復
tabs: tabs:
local: local:
label: 上傳 label: 上傳