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