Fix the problem that deletable extensions created by plugins cannot be recycled (#4526)

#### What type of PR is this?

/kind bug
/area core
/milestone 2.9.x

#### What this PR does / why we need it:

As I mentioned in <https://github.com/halo-dev/halo/issues/4519>, some extensions which are deletable cannot be recycled by GC. This PR provides an ability to watch scheme changes and recycles deletable extensions.

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

Fixes https://github.com/halo-dev/halo/issues/4519

#### Does this PR introduce a user-facing change?

```release-note
修复因重启后部分可被回收的资源一直处于删除中的状态
```
pull/4530/head
John Niang 2023-08-31 12:12:12 +08:00 committed by GitHub
parent 329b389d60
commit 58eac2e30b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 78 additions and 8 deletions

View File

@ -10,6 +10,7 @@ import run.halo.app.extension.Extension;
import run.halo.app.extension.ExtensionClient; import run.halo.app.extension.ExtensionClient;
import run.halo.app.extension.ExtensionConverter; import run.halo.app.extension.ExtensionConverter;
import run.halo.app.extension.SchemeManager; import run.halo.app.extension.SchemeManager;
import run.halo.app.extension.SchemeWatcherManager;
import run.halo.app.extension.controller.Controller; import run.halo.app.extension.controller.Controller;
import run.halo.app.extension.controller.ControllerBuilder; import run.halo.app.extension.controller.ControllerBuilder;
import run.halo.app.extension.controller.DefaultController; import run.halo.app.extension.controller.DefaultController;
@ -29,12 +30,18 @@ class GcReconciler implements Reconciler<GcRequest> {
private final SchemeManager schemeManager; private final SchemeManager schemeManager;
GcReconciler(ExtensionClient client, ExtensionStoreClient storeClient, private final SchemeWatcherManager schemeWatcherManager;
ExtensionConverter converter, SchemeManager schemeManager) {
GcReconciler(ExtensionClient client,
ExtensionStoreClient storeClient,
ExtensionConverter converter,
SchemeManager schemeManager,
SchemeWatcherManager schemeWatcherManager) {
this.client = client; this.client = client;
this.storeClient = storeClient; this.storeClient = storeClient;
this.converter = converter; this.converter = converter;
this.schemeManager = schemeManager; this.schemeManager = schemeManager;
this.schemeWatcherManager = schemeWatcherManager;
} }
@ -56,7 +63,7 @@ class GcReconciler implements Reconciler<GcRequest> {
@Override @Override
public Controller setupWith(ControllerBuilder builder) { public Controller setupWith(ControllerBuilder builder) {
var queue = new DefaultQueue<GcRequest>(Instant::now, Duration.ofMillis(500)); var queue = new DefaultQueue<GcRequest>(Instant::now, Duration.ofMillis(500));
var synchronizer = new GcSynchronizer(client, queue, schemeManager); var synchronizer = new GcSynchronizer(client, queue, schemeManager, schemeWatcherManager);
return new DefaultController<>( return new DefaultController<>(
"garbage-collector-controller", "garbage-collector-controller",
this, this,

View File

@ -1,10 +1,14 @@
package run.halo.app.extension.gc; package run.halo.app.extension.gc;
import static run.halo.app.extension.Comparators.compareCreationTimestamp;
import java.util.function.Predicate; import java.util.function.Predicate;
import run.halo.app.extension.Extension; import run.halo.app.extension.Extension;
import run.halo.app.extension.ExtensionClient; import run.halo.app.extension.ExtensionClient;
import run.halo.app.extension.Scheme; import run.halo.app.extension.Scheme;
import run.halo.app.extension.SchemeManager; import run.halo.app.extension.SchemeManager;
import run.halo.app.extension.SchemeWatcherManager;
import run.halo.app.extension.SchemeWatcherManager.SchemeRegistered;
import run.halo.app.extension.Watcher; import run.halo.app.extension.Watcher;
import run.halo.app.extension.controller.RequestQueue; import run.halo.app.extension.controller.RequestQueue;
import run.halo.app.extension.controller.Synchronizer; import run.halo.app.extension.controller.Synchronizer;
@ -13,10 +17,10 @@ class GcSynchronizer implements Synchronizer<GcRequest> {
private final ExtensionClient client; private final ExtensionClient client;
private final RequestQueue<GcRequest> queue;
private final SchemeManager schemeManager; private final SchemeManager schemeManager;
private final SchemeWatcherManager schemeWatcherManager;
private boolean disposed = false; private boolean disposed = false;
private boolean started = false; private boolean started = false;
@ -24,11 +28,11 @@ class GcSynchronizer implements Synchronizer<GcRequest> {
private final Watcher watcher; private final Watcher watcher;
GcSynchronizer(ExtensionClient client, RequestQueue<GcRequest> queue, GcSynchronizer(ExtensionClient client, RequestQueue<GcRequest> queue,
SchemeManager schemeManager) { SchemeManager schemeManager, SchemeWatcherManager schemeWatcherManager) {
this.client = client; this.client = client;
this.queue = queue;
this.schemeManager = schemeManager; this.schemeManager = schemeManager;
this.watcher = new GcWatcher(queue); this.watcher = new GcWatcher(queue);
this.schemeWatcherManager = schemeWatcherManager;
} }
@Override @Override
@ -51,10 +55,17 @@ class GcSynchronizer implements Synchronizer<GcRequest> {
return; return;
} }
this.started = true; this.started = true;
this.schemeWatcherManager.register(event -> {
if (event instanceof SchemeRegistered registeredEvent) {
var newScheme = registeredEvent.getNewScheme();
client.list(newScheme.type(), deleted(), compareCreationTimestamp(true))
.forEach(watcher::onDelete);
}
});
client.watch(watcher); client.watch(watcher);
schemeManager.schemes().stream() schemeManager.schemes().stream()
.map(Scheme::type) .map(Scheme::type)
.forEach(type -> client.list(type, deleted(), null) .forEach(type -> client.list(type, deleted(), compareCreationTimestamp(true))
.forEach(watcher::onDelete)); .forEach(watcher::onDelete));
} }

View File

@ -0,0 +1,52 @@
package run.halo.app.extension.gc;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.isA;
import static org.mockito.Mockito.verify;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import run.halo.app.extension.ExtensionClient;
import run.halo.app.extension.SchemeManager;
import run.halo.app.extension.SchemeWatcherManager;
import run.halo.app.extension.SchemeWatcherManager.SchemeWatcher;
@ExtendWith(MockitoExtension.class)
class GcSynchronizerTest {
@Mock
ExtensionClient client;
@Mock
SchemeManager schemeManager;
@Mock
SchemeWatcherManager schemeWatcherManager;
@InjectMocks
GcSynchronizer synchronizer;
@Test
void shouldStartNormally() {
synchronizer.start();
assertFalse(synchronizer.isDisposed());
verify(schemeWatcherManager).register(any(SchemeWatcher.class));
verify(client).watch(isA(GcWatcher.class));
verify(schemeManager).schemes();
}
@Test
void shouldDisposeSuccessfully() {
assertFalse(synchronizer.isDisposed());
synchronizer.dispose();
assertTrue(synchronizer.isDisposed());
}
}