Nach jedem Verbindungsaufbau lief ein weiterer IBKR-Lesethread mit: Die Schleifenbedingung "while (_socket.IsConnected())" ist nach einem Reconnect wieder wahr, weil dasselbe Socket-Objekt neu verbunden wird. Auf der Test-VM waren es nach zwei TWS-Neustarts drei Threads gleichzeitig. Alle warten auf demselben EReaderMonitorSignal, und ein Signal weckt genau einen. Weckt es den falschen, bleibt die Nachricht in der Warteschlange eines anderen Readers liegen. Bei laufend wiederholten Push-Daten faellt das nicht auf, die einmalige Antwort auf reqPositions laeuft in die Frist - der Dienst sieht gesund aus und ist blind. Zeitueberschreitungen je Tag: 1, 3, 215, 96. Jeder Lesethread bekommt jetzt eine Generationsnummer. Veraltete beenden sich beim naechsten Aufwachen und reichen das verbrauchte Signal weiter, damit die Nachricht des Nachfolgers nicht liegen bleibt. Dazu ein Zwischenspeicher fuer die Handelszeiten (#505): dieselbe Auskunft wurde viermal je Stunde und Instrument neu angefragt, obwohl sie sich hoechstens taeglich aendert. Gueltig bis zum Ende des gemeldeten Zeitraums; Fehlschlaege werden bewusst nicht gemerkt, sonst bliebe ein Modul nach einer einzigen Stoerung bis zum Neustart stumm. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
using System.Collections.Concurrent;
|
||||
using IBKRTrader.Core.Logging;
|
||||
using IBKRTrader.Core.Settings;
|
||||
|
||||
@@ -26,6 +27,10 @@ public sealed class IbkrBrokerClient
|
||||
private readonly TimeSpan _requestTimeout;
|
||||
private readonly TimeSpan _orderTimeout;
|
||||
|
||||
// Handelszeiten je Instrument, bis zum Ende des gemeldeten Zeitraums gueltig (#505).
|
||||
private readonly ConcurrentDictionary<string, MarketSchedule> _schedules =
|
||||
new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
public IbkrBrokerClient(SettingsService settings, LoggingService logger)
|
||||
{
|
||||
_logger = logger;
|
||||
@@ -113,11 +118,38 @@ public sealed class IbkrBrokerClient
|
||||
return await _connection!.RequestExecutionsAsync(since, _requestTimeout, ct).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handelszeiten des Instruments, zwischengespeichert bis zum Ende des gemeldeten Zeitraums.
|
||||
///
|
||||
/// <para><b>Warum ueberhaupt ein Zwischenspeicher.</b> Ein Modul im 15-Minuten-Takt fragte sonst
|
||||
/// dieselbe Auskunft viermal je Stunde und Instrument neu ab, obwohl sie sich hoechstens
|
||||
/// taeglich aendert. Jede dieser Anfragen belegt einen Antwortplatz beim Broker und kann in die
|
||||
/// Zeitueberschreitung laufen – Aufwand ohne Ertrag.</para>
|
||||
///
|
||||
/// <para><b>Fehlschlaege werden nicht gemerkt.</b> Ein <c>null</c> heisst „keine Auskunft" und
|
||||
/// gilt beim Aufrufer als geschlossen. Wuerde das haengenbleiben, bliebe ein Modul nach einer
|
||||
/// einzigen Stoerung bis zum Neustart stumm.</para>
|
||||
/// </summary>
|
||||
public async Task<MarketSchedule?> GetScheduleAsync(InstrumentRef instrument, CancellationToken ct = default)
|
||||
{
|
||||
var key = instrument.Key;
|
||||
|
||||
// Gueltig, solange der gemeldete Zeitraum noch in der Zukunft endet. Danach wuerde die
|
||||
// Auskunft still veralten - und "gestern war offen" ist die gefaehrliche Richtung.
|
||||
if (_schedules.TryGetValue(key, out var cached) &&
|
||||
cached.Sessions.Count > 0 &&
|
||||
cached.Sessions[^1].CloseUtc > DateTime.UtcNow)
|
||||
{
|
||||
return cached;
|
||||
}
|
||||
|
||||
if (!await IsReadyAsync(ct).ConfigureAwait(false)) return null;
|
||||
|
||||
return await _connection!.RequestMarketScheduleAsync(instrument, _requestTimeout, ct).ConfigureAwait(false);
|
||||
var schedule = await _connection!.RequestMarketScheduleAsync(instrument, _requestTimeout, ct)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (schedule is not null) _schedules[key] = schedule;
|
||||
return schedule;
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<BrokerOrder>> GetOpenOrdersAsync(CancellationToken ct = default)
|
||||
|
||||
@@ -55,6 +55,9 @@ internal sealed class IbkrConnection : DefaultEWrapper, IDisposable
|
||||
private bool _zoneWarningIssued;
|
||||
private volatile bool _ready;
|
||||
private volatile bool _disposed;
|
||||
// Laufende Nummer des Lesethreads. Jeder Verbindungsaufbau startet einen neuen; die aelteren
|
||||
// erkennen an dieser Nummer, dass sie nicht mehr zustaendig sind, und beenden sich.
|
||||
private int _readerGeneration;
|
||||
private int _nextRequestId = 1000;
|
||||
private int _nextOrderId = -1;
|
||||
private string? _account;
|
||||
@@ -235,29 +238,57 @@ internal sealed class IbkrConnection : DefaultEWrapper, IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Startet den Lesethread fuer die gerade aufgebaute Verbindung.
|
||||
///
|
||||
/// <para><b>Warum hier eine Generationsnummer noetig ist.</b> <see cref="_socket"/> und
|
||||
/// <see cref="_signal"/> ueberleben den Reconnect – wir verbinden dasselbe Socket-Objekt neu.
|
||||
/// Ein Lesethread mit der Bedingung <c>while (_socket.IsConnected())</c> wuerde deshalb nach
|
||||
/// einem Reconnect <b>weiterlaufen</b>, denn die Bedingung ist wieder wahr. Nach zwei Nachtneu-
|
||||
/// starts der TWS liefen am 2026-09-08 drei Lesethreads gleichzeitig (#504).</para>
|
||||
///
|
||||
/// <para>Das ist kein harmloses Leck: Alle warten auf dasselbe
|
||||
/// <see cref="EReaderMonitorSignal"/>, und ein Signal weckt genau einen. Weckt es den falschen,
|
||||
/// bleibt die Nachricht in der Warteschlange eines anderen Readers liegen, bis zufaellig das
|
||||
/// naechste Signal kommt. Was der Broker laufend wiederholt (Konto, Kurse), faellt dadurch nicht
|
||||
/// auf – die einmalige Antwort auf <c>reqPositions</c> oder <c>reqContractDetails</c> laeuft in
|
||||
/// die Zeitueberschreitung. Der Dienst sieht dann gesund aus und ist blind.</para>
|
||||
/// </summary>
|
||||
private void StartReader()
|
||||
{
|
||||
var reader = new EReader(_socket, _signal);
|
||||
var generation = Interlocked.Increment(ref _readerGeneration);
|
||||
var reader = new EReader(_socket, _signal);
|
||||
reader.Start();
|
||||
|
||||
new Thread(() =>
|
||||
{
|
||||
while (_socket.IsConnected())
|
||||
while (_socket.IsConnected() && Volatile.Read(ref _readerGeneration) == generation)
|
||||
{
|
||||
_signal.waitForSignal();
|
||||
|
||||
// Nach dem Aufwachen erneut pruefen: Waehrend wir warteten, kann ein Reconnect
|
||||
// einen neuen Reader gestartet haben. Dann gehoert diese Nachricht ihm.
|
||||
if (Volatile.Read(ref _readerGeneration) != generation)
|
||||
{
|
||||
// Das verbrauchte Signal weitergeben, sonst bliebe die Nachricht des Nachfolgers
|
||||
// bis zum naechsten Eintreffen liegen.
|
||||
_signal.issueSignal();
|
||||
break;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
reader.processMsgs();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (!_disposed)
|
||||
if (!_disposed && Volatile.Read(ref _readerGeneration) == generation)
|
||||
_logger.Warn(LogModule, $"Nachrichten-Reader beendet: {ex.Message}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
})
|
||||
{ IsBackground = true, Name = "IBKR-Reader" }.Start();
|
||||
{ IsBackground = true, Name = $"IBKR-Reader-{generation}" }.Start();
|
||||
}
|
||||
|
||||
private void SafeDisconnect()
|
||||
|
||||
Reference in New Issue
Block a user