backend: confirm a registered update by itself after a delay when auto update is switched on [CI]
This commit is contained in:
1 parent
4e7a8c2ba3
commit
49e787a7a5
4 files changed
+138
No files matched your search
@@ -52,6 +52,8 @@ public static class ApplicationServiceCollectionExtensions
|
||||
services.AddSingleton<ReleaseService>();
|
||||
services.AddSingleton<ScheduleService>();
|
||||
services.AddSingleton<BackendUpdates>();
|
||||
services.AddSingleton<BackendAutoUpdater>();
|
||||
services.AddHostedService(provider => provider.GetRequiredService<BackendAutoUpdater>());
|
||||
services.AddSingleton<JoinTokenService>();
|
||||
services.AddSingleton<RawBrowserService>();
|
||||
services.AddSingleton<EventService>();
|
||||
|
||||
@@ -21,6 +21,15 @@ public sealed class BackendOptions
|
||||
/// </summary>
|
||||
public string? UpdateMode { get; set; } = Environment.GetEnvironmentVariable("MOSHIRO_UPDATE");
|
||||
|
||||
/// <summary>
|
||||
/// 自动更新(环境变量 <c>MoShiro__AutoUpdate=true</c>):新版本登记之后过了 <see cref="AutoUpdateDelayMinutes"/> 分钟自动确认,和管理员点「更新并重启」是同一条路径
|
||||
/// (先备份数据库,备份失败就不更新)。默认关:Backend 重启会让所有节点断一两分钟。同样需要 <see cref="UpdateMode"/> 是 <c>restart</c>。
|
||||
/// </summary>
|
||||
public bool AutoUpdate { get; set; }
|
||||
|
||||
/// <summary>新版本登记之后等多久再自动更新(分钟):连着推几次的时候只更新到最后一版,也给 CI 里同时发布的 Agent 版本留出时间。</summary>
|
||||
public int AutoUpdateDelayMinutes { get; set; } = 10;
|
||||
|
||||
/// <summary>
|
||||
/// 每个 Agent 都必须有属主(passkey):创建加入码时必须已经建立信任根,并且至少选一把操作员集合里的 passkey 作为属主。
|
||||
/// 属主是节点敏感内容(extension 会话、加密数据)绑定的人,装上时固定在节点本地,面板改不了。默认开;没有信任根的老部署才需要关掉。
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MoShiro.Protocol;
|
||||
|
||||
namespace MoShiro.Backend.Application;
|
||||
|
||||
/// <summary>
|
||||
/// 自动更新(<see cref="BackendOptions.AutoUpdate"/>,默认关):每分钟看一眼面板上登记的 Backend 新版本,登记满 <see cref="BackendOptions.AutoUpdateDelayMinutes"/> 分钟之后
|
||||
/// 替管理员确认。确认走 <see cref="BackendUpdates.ConfirmAsync"/>,所以数据库备份、清单签名、「只更新到登记时的那一版」都和手动更新一样;
|
||||
/// 更新是有序退出,由 Quadlet 单元(<c>Pull=always</c>、<c>Restart=always</c>)拉起新镜像。
|
||||
/// </summary>
|
||||
public sealed partial class BackendAutoUpdater : BackgroundService
|
||||
{
|
||||
private static readonly TimeSpan s_startupDelay = TimeSpan.FromMinutes(2);
|
||||
private static readonly TimeSpan s_interval = TimeSpan.FromMinutes(1);
|
||||
|
||||
private readonly BackendUpdates _updates;
|
||||
private readonly BackendOptions _options;
|
||||
private readonly TimeProvider _time;
|
||||
private readonly ILogger<BackendAutoUpdater> _log;
|
||||
|
||||
public BackendAutoUpdater(BackendUpdates updates, BackendOptions options, TimeProvider time, ILogger<BackendAutoUpdater> log)
|
||||
{
|
||||
_updates = updates;
|
||||
_options = options;
|
||||
_time = time;
|
||||
_log = log;
|
||||
}
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
if (!_options.AutoUpdate)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await Task.Delay(s_startupDelay, _time, stoppingToken);
|
||||
using var timer = new PeriodicTimer(s_interval, _time);
|
||||
do
|
||||
{
|
||||
try
|
||||
{
|
||||
await RunOnceAsync(stoppingToken);
|
||||
}
|
||||
catch (Exception ex) when (ex is not OperationCanceledException)
|
||||
{
|
||||
Failed(ex);
|
||||
}
|
||||
}
|
||||
while (await timer.WaitForNextTickAsync(stoppingToken));
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// 后端正在停止
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>检查一次;确认了更新(Backend 即将有序退出)返回 true。</summary>
|
||||
public async ValueTask<bool> RunOnceAsync(CancellationToken ct)
|
||||
{
|
||||
if (!_options.AutoUpdate || !_updates.Enabled)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
BackendUpdateView view = await _updates.GetAsync(ct);
|
||||
if (view.State != "available" || view.Available is not { } offer)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// 清单的 seq 是发布时间(秒)
|
||||
TimeSpan age = _time.GetUtcNow() - DateTimeOffset.FromUnixTimeSeconds(offer.Seq);
|
||||
if (age < TimeSpan.FromMinutes(Math.Max(0, _options.AutoUpdateDelayMinutes)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Fleet.Core.Result<bool> result = await _updates.ConfirmAsync(offer.Seq, ct);
|
||||
if (!result.IsSuccess)
|
||||
{
|
||||
Refused(offer.Tag, result.Error.Message);
|
||||
return false;
|
||||
}
|
||||
|
||||
AutoConfirmed(offer.Tag, (int)age.TotalMinutes);
|
||||
return true;
|
||||
}
|
||||
|
||||
[LoggerMessage(Level = LogLevel.Information, Message = "auto update: {Tag} was offered {Minutes} minutes ago, confirming it")]
|
||||
private partial void AutoConfirmed(string tag, int minutes);
|
||||
|
||||
[LoggerMessage(Level = LogLevel.Warning, Message = "auto update to {Tag} was not started: {Reason}")]
|
||||
private partial void Refused(string tag, string reason);
|
||||
|
||||
[LoggerMessage(Level = LogLevel.Warning, Message = "the auto update check failed")]
|
||||
private partial void Failed(Exception exception);
|
||||
}
|
||||
@@ -69,6 +69,33 @@ public sealed class BackendUpdateTests : IAsyncLifetime
|
||||
Assert.Equal(HttpStatusCode.Conflict, replay.StatusCode);
|
||||
}
|
||||
|
||||
/// <summary>自动更新默认关;打开之后只在新版本登记满设定的时间才确认,走的是和手动一样的路径(先备份)。</summary>
|
||||
[Fact]
|
||||
public async Task AutoUpdate_IsOffByDefault_AndConfirmsOnlyAfterTheOfferHasAged()
|
||||
{
|
||||
BackendOptions options = _backend.Services.GetRequiredService<BackendOptions>();
|
||||
var updater = _backend.Services.GetRequiredService<BackendAutoUpdater>();
|
||||
using HttpResponseMessage old = await PublishAsync(10); // seq 是发布时间(秒):1970 年,早就过了等待时间
|
||||
Assert.Equal(HttpStatusCode.Created, old.StatusCode);
|
||||
|
||||
Assert.False(await updater.RunOnceAsync(default)); // 默认关
|
||||
Assert.Equal(0, _backend.Restarts.Count);
|
||||
|
||||
options.AutoUpdate = true;
|
||||
options.AutoUpdateDelayMinutes = 10;
|
||||
using HttpResponseMessage fresh = await PublishAsync(TimeProvider.System.GetUtcNow().ToUnixTimeSeconds());
|
||||
Assert.Equal(HttpStatusCode.Created, fresh.StatusCode);
|
||||
Assert.False(await updater.RunOnceAsync(default)); // 刚登记的版本还要等
|
||||
Assert.Equal(0, _backend.Restarts.Count);
|
||||
|
||||
options.AutoUpdateDelayMinutes = 0;
|
||||
Assert.True(await updater.RunOnceAsync(default));
|
||||
Assert.Equal(1, _backend.Restarts.Count);
|
||||
Assert.Single(Directory.GetFiles(Path.Combine(_backend.DataDirectory, "backups"), "pre-update-*.db")); // 先备份
|
||||
Assert.False(await updater.RunOnceAsync(default)); // 已经在更新,不会再确认第二次
|
||||
Assert.Equal(1, _backend.Restarts.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Confirm_BacksUpTheDatabase_ThenExits_AndRejectsASupersededSequence()
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user