mirror of https://github.com/Squidex/squidex.git
34 changed files with 233 additions and 651 deletions
@ -1,213 +0,0 @@ |
|||||
// ==========================================================================
|
|
||||
// Squidex Headless CMS
|
|
||||
// ==========================================================================
|
|
||||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|
||||
// All rights reserved. Licensed under the MIT license.
|
|
||||
// ==========================================================================
|
|
||||
|
|
||||
using System; |
|
||||
using System.Collections.Generic; |
|
||||
using System.Linq; |
|
||||
using System.Threading.Tasks; |
|
||||
using Orleans; |
|
||||
using Orleans.Concurrency; |
|
||||
using Orleans.Runtime; |
|
||||
using Squidex.Infrastructure; |
|
||||
using Squidex.Infrastructure.EventSourcing; |
|
||||
using Squidex.Infrastructure.Log; |
|
||||
using Squidex.Infrastructure.Orleans; |
|
||||
using Squidex.Infrastructure.States; |
|
||||
using Squidex.Infrastructure.Tasks; |
|
||||
|
|
||||
namespace Squidex.Domain.Apps.Entities.Backup |
|
||||
{ |
|
||||
[Reentrant] |
|
||||
public sealed class AppCleanerGrain : GrainOfString, IRemindable, IAppCleanerGrain |
|
||||
{ |
|
||||
private readonly IGrainFactory grainFactory; |
|
||||
private readonly IStore<Guid> store; |
|
||||
private readonly IEventStore eventStore; |
|
||||
private readonly IEnumerable<BackupHandler> handlers; |
|
||||
private readonly ISemanticLog log; |
|
||||
private IPersistence<State> persistence; |
|
||||
private bool isCleaning; |
|
||||
private State state = new State(); |
|
||||
|
|
||||
[CollectionName("AppCleaner")] |
|
||||
public sealed class State |
|
||||
{ |
|
||||
public HashSet<Guid> Apps { get; set; } = new HashSet<Guid>(); |
|
||||
|
|
||||
public HashSet<Guid> FailedApps { get; set; } = new HashSet<Guid>(); |
|
||||
} |
|
||||
|
|
||||
public AppCleanerGrain(IGrainFactory grainFactory, IEventStore eventStore, IStore<Guid> store, IEnumerable<BackupHandler> handlers, ISemanticLog log) |
|
||||
{ |
|
||||
Guard.NotNull(grainFactory, nameof(grainFactory)); |
|
||||
Guard.NotNull(store, nameof(store)); |
|
||||
Guard.NotNull(handlers, nameof(handlers)); |
|
||||
Guard.NotNull(eventStore, nameof(eventStore)); |
|
||||
Guard.NotNull(log, nameof(log)); |
|
||||
|
|
||||
this.grainFactory = grainFactory; |
|
||||
|
|
||||
this.store = store; |
|
||||
this.handlers = handlers; |
|
||||
|
|
||||
this.log = log; |
|
||||
|
|
||||
this.eventStore = eventStore; |
|
||||
} |
|
||||
|
|
||||
public async override Task OnActivateAsync(string key) |
|
||||
{ |
|
||||
await RegisterOrUpdateReminder("Default", TimeSpan.Zero, TimeSpan.FromMinutes(1)); |
|
||||
|
|
||||
persistence = store.WithSnapshots<AppCleanerGrain, State, Guid>(Guid.Empty, s => |
|
||||
{ |
|
||||
state = s; |
|
||||
}); |
|
||||
|
|
||||
await ReadAsync(); |
|
||||
|
|
||||
Clean(); |
|
||||
} |
|
||||
|
|
||||
public Task EnqueueAppAsync(Guid appId) |
|
||||
{ |
|
||||
if (appId != Guid.Empty) |
|
||||
{ |
|
||||
state.Apps.Add(appId); |
|
||||
|
|
||||
Clean(); |
|
||||
} |
|
||||
|
|
||||
return WriteAsync(); |
|
||||
} |
|
||||
|
|
||||
public Task ActivateAsync() |
|
||||
{ |
|
||||
Clean(); |
|
||||
|
|
||||
return TaskHelper.Done; |
|
||||
} |
|
||||
|
|
||||
public Task ReceiveReminder(string reminderName, TickStatus status) |
|
||||
{ |
|
||||
Clean(); |
|
||||
|
|
||||
return TaskHelper.Done; |
|
||||
} |
|
||||
|
|
||||
public Task<CleanerStatus> GetStatusAsync(Guid appId) |
|
||||
{ |
|
||||
if (state.Apps.Contains(appId)) |
|
||||
{ |
|
||||
return Task.FromResult(CleanerStatus.Cleaning); |
|
||||
} |
|
||||
|
|
||||
if (state.FailedApps.Contains(appId)) |
|
||||
{ |
|
||||
return Task.FromResult(CleanerStatus.Failed); |
|
||||
} |
|
||||
|
|
||||
return Task.FromResult(CleanerStatus.Cleaned); |
|
||||
} |
|
||||
|
|
||||
private void Clean() |
|
||||
{ |
|
||||
CleanAsync().Forget(); |
|
||||
} |
|
||||
|
|
||||
private async Task CleanAsync() |
|
||||
{ |
|
||||
if (isCleaning) |
|
||||
{ |
|
||||
return; |
|
||||
} |
|
||||
|
|
||||
isCleaning = true; |
|
||||
try |
|
||||
{ |
|
||||
foreach (var appId in state.Apps.ToList()) |
|
||||
{ |
|
||||
await CleanupAppAsync(appId); |
|
||||
} |
|
||||
} |
|
||||
finally |
|
||||
{ |
|
||||
isCleaning = false; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
private async Task CleanupAppAsync(Guid appId) |
|
||||
{ |
|
||||
using (Profiler.StartSession()) |
|
||||
{ |
|
||||
try |
|
||||
{ |
|
||||
log.LogInformation(w => w |
|
||||
.WriteProperty("action", "cleanApp") |
|
||||
.WriteProperty("status", "started") |
|
||||
.WriteProperty("appId", appId.ToString())); |
|
||||
|
|
||||
await CleanupAppCoreAsync(appId); |
|
||||
|
|
||||
log.LogInformation(w => |
|
||||
{ |
|
||||
w.WriteProperty("action", "cleanApp"); |
|
||||
w.WriteProperty("status", "completed"); |
|
||||
w.WriteProperty("appId", appId.ToString()); |
|
||||
|
|
||||
Profiler.Session?.Write(w); |
|
||||
}); |
|
||||
} |
|
||||
catch (Exception ex) |
|
||||
{ |
|
||||
state.FailedApps.Add(appId); |
|
||||
|
|
||||
log.LogError(ex, w => |
|
||||
{ |
|
||||
w.WriteProperty("action", "cleanApp"); |
|
||||
w.WriteProperty("status", "failed"); |
|
||||
w.WriteProperty("appId", appId.ToString()); |
|
||||
|
|
||||
Profiler.Session?.Write(w); |
|
||||
}); |
|
||||
} |
|
||||
finally |
|
||||
{ |
|
||||
state.Apps.Remove(appId); |
|
||||
|
|
||||
await WriteAsync(); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
private async Task CleanupAppCoreAsync(Guid appId) |
|
||||
{ |
|
||||
using (Profiler.Trace("DeleteEvents")) |
|
||||
{ |
|
||||
await eventStore.DeleteManyAsync("AppId", appId.ToString()); |
|
||||
} |
|
||||
|
|
||||
foreach (var handler in handlers) |
|
||||
{ |
|
||||
using (Profiler.TraceMethod(handler.GetType(), nameof(BackupHandler.RemoveAsync))) |
|
||||
{ |
|
||||
await handler.RemoveAsync(appId); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
private async Task ReadAsync() |
|
||||
{ |
|
||||
await persistence.ReadAsync(); |
|
||||
} |
|
||||
|
|
||||
private async Task WriteAsync() |
|
||||
{ |
|
||||
await persistence.WriteSnapshotAsync(state); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,16 +0,0 @@ |
|||||
// ==========================================================================
|
|
||||
// Squidex Headless CMS
|
|
||||
// ==========================================================================
|
|
||||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|
||||
// All rights reserved. Licensed under the MIT license.
|
|
||||
// ==========================================================================
|
|
||||
|
|
||||
namespace Squidex.Domain.Apps.Entities.Backup |
|
||||
{ |
|
||||
public enum CleanerStatus |
|
||||
{ |
|
||||
Cleaned, |
|
||||
Cleaning, |
|
||||
Failed |
|
||||
} |
|
||||
} |
|
||||
@ -1,44 +0,0 @@ |
|||||
// ==========================================================================
|
|
||||
// Squidex Headless CMS
|
|
||||
// ==========================================================================
|
|
||||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|
||||
// All rights reserved. Licensed under the MIT license.
|
|
||||
// ==========================================================================
|
|
||||
|
|
||||
using System; |
|
||||
using System.Threading.Tasks; |
|
||||
using Orleans; |
|
||||
using Squidex.Domain.Apps.Entities.Apps.Commands; |
|
||||
using Squidex.Infrastructure; |
|
||||
using Squidex.Infrastructure.Commands; |
|
||||
using Squidex.Infrastructure.Orleans; |
|
||||
|
|
||||
namespace Squidex.Domain.Apps.Entities.Backup |
|
||||
{ |
|
||||
public sealed class EnqueueAppToCleanerMiddleware : ICommandMiddleware |
|
||||
{ |
|
||||
private readonly IAppCleanerGrain cleaner; |
|
||||
|
|
||||
public EnqueueAppToCleanerMiddleware(IGrainFactory grainFactory) |
|
||||
{ |
|
||||
Guard.NotNull(grainFactory, nameof(grainFactory)); |
|
||||
|
|
||||
cleaner = grainFactory.GetGrain<IAppCleanerGrain>(SingleGrain.Id); |
|
||||
} |
|
||||
|
|
||||
public async Task HandleAsync(CommandContext context, Func<Task> next) |
|
||||
{ |
|
||||
if (context.IsCompleted) |
|
||||
{ |
|
||||
switch (context.Command) |
|
||||
{ |
|
||||
case ArchiveApp archiveApp: |
|
||||
await cleaner.EnqueueAppAsync(archiveApp.AppId); |
|
||||
break; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
await next(); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,20 +0,0 @@ |
|||||
// ==========================================================================
|
|
||||
// Squidex Headless CMS
|
|
||||
// ==========================================================================
|
|
||||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|
||||
// All rights reserved. Licensed under the MIT license.
|
|
||||
// ==========================================================================
|
|
||||
|
|
||||
using System; |
|
||||
using System.Threading.Tasks; |
|
||||
using Squidex.Infrastructure.Orleans; |
|
||||
|
|
||||
namespace Squidex.Domain.Apps.Entities.Backup |
|
||||
{ |
|
||||
public interface IAppCleanerGrain : IBackgroundGrain |
|
||||
{ |
|
||||
Task EnqueueAppAsync(Guid appId); |
|
||||
|
|
||||
Task<CleanerStatus> GetStatusAsync(Guid appId); |
|
||||
} |
|
||||
} |
|
||||
@ -1,34 +0,0 @@ |
|||||
// ==========================================================================
|
|
||||
// Squidex Headless CMS
|
|
||||
// ==========================================================================
|
|
||||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|
||||
// All rights reserved. Licensed under the MIT license.
|
|
||||
// ==========================================================================
|
|
||||
|
|
||||
using System; |
|
||||
using System.Threading.Tasks; |
|
||||
using Squidex.Domain.Apps.Entities.Backup; |
|
||||
using Squidex.Domain.Apps.Entities.History.Repositories; |
|
||||
using Squidex.Infrastructure; |
|
||||
|
|
||||
namespace Squidex.Domain.Apps.Entities.History |
|
||||
{ |
|
||||
public sealed class BackupHistory : BackupHandler |
|
||||
{ |
|
||||
private readonly IHistoryEventRepository historyEventRepository; |
|
||||
|
|
||||
public override string Name { get; } = "History"; |
|
||||
|
|
||||
public BackupHistory(IHistoryEventRepository historyEventRepository) |
|
||||
{ |
|
||||
Guard.NotNull(historyEventRepository, nameof(historyEventRepository)); |
|
||||
|
|
||||
this.historyEventRepository = historyEventRepository; |
|
||||
} |
|
||||
|
|
||||
public override Task RemoveAsync(Guid appId) |
|
||||
{ |
|
||||
return historyEventRepository.RemoveAsync(appId); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,65 @@ |
|||||
|
<sqx-title message="Restore Backup"></sqx-title> |
||||
|
|
||||
|
<sqx-panel theme="light" desiredWidth="70rem"> |
||||
|
<ng-container title> |
||||
|
Restore Backup |
||||
|
</ng-container> |
||||
|
|
||||
|
<ng-container content> |
||||
|
<ng-container *ngIf="restoreJob; let job"> |
||||
|
<div class="card section"> |
||||
|
<div class="card-header"> |
||||
|
<div class="row no-gutters"> |
||||
|
<div class="col col-auto pr-2"> |
||||
|
<div *ngIf="job.status === 'Started'" class="restore-status restore-status-pending spin"> |
||||
|
<i class="icon-hour-glass"></i> |
||||
|
</div> |
||||
|
<div *ngIf="job.status === 'Failed'" class="restore-status restore-status-failed"> |
||||
|
<i class="icon-exclamation"></i> |
||||
|
</div> |
||||
|
<div *ngIf="job.status === 'Completed'" class="restore-status restore-status-success"> |
||||
|
<i class="icon-checkmark"></i> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<div class="col"> |
||||
|
<h3>Last Restore Operation</h3> |
||||
|
</div> |
||||
|
|
||||
|
<div class="col text-right restore-url"> |
||||
|
{{job.url}} |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="card-body"> |
||||
|
<div *ngFor="let row of job.log"> |
||||
|
{{row}} |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="card-footer text-muted"> |
||||
|
<div class="row"> |
||||
|
<div class="col"> |
||||
|
Started: {{job.started | sqxISODate}} |
||||
|
</div> |
||||
|
<div class="col text-right" *ngIf="job.stopped"> |
||||
|
Stopped: {{job.stopped | sqxISODate}} |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</ng-container> |
||||
|
|
||||
|
<div class="table-items-row"> |
||||
|
<form [formGroup]="restoreForm.form" (submit)="restore()"> |
||||
|
<div class="row no-gutters"> |
||||
|
<div class="col"> |
||||
|
<input class="form-control" name="url" formControlName="url" placeholder="Url to backup" /> |
||||
|
</div> |
||||
|
<div class="col col-auto pl-1"> |
||||
|
<button type="submit" class="btn btn-success" [disabled]="restoreForm.hasNoUrl | async">Restore Backup</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
</form> |
||||
|
</div> |
||||
|
</ng-container> |
||||
|
</sqx-panel> |
||||
@ -1,59 +0,0 @@ |
|||||
<sqx-title message="Restore Backup"></sqx-title> |
|
||||
|
|
||||
<div class="container"> |
|
||||
<ng-container *ngIf="restoreJob; let job"> |
|
||||
<div class="card section"> |
|
||||
<div class="card-header"> |
|
||||
<div class="row no-gutters"> |
|
||||
<div class="col col-auto pr-2"> |
|
||||
<div *ngIf="job.status === 'Started'" class="restore-status restore-status-pending spin"> |
|
||||
<i class="icon-hour-glass"></i> |
|
||||
</div> |
|
||||
<div *ngIf="job.status === 'Failed'" class="restore-status restore-status-failed"> |
|
||||
<i class="icon-exclamation"></i> |
|
||||
</div> |
|
||||
<div *ngIf="job.status === 'Completed'" class="restore-status restore-status-success"> |
|
||||
<i class="icon-checkmark"></i> |
|
||||
</div> |
|
||||
</div> |
|
||||
|
|
||||
<div class="col"> |
|
||||
<h3>Last Restore Operation</h3> |
|
||||
</div> |
|
||||
|
|
||||
<div class="col text-right restore-url"> |
|
||||
{{job.url}} |
|
||||
</div> |
|
||||
</div> |
|
||||
</div> |
|
||||
<div class="card-body"> |
|
||||
<div *ngFor="let row of job.log"> |
|
||||
{{row}} |
|
||||
</div> |
|
||||
</div> |
|
||||
<div class="card-footer text-muted"> |
|
||||
<div class="row"> |
|
||||
<div class="col"> |
|
||||
Started: {{job.started | sqxISODate}} |
|
||||
</div> |
|
||||
<div class="col text-right" *ngIf="job.stopped"> |
|
||||
Stopped: {{job.stopped | sqxISODate}} |
|
||||
</div> |
|
||||
</div> |
|
||||
</div> |
|
||||
</div> |
|
||||
</ng-container> |
|
||||
|
|
||||
<div class="table-items-row"> |
|
||||
<form [formGroup]="restoreForm.form" (submit)="restore()"> |
|
||||
<div class="row no-gutters"> |
|
||||
<div class="col"> |
|
||||
<input class="form-control" name="url" formControlName="url" placeholder="Url to backup" /> |
|
||||
</div> |
|
||||
<div class="col col-auto pl-1"> |
|
||||
<button type="submit" class="btn btn-success" [disabled]="restoreForm.hasNoUrl | async">Restore Backup</button> |
|
||||
</div> |
|
||||
</div> |
|
||||
</form> |
|
||||
</div> |
|
||||
</div> |
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 79 KiB |
Binary file not shown.
Binary file not shown.
@ -1,48 +0,0 @@ |
|||||
// ==========================================================================
|
|
||||
// Squidex Headless CMS
|
|
||||
// ==========================================================================
|
|
||||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|
||||
// All rights reserved. Licensed under the MIT license.
|
|
||||
// ==========================================================================
|
|
||||
|
|
||||
using System; |
|
||||
using System.Threading.Tasks; |
|
||||
using FakeItEasy; |
|
||||
using Orleans; |
|
||||
using Squidex.Domain.Apps.Entities.Apps.Commands; |
|
||||
using Squidex.Infrastructure.Commands; |
|
||||
using Squidex.Infrastructure.Orleans; |
|
||||
using Xunit; |
|
||||
|
|
||||
namespace Squidex.Domain.Apps.Entities.Backup |
|
||||
{ |
|
||||
public class EnqueueAppToCleanerMiddlewareTests |
|
||||
{ |
|
||||
private readonly IGrainFactory grainFactory = A.Fake<IGrainFactory>(); |
|
||||
private readonly ICommandBus commandBus = A.Fake<ICommandBus>(); |
|
||||
private readonly IAppCleanerGrain index = A.Fake<IAppCleanerGrain>(); |
|
||||
private readonly Guid appId = Guid.NewGuid(); |
|
||||
private readonly EnqueueAppToCleanerMiddleware sut; |
|
||||
|
|
||||
public EnqueueAppToCleanerMiddlewareTests() |
|
||||
{ |
|
||||
A.CallTo(() => grainFactory.GetGrain<IAppCleanerGrain>(SingleGrain.Id, null)) |
|
||||
.Returns(index); |
|
||||
|
|
||||
sut = new EnqueueAppToCleanerMiddleware(grainFactory); |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public async Task Should_enqueue_for_cleanup_on_archive() |
|
||||
{ |
|
||||
var context = |
|
||||
new CommandContext(new ArchiveApp { AppId = appId }, commandBus) |
|
||||
.Complete(); |
|
||||
|
|
||||
await sut.HandleAsync(context); |
|
||||
|
|
||||
A.CallTo(() => index.EnqueueAppAsync(appId)) |
|
||||
.MustHaveHappened(); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
Loading…
Reference in new issue