Add db for backup and memory happiness

This commit is contained in:
jheaps
2025-07-05 18:48:30 -06:00
parent 7a532e52e1
commit 9003d2c4dd
27 changed files with 2682 additions and 245 deletions
@@ -0,0 +1,105 @@
namespace JoshHeaps.Net.Services.Implementations;
public class AutoIpUpdateService(
IConfiguration config,
ILogger<AutoIpUpdateService> log)
: BackgroundService
{
private static readonly TimeSpan CheckInterval = TimeSpan.FromMinutes(5);
private static readonly HttpClient httpClient = new();
public static bool IsEnabled { get; private set; } = false;
protected override async Task ExecuteAsync(CancellationToken stop)
{
IsEnabled = true;
var timer = new PeriodicTimer(CheckInterval);
AAAARecord dnsRecord = await GetDnsRecordAsync();
string lastKnownIp = dnsRecord.Content;
while (await timer.WaitForNextTickAsync(stop))
{
try
{
string currentIp = await GetPublicIpAsync() ?? "";
if (lastKnownIp != currentIp)
{
await UpdateDnsIpAsync(config, dnsRecord, currentIp);
lastKnownIp = currentIp;
}
}
catch (OperationCanceledException) { /* shutting down */ }
catch (Exception ex)
{
log.LogError(ex, "Error while attempting ip update");
}
}
}
private static async Task<string> GetPublicIpAsync()
{
try
{
return await httpClient.GetStringAsync(@"https://api.ipify.org/");
}
catch
{
await Task.Delay(TimeSpan.FromSeconds(10));
Console.WriteLine("Reattempting to grab public ip");
return await GetPublicIpAsync();
}
}
private async Task<AAAARecord> GetDnsRecordAsync()
{
try
{
HttpClient cfClient = new();
cfClient.DefaultRequestHeaders.Add("X-Auth-Email", config["cfEmail"]);
cfClient.DefaultRequestHeaders.Add("X-Auth-Key", config["cfKey"]);
var result = await cfClient.GetAsync(@$"https://api.cloudflare.com/client/v4/zones/{config["zoneId"]}/dns_records");
Console.WriteLine(await result.Content.ReadAsStringAsync());
var records = System.Text.Json.JsonSerializer.Deserialize<RecordList>(await result.Content.ReadAsStringAsync());
return records!.Result[0];
}
catch
{
await Task.Delay(TimeSpan.FromSeconds(10));
Console.WriteLine("Reattempting to grab current ip");
return await GetDnsRecordAsync();
}
}
private static async Task UpdateDnsIpAsync(IConfiguration config, AAAARecord record, string ip)
{
HttpClient cfClient = new();
cfClient.DefaultRequestHeaders.Add("X-Auth-Email", config["cfEmail"]);
cfClient.DefaultRequestHeaders.Add("X-Auth-Key", config["cfKey"]);
object content = new
{
comment = "Update as needed",
content = ip,
name = "@",
proxied = true,
ttl = 3600,
type = "AAAA"
};
var result = await cfClient.PutAsJsonAsync(@$"https://api.cloudflare.com/client/v4/zones/{config["zoneId"]}/dns_records{record.Id}", content);
if (!result.IsSuccessStatusCode)
{
await Task.Delay(TimeSpan.FromSeconds(10));
Console.WriteLine("Reattempting to update ip");
await UpdateDnsIpAsync(config, record, ip);
}
}
record AAAARecord(string Comment, string Content, string Name, string Id);
record RecordList(List<AAAARecord> Result);
}
@@ -4,18 +4,21 @@ using System.Threading.Channels;
namespace JoshHeaps.Net.Services.Implementations;
public class BackgroundTaskQueue : IBackgroundTaskQueue
public class BackgroundTaskQueue
{
private readonly ConcurrentDictionary<int, Task> _runningTasks = new();
private readonly ConcurrentDictionary<Guid, Task> _runningTasks = new();
public void Queue(Func<Task> workItem)
public void Queue(Func<Task> workItem, Guid workId)
{
var task = Task.Run(workItem);
_runningTasks.TryAdd(task.Id, task);
if (_runningTasks.ContainsKey(workId))
return;
task.ContinueWith(t => _runningTasks.TryRemove(t.Id, out _), TaskScheduler.Default);
var task = Task.Run(workItem);
_runningTasks.TryAdd(workId, task);
task.ContinueWith(t => _runningTasks.TryRemove(workId, out _), TaskScheduler.Default);
}
public IReadOnlyCollection<Task> Running => [.. _runningTasks.Values];
public Task WhenAllDone() => Task.WhenAll(Running);
public IReadOnlyDictionary<Guid, Task> Running => _runningTasks;
public Task WhenAllDone() => Task.WhenAll(Running.Values);
}
@@ -1,9 +1,10 @@
using JoshHeaps.Net.Models;
using JoshHeaps.Net.DAL;
using JoshHeaps.Net.Models;
using JoshHeaps.Net.Services.Interfaces;
namespace JoshHeaps.Net.Services.Implementations;
public class ChessService : IChessService
public class ChessService(ChessDbAccess dbAccess) : IChessService
{
public GameState CreateNewGame()
{
@@ -122,7 +123,7 @@ public class ChessService : IChessService
return legalMoves;
}
public MoveResultDto MakeMove(GameState gameState, MoveDto moveDto)
public async Task<MoveResultDto> MakeMove(GameState gameState, MoveDto moveDto)
{
var piece = gameState.Pieces.FirstOrDefault(p => p.Id == moveDto.PieceId);
@@ -145,6 +146,12 @@ public class ChessService : IChessService
var notation = $"{piece.Id}:{piece.Position}->{targetPos}";
gameState.MoveHistory.Add(notation);
gameState.CurrentPlayer = gameState.CurrentPlayer == PieceColor.White
? PieceColor.Black
: PieceColor.White;
await dbAccess.SaveAsync(gameState);
return new MoveResultDto
{
Success = true,
@@ -166,8 +173,8 @@ public class ChessService : IChessService
piece.Position = targetPos;
gs.Board[targetPos.Row, targetPos.Col] = piece;
if (captured != null && captured != piece)
captured.Position = new Position(-1, -1);
if (captured is not null && captured != piece)
captured.Position = new Position(-1, -1); // Remove captured piece from board
bool wasFirstMove = !piece.HasMoved;
piece.HasMoved = true;
@@ -179,10 +186,6 @@ public class ChessService : IChessService
HandlePawnPromotionIfNeeded(piece, moveDto);
UpdateCastlingRights(gs, piece, oldPos);
gs.CurrentPlayer = gs.CurrentPlayer == PieceColor.White
? PieceColor.Black
: PieceColor.White;
}
private static void HandleEnPassantIfNeeded(GameState gs, ChessPiece piece, Position targetPos, ref ChessPiece? captured)
@@ -0,0 +1,44 @@
using JoshHeaps.Net.DAL;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace JoshHeaps.Net.Services.Implementations;
public sealed class GameCleanupService(
IDbContextFactory<ChessDbContext> dbFactory,
ILogger<GameCleanupService> log)
: BackgroundService
{
private static readonly TimeSpan CheckInterval = TimeSpan.FromMinutes(5);
protected override async Task ExecuteAsync(CancellationToken stop)
{
var timer = new PeriodicTimer(CheckInterval);
while (await timer.WaitForNextTickAsync(stop))
{
try
{
await using var db = await dbFactory.CreateDbContextAsync(stop);
var now = DateTime.UtcNow;
var expired = await db.Games
.Where(g => g.LastMoveUtc.AddDays(7) < DateTime.UtcNow)
.ToListAsync(stop);
if (expired.Count == 0) continue;
db.Games.RemoveRange(expired);
await db.SaveChangesAsync(stop);
log.LogInformation("🗑️ Removed {Count} expired games", expired.Count);
}
catch (OperationCanceledException) { /* shutting down */ }
catch (Exception ex)
{
log.LogError(ex, "Error while purging old games");
}
}
}
}
@@ -1,7 +1,9 @@
using JoshHeaps.Net.Hubs;
using JoshHeaps.Net.DAL;
using JoshHeaps.Net.Hubs;
using JoshHeaps.Net.Models;
using JoshHeaps.Net.Services.Interfaces;
using JoshHeaps.Net.Utilities;
using Microsoft.AspNetCore.Rewrite;
using Microsoft.AspNetCore.SignalR;
using System.Diagnostics;
using System.Runtime.InteropServices;
@@ -16,6 +18,12 @@ public sealed class Stockfish : IAsyncDisposable
private readonly Channel<string> _stdout = Channel.CreateUnbounded<string>();
private readonly int _skill;
private static readonly SemaphoreSlim _mutex = new(1, 1);
private static readonly List<Guid> _statesRunning = [];
public bool IsRunning => _p is not null && !_p.HasExited;
public bool InUse { get; set; }
public Stockfish(int skill = 20, int hash = 256)
{
_skill = skill;
@@ -44,7 +52,7 @@ public sealed class Stockfish : IAsyncDisposable
RedirectStandardInput = true,
RedirectStandardOutput = true,
UseShellExecute = false,
CreateNoWindow = true
CreateNoWindow = true,
}
};
@@ -70,7 +78,6 @@ public sealed class Stockfish : IAsyncDisposable
Send("uci");
WaitFor("uciok").GetAwaiter().GetResult();
Send($"setoption name Skill Level value {skill}");
Send($"setoption name Hash value {hash}");
Send("isready");
WaitFor("readyok").GetAwaiter().GetResult();
@@ -109,8 +116,15 @@ public sealed class Stockfish : IAsyncDisposable
_p.Dispose();
}
public async Task MakeMove(GameState state, IHubContext<ChessHub> chessHub, IChessService chessService)
public async Task<bool> MakeMove(GameState state, IHubContext<ChessHub> chessHub, IChessService chessService, ChessDbAccess dbAccess)
{
if (state.ComputerColor != state.CurrentPlayer)
return false;
Send($"setoption name Skill Level value {state.ComputerDifficulty}");
Send("isready");
await WaitFor("readyok");
var move = await GetBestMoveAsync(state.ToFen());
var moveDto = move.ToMoveDto(
@@ -119,8 +133,10 @@ public sealed class Stockfish : IAsyncDisposable
? state.WhitePlayerId
: state.BlackPlayerId);
var result = chessService.MakeMove(state, moveDto);
var result = await chessService.MakeMove(state, moveDto);
await chessHub.Clients.Group(state.GameId.ToString()).SendAsync("ReceiveMoveUpdate", state.GameId.ToString(), moveDto, result);
return true;
}
}
@@ -0,0 +1,62 @@
using JoshHeaps.Net.DAL;
using JoshHeaps.Net.Hubs;
using JoshHeaps.Net.Models;
using JoshHeaps.Net.Services.Interfaces;
using Microsoft.AspNetCore.SignalR;
using System.Collections.Concurrent;
namespace JoshHeaps.Net.Services.Implementations;
public class StockfishManager(IHubContext<ChessHub> chessHub, IChessService chessService, ChessDbAccess dbAccess)
{
private readonly List<Stockfish> _workers = [];
public async Task<bool> Run(GameState state)
{
if (!state.IsVsComputer)
return true;
if (!_workers.Any(x => x.InUse) && _workers.Count < 5)
_workers.Add(new Stockfish(state.ComputerDifficulty));
var worker = _workers.First(x => !x.InUse);
worker.InUse = true;
try
{
await Task.Delay(TimeSpan.FromMilliseconds(500));
return await worker.MakeMove(state, chessHub, chessService, dbAccess);
}
finally
{
worker.InUse = false;
}
}
public async Task<IEnumerable<KeyValuePair<Guid, GameState>>> RunDeliquents(ConcurrentDictionary<Guid, GameState> deliquents)
{
ConcurrentDictionary<Guid, GameState> continuedDeliquents = [];
List<Task> tasks = [];
foreach (var deliquent in deliquents)
{
tasks.Add(Task.Run(async () =>
{
var state = deliquent.Value;
bool result = false;
if (state.IsVsComputer && state.ComputerColor == state.CurrentPlayer)
result = !(await Run(state));
if (result)
continuedDeliquents[deliquent.Key] = deliquent.Value;
}));
}
await Task.WhenAll(tasks);
return continuedDeliquents;
}
}
@@ -2,5 +2,5 @@
public interface IBackgroundTaskQueue
{
void Queue(Func<Task> workItem);
void Queue(Func<Task> workItem, Guid workId);
}
@@ -7,5 +7,5 @@ public interface IChessService
GameState CreateNewGame();
List<(ChessPiece piece, List<Position> moves)> GetAllLegalMoves(GameState gameState);
List<Position> GetLegalMovesForPiece(GameState gameState, string pieceId);
MoveResultDto MakeMove(GameState gameState, MoveDto moveDto);
Task<MoveResultDto> MakeMove(GameState gameState, MoveDto moveDto);
}