Einstellungen: erst schreiben, dann uebernehmen
Behebt #534. Die vier schreibenden Endpunkte verschluckten die Ausnahme des Repositories und meldeten anschliessend unbedingt Erfolg. Das Verschlucken war unschoen; die Reihenfolge war gefaehrlich: copyState wurde auch dann geaendert, wenn die Datenbank abgelehnt hatte. Das Modul handelte danach mit Risikolimits, die nirgends standen, und nach dem naechsten Neustart konnte niemand mehr rekonstruieren, womit es gehandelt hatte. Neu geht der Schreibvorgang zuerst, und der Hauptspeicher wird nur nachgezogen, wenn er gelang. Sonst 500 mit Klartext und unveraenderter Zustand. Fehlt die Ablage ganz, ist die Aenderung ebenso rein fluechtig — auch das wird abgelehnt, mit 503. Betroffen: CopyTrading-Kontoeinstellungen, ResolutionFarming-Kontoeinstellungen, Master-Trader anlegen/aendern, Master-Trader loeschen. Dass alle vier denselben Helfer (PersistenceGuard) benutzen, ist der eigentliche Punkt: Aus je eigenem try/catch je Endpunkt ist die Ungleichheit ueberhaupt erst entstanden. Bewusst anders behandelt bleibt das Archivieren eines Supervisor-Berichts: Die Antwort des Agenten steht dort bereits und hat Geld gekostet: sie wegen eines fehlgeschlagenen Archiveintrags zu verwerfen, waere schlimmer als der fehlende Eintrag, und an diesem Schreibvorgang haengt kein Verhalten des Systems. Stillschweigend verschwindet er aber nicht mehr, sondern wird protokolliert. In der Oberflaeche zeigten die Schreibwege bisher nur "HTTP 500" an und verschenkten damit genau die Begruendung, die der Server jetzt mitliefert. errorText() liest sie aus der Antwort. Entscheidung von Richard: ablehnen statt fluechtig uebernehmen. Damit sind waehrend eines Datenbankausfalls keine Aenderungen an den Einstellungen mehr moeglich; das Handeln selbst laeuft weiter. Geprueft auf der Linux-Test-VM mit angehaltener MariaDB: alle drei Schreibwege antworten mit 500 und nennen den Grund, das Limit bleibt bei 11,25 statt auf 99,99 zu springen, der Phantom-Trader entsteht weder im Hauptspeicher noch in der Datenbank, jeder Fehlschlag steht im Protokoll. Nach dem Wiederanlauf speichert es wieder. 556 von 556 Tests bestanden (5 neue). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using PolyTrader.Web.Endpoints;
|
||||
using PolyTraderSharp.Services;
|
||||
using Xunit;
|
||||
|
||||
namespace PolyTrader.Tests.WebUi
|
||||
{
|
||||
/// <summary>
|
||||
/// Der gemeinsame Schreibweg der Einstellungs-Endpunkte (#534).
|
||||
///
|
||||
/// Der Kern ist nicht „meldet einen Fehler", sondern <b>„lässt den Hauptspeicher in Ruhe"</b>:
|
||||
/// Vorher wurde die Änderung auch nach einem gescheiterten Schreibvorgang übernommen und als
|
||||
/// Erfolg gemeldet. Bei Risikolimits heißt das, dass das Modul mit Werten handelt, die nirgends
|
||||
/// stehen und nach einem Neustart nicht mehr rekonstruierbar sind.
|
||||
/// </summary>
|
||||
public class PersistenceGuardTests
|
||||
{
|
||||
private static TerminalLogger Logger() => new();
|
||||
|
||||
[Fact]
|
||||
public void Gelungenes_Schreiben_gibt_keine_Fehlerantwort()
|
||||
{
|
||||
bool geschrieben = false;
|
||||
|
||||
var result = PersistenceGuard.Write(true, () => geschrieben = true, Logger(), "Testwert");
|
||||
|
||||
Assert.Null(result); // null = der Aufrufer darf nachziehen
|
||||
Assert.True(geschrieben);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ausnahme_beim_Schreiben_liefert_eine_Fehlerantwort()
|
||||
{
|
||||
var result = PersistenceGuard.Write(
|
||||
true,
|
||||
() => throw new InvalidOperationException("Verbindung weg"),
|
||||
Logger(),
|
||||
"CopyTrading-Einstellungen fuer Konto 1");
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.IsAssignableFrom<IStatusCodeHttpResult>(result);
|
||||
Assert.Equal(StatusCodes.Status500InternalServerError, ((IStatusCodeHttpResult)result!).StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Fehlende_Ablage_wird_ebenso_abgelehnt()
|
||||
{
|
||||
bool geschrieben = false;
|
||||
|
||||
// Ohne Repository waere die Aenderung rein fluechtig — also dasselbe Problem.
|
||||
var result = PersistenceGuard.Write(false, () => geschrieben = true, Logger(), "Testwert");
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(StatusCodes.Status503ServiceUnavailable, ((IStatusCodeHttpResult)result!).StatusCode);
|
||||
Assert.False(geschrieben);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Das eigentliche Versprechen, nachgestellt am Ablauf des Endpunkts: Erst schreiben, den
|
||||
/// Hauptspeicher nur bei Erfolg nachziehen.
|
||||
/// </summary>
|
||||
[Theory]
|
||||
[InlineData(true)]
|
||||
[InlineData(false)]
|
||||
public void Hauptspeicher_wird_nur_bei_Erfolg_nachgezogen(bool schreibenGelingt)
|
||||
{
|
||||
var hauptspeicher = new Dictionary<int, decimal> { [1] = 5.00m };
|
||||
|
||||
var result = PersistenceGuard.Write(
|
||||
true,
|
||||
() => { if (!schreibenGelingt) throw new InvalidOperationException("DB weg"); },
|
||||
Logger(),
|
||||
"Risikolimit Konto 1");
|
||||
|
||||
if (result == null)
|
||||
hauptspeicher[1] = 7.50m;
|
||||
|
||||
Assert.Equal(schreibenGelingt ? 7.50m : 5.00m, hauptspeicher[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user