Skip to content

Commit 1c459b5

Browse files
committed
TraderHelper.CreateCsvReader: don't wrap StreamReader
FastCsvReader's dispose doesn't touch the wrapped TextReader, so the underlying FileStream stayed open even after Dispose. Use the FastCsvReader(stream, encoding, separator, leaveOpen) ctor directly. Regression test in CsvStorageTests covers the init-then-append flow on LocalFileSystem (where the leaked handle blocks the subsequent MoveFile).
1 parent 7064964 commit 1c459b5

2 files changed

Lines changed: 43 additions & 1 deletion

File tree

Algo/TraderHelper.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1240,7 +1240,7 @@ public static decimal GetPnL(this IPnLManager manager)
12401240
}
12411241

12421242
internal static FastCsvReader CreateCsvReader(this Stream stream, Encoding encoding, bool leaveOpen = true)
1243-
=> new(new StreamReader(stream, encoding, leaveOpen: leaveOpen), StringHelper.RN);
1243+
=> new(stream, encoding, StringHelper.RN, leaveOpen);
12441244

12451245
internal static CsvFileWriter CreateCsvWriter(this Stream stream, Encoding encoding = null, bool leaveOpen = true)
12461246
=> new(new StreamWriter(stream, encoding, leaveOpen: leaveOpen)) { LineSeparator = StringHelper.RN };

Tests/CsvStorageTests.cs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1064,6 +1064,48 @@ public async Task CsvNativeId_Clear()
10641064
AreEqual(0, items.Length);
10651065
}
10661066

1067+
/// <summary>
1068+
/// Init-then-append on the same on-disk CSV must not leak the file handle
1069+
/// from InitAsync's reader, otherwise TryAddAsync's MoveFile fails on
1070+
/// LocalFileSystem. MemoryFileSystem has no real handles, so this one
1071+
/// runs against LocalFileSystem.
1072+
/// </summary>
1073+
[TestMethod]
1074+
public async Task CsvNativeId_Reload_ThenAppend_LocalFs_NoHandleLeak()
1075+
{
1076+
var token = CancellationToken;
1077+
var fs = new LocalFileSystem();
1078+
var path = fs.GetSubTemp("nativeids");
1079+
1080+
var executor1 = CreateExecutor(token);
1081+
var provider1 = new CsvNativeIdStorageProvider(fs, path, executor1);
1082+
await provider1.InitAsync(token);
1083+
var storage1 = provider1.GetStorage("MarketData");
1084+
await storage1.TryAddAsync(new SecurityId { SecurityCode = "AAA", BoardCode = "T" }, 1L, true, token);
1085+
await FlushAsync(executor1, token);
1086+
await provider1.DisposeAsync();
1087+
1088+
// Second provider over the same on-disk file. InitAsync reads the
1089+
// existing CSV — that's the path that used to leak a file handle.
1090+
var executor2 = CreateExecutor(token);
1091+
var provider2 = new CsvNativeIdStorageProvider(fs, path, executor2);
1092+
await provider2.InitAsync(token);
1093+
var storage2 = provider2.GetStorage("MarketData");
1094+
1095+
// Appending an item must trigger WriteItemsToFile → CommitAsync →
1096+
// MoveFile, which on LocalFileSystem requires the existing CSV to be
1097+
// unlocked. With the leaked reader handle this throws IOException
1098+
// "process cannot access the file because it is being used by another
1099+
// process".
1100+
await storage2.TryAddAsync(new SecurityId { SecurityCode = "BBB", BoardCode = "T" }, 2L, true, token);
1101+
await FlushAsync(executor2, token);
1102+
1103+
// Cleanly close — also exercises the dispose chain that surfaced the
1104+
// secondary ObjectDisposedException once the half-broken commit left
1105+
// TransactionFileStream._temp in a bad state.
1106+
await provider2.DisposeAsync();
1107+
}
1108+
10671109
[TestMethod]
10681110
public async Task CsvNativeId_RemoveBySecurityId()
10691111
{

0 commit comments

Comments
 (0)