feat: daily node restart controlled from the admin, connect timings as a default line task [CI]
ci/woodpecker/push/main-amd64-build Pipeline was successful
ci/woodpecker/push/main-amd64-build Pipeline was successful
Agents and Probes exit in order at a configured time (default 04:30 UTC+8, switch and time zone in the admin settings) and the service manager starts them again; a local restart_at or MOSHIRO_RESTART_AT wins. The connect kind (TCP connect and TLS handshake timings) is now a default continuous line template, with a section on the line page.
This commit is contained in:
1 parent
388619b8b6
commit
9bcef56497
37 files changed
+928
-101
No files matched your search
@@ -126,7 +126,8 @@ public static class PlatformCheck
|
||||
/// <summary>处理 Ctrl+C 和 SIGTERM:取消令牌,让各个模块有序退出。</summary>
|
||||
public static class ServiceShell
|
||||
{
|
||||
public static async Task RunAsync(Func<CancellationToken, Task> run, INodeLog log)
|
||||
/// <param name="restart">每天定时有序退出一次(见 <see cref="RestartController"/>),重新拉起由服务管理器负责。</param>
|
||||
public static async Task RunAsync(Func<CancellationToken, Task> run, INodeLog log, RestartController? restart = null, TimeProvider? time = null)
|
||||
{
|
||||
using var cts = new CancellationTokenSource();
|
||||
using PosixSignalRegistration term = PosixSignalRegistration.Create(PosixSignal.SIGTERM, context =>
|
||||
@@ -156,6 +157,7 @@ public static class ServiceShell
|
||||
cts.Cancel();
|
||||
});
|
||||
Task trimmer = TrimHeapAsync(cts.Token);
|
||||
Task scheduled = restart is null ? Task.CompletedTask : WaitForRestartAsync(restart, time ?? TimeProvider.System, log, cts);
|
||||
try
|
||||
{
|
||||
await run(cts.Token);
|
||||
@@ -168,6 +170,38 @@ public static class ServiceShell
|
||||
{
|
||||
await cts.CancelAsync();
|
||||
await trimmer;
|
||||
await scheduled;
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task WaitForRestartAsync(RestartController restart, TimeProvider time, INodeLog log, CancellationTokenSource cts)
|
||||
{
|
||||
while (!cts.IsCancellationRequested)
|
||||
{
|
||||
// 先取令牌、再取设置:中间被改动的话令牌会被取消,下一轮重新排
|
||||
CancellationToken changed = restart.ChangeToken;
|
||||
RestartSchedule? schedule = restart.Current;
|
||||
using CancellationTokenSource wait = CancellationTokenSource.CreateLinkedTokenSource(cts.Token, changed);
|
||||
try
|
||||
{
|
||||
if (schedule is null)
|
||||
{
|
||||
await Task.Delay(Timeout.Infinite, wait.Token);
|
||||
continue;
|
||||
}
|
||||
|
||||
DateTimeOffset next = schedule.NextAfter(time.GetUtcNow() + RestartSchedule.MinimumUptime);
|
||||
log.Info("restart scheduled", ("at", next.ToString("O", System.Globalization.CultureInfo.InvariantCulture)), ("source", restart.IsLocal ? "local" : "panel"));
|
||||
await Task.Delay(next - time.GetUtcNow(), time, wait.Token);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
continue; // 进程在退出,或者设置被改了:循环条件和下一轮会处理
|
||||
}
|
||||
|
||||
log.Info("stopping", ("result", "scheduled_restart"));
|
||||
await cts.CancelAsync();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
namespace Fleet.Hosting;
|
||||
|
||||
/// <summary>
|
||||
/// 当前生效的定时重启:节点本地的设置优先;本地没有写时用面板下发的,面板改了之后立即重新排下一次,不需要重启进程。
|
||||
/// </summary>
|
||||
public sealed class RestartController : IDisposable
|
||||
{
|
||||
private readonly RestartSchedule? _local;
|
||||
private readonly object _gate = new();
|
||||
private RestartSchedule? _remote;
|
||||
private CancellationTokenSource _changed = new();
|
||||
|
||||
public RestartController(RestartSchedule? local = null)
|
||||
{
|
||||
_local = local;
|
||||
}
|
||||
|
||||
/// <summary>本地有设置时为 true:面板下发的不生效。</summary>
|
||||
public bool IsLocal => _local is not null;
|
||||
|
||||
public RestartSchedule? Current
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
return _local ?? _remote;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>当前设置被改动时会被取消的令牌;先取令牌、再取 <see cref="Current"/>,就不会漏掉中间的改动。</summary>
|
||||
public CancellationToken ChangeToken
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
return _changed.Token;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>面板下发的设置;为空表示面板关闭了定时重启。返回 false 表示文本不合法(节点忽略它)。</summary>
|
||||
public bool SetRemote(string? text)
|
||||
{
|
||||
RestartSchedule? schedule = null;
|
||||
if (!string.IsNullOrWhiteSpace(text) && !RestartSchedule.TryParse(text, out schedule))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
CancellationTokenSource previous;
|
||||
lock (_gate)
|
||||
{
|
||||
_remote = schedule;
|
||||
previous = _changed;
|
||||
_changed = new CancellationTokenSource();
|
||||
}
|
||||
|
||||
previous.Cancel();
|
||||
previous.Dispose();
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
_changed.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
using System.Globalization;
|
||||
|
||||
namespace Fleet.Hosting;
|
||||
|
||||
/// <summary>
|
||||
/// 每天固定时刻有序退出一次,由 systemd、容器引擎或 procd 的「总是重启」拉起。写法 <c>HH:mm</c>(按 UTC)、
|
||||
/// <c>HH:mm±hh:mm</c> 或 <c>HH:mm</c> 加 <c>Z</c>:容器里没有时区数据库,所以偏移要写明,例如 <c>04:00+08:00</c>。
|
||||
/// 进程自己退出,不需要任何执行外部命令的能力。
|
||||
/// </summary>
|
||||
public sealed class RestartSchedule
|
||||
{
|
||||
/// <summary>环境变量,设置了就覆盖配置文件里的值。</summary>
|
||||
public const string EnvironmentVariable = "MOSHIRO_RESTART_AT";
|
||||
|
||||
/// <summary>刚启动的一段时间内不会触发:重启之后不会因为时刻还没过去而再重启一次,也避免重启风暴。</summary>
|
||||
public static readonly TimeSpan MinimumUptime = TimeSpan.FromMinutes(10);
|
||||
|
||||
private readonly TimeSpan _time;
|
||||
private readonly TimeSpan _offset;
|
||||
|
||||
private RestartSchedule(TimeSpan time, TimeSpan offset)
|
||||
{
|
||||
_time = time;
|
||||
_offset = offset;
|
||||
}
|
||||
|
||||
public static bool TryParse(string? text, out RestartSchedule? schedule)
|
||||
{
|
||||
schedule = null;
|
||||
string value = text?.Trim() ?? "";
|
||||
// HH:mm,后面可以跟 Z 或 ±hh:mm
|
||||
if (value.Length < 5 || value[2] != ':' ||
|
||||
!TryTwoDigits(value, 0, out int hour) || !TryTwoDigits(value, 3, out int minute) || hour > 23 || minute > 59)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
TimeSpan offset = TimeSpan.Zero;
|
||||
string rest = value[5..];
|
||||
if (rest.Length > 0 && rest != "Z")
|
||||
{
|
||||
if (rest.Length != 6 || rest[0] is not ('+' or '-') || rest[3] != ':' ||
|
||||
!TryTwoDigits(rest, 1, out int offsetHours) || !TryTwoDigits(rest, 4, out int offsetMinutes) || offsetMinutes > 59)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
offset = new TimeSpan(offsetHours, offsetMinutes, 0);
|
||||
if (offset > TimeSpan.FromHours(14))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (rest[0] == '-')
|
||||
{
|
||||
offset = -offset;
|
||||
}
|
||||
}
|
||||
|
||||
schedule = new RestartSchedule(new TimeSpan(hour, minute, 0), offset);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryTwoDigits(string text, int start, out int value)
|
||||
{
|
||||
value = 0;
|
||||
if (!char.IsAsciiDigit(text[start]) || !char.IsAsciiDigit(text[start + 1]))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
value = (text[start] - '0') * 10 + (text[start + 1] - '0');
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 环境变量优先,其次配置文件的值;都没有就不定时重启。写错了抛 <see cref="InvalidOperationException"/>,
|
||||
/// 启动时就能看到,而不是悄悄不生效。
|
||||
/// </summary>
|
||||
public static RestartSchedule? Resolve(string? configured)
|
||||
{
|
||||
string? text = Environment.GetEnvironmentVariable(EnvironmentVariable);
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
{
|
||||
text = configured;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return TryParse(text, out RestartSchedule? schedule)
|
||||
? schedule
|
||||
: throw new InvalidOperationException($"restart_at must look like 04:00, 04:00Z or 04:00+08:00 (got '{text}')");
|
||||
}
|
||||
|
||||
/// <summary>严格晚于 <paramref name="now"/> 的下一个触发时刻。</summary>
|
||||
public DateTimeOffset NextAfter(DateTimeOffset now)
|
||||
{
|
||||
DateTimeOffset local = now.ToOffset(_offset);
|
||||
DateTimeOffset candidate = new DateTimeOffset(local.Year, local.Month, local.Day, 0, 0, 0, _offset) + _time;
|
||||
return candidate <= local ? candidate.AddDays(1) : candidate;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user