From c436b998b4b1ce3f02af61fb689c4f08bf9aabdb Mon Sep 17 00:00:00 2001 From: Shubham Padkonde Date: Mon, 2 Nov 2026 06:56:34 +0530 Subject: [PATCH] fix: persist repeated profile test results (#10466) --- .../ProfileExManagerPersistenceTests.cs | 92 +++++++++++++++++++ v2rayN/ServiceLib/Helper/SqliteHelper.cs | 5 + v2rayN/ServiceLib/Manager/ProfileExManager.cs | 29 ++---- 3 files changed, 104 insertions(+), 22 deletions(-) create mode 100644 v2rayN/ServiceLib.Tests/Manager/ProfileExManagerPersistenceTests.cs diff --git a/v2rayN/ServiceLib.Tests/Manager/ProfileExManagerPersistenceTests.cs b/v2rayN/ServiceLib.Tests/Manager/ProfileExManagerPersistenceTests.cs new file mode 100644 index 00000000..fda14d9b --- /dev/null +++ b/v2rayN/ServiceLib.Tests/Manager/ProfileExManagerPersistenceTests.cs @@ -0,0 +1,92 @@ +namespace ServiceLib.Tests.Manager; + +public class ProfileExManagerPersistenceTests +{ + [Test] + public async Task SaveTo_NewProfilesWithRepeatedResults_ShouldPersistEveryProfile() + { + SQLiteHelper.Instance.CreateTable(); + var manager = new ProfileExManager(); + var ids = Enumerable.Range(0, 3).Select(_ => Guid.NewGuid().ToString("N")).ToArray(); + + try + { + foreach (var id in ids) + { + manager.SetTestDelay(id, 42); + manager.SetTestSpeed(id, 12.5m); + manager.SetTestMessage(id, "completed"); + manager.SetTestIpInfo(id, "192.0.2.1"); + manager.SetSort(id, 7); + } + + await manager.SaveTo(); + + foreach (var id in ids) + { + var saved = await SQLiteHelper.Instance.TableAsync() + .Where(t => t.IndexId == id).ToListAsync(); + await saved.Count.Should().BeEqualTo(1); + await saved[0].Delay.Should().BeEqualTo(42); + await saved[0].Speed.Should().BeEqualTo(12.5m); + await saved[0].Message.Should().BeEqualTo("completed"); + await saved[0].IpInfo.Should().BeEqualTo("192.0.2.1"); + await saved[0].Sort.Should().BeEqualTo(7); + } + } + finally + { + foreach (var id in ids) + { + await SQLiteHelper.Instance.DeleteAsync(new ProfileExItem { IndexId = id }); + } + } + } + + [Test] + public async Task SaveTo_MixedNewAndExistingProfiles_ShouldInsertAndUpdateResults() + { + SQLiteHelper.Instance.CreateTable(); + var manager = new ProfileExManager(); + var existingId = Guid.NewGuid().ToString("N"); + var newId = Guid.NewGuid().ToString("N"); + + try + { + await SQLiteHelper.Instance.InsertAsync(new ProfileExItem + { + IndexId = existingId, + Delay = 1, + Speed = 2, + }); + manager.SetTestDelay(existingId, 100); + manager.SetTestSpeed(existingId, 20); + manager.SetTestDelay(newId, 200); + manager.SetTestIpInfo(newId, "192.0.2.2"); + + await manager.SaveTo(); + + var existing = await SQLiteHelper.Instance.TableAsync() + .Where(t => t.IndexId == existingId).FirstAsync(); + await existing.Delay.Should().BeEqualTo(100); + await existing.Speed.Should().BeEqualTo(20m); + var inserted = await SQLiteHelper.Instance.TableAsync() + .Where(t => t.IndexId == newId).ToListAsync(); + await inserted.Count.Should().BeEqualTo(1); + await inserted[0].Delay.Should().BeEqualTo(200); + await inserted[0].IpInfo.Should().BeEqualTo("192.0.2.2"); + + manager.SetTestDelay(newId, 300); + await manager.SaveTo(); + var updated = await SQLiteHelper.Instance.TableAsync() + .Where(t => t.IndexId == newId).FirstAsync(); + await updated.Delay.Should().BeEqualTo(300); + await updated.IpInfo.Should().BeEqualTo("192.0.2.2"); + } + finally + { + await SQLiteHelper.Instance.DeleteAsync(new ProfileExItem { IndexId = existingId }); + await SQLiteHelper.Instance.DeleteAsync(new ProfileExItem { IndexId = newId }); + } + } +} diff --git a/v2rayN/ServiceLib/Helper/SqliteHelper.cs b/v2rayN/ServiceLib/Helper/SqliteHelper.cs index 82dd60d3..c57b70fb 100644 --- a/v2rayN/ServiceLib/Helper/SqliteHelper.cs +++ b/v2rayN/ServiceLib/Helper/SqliteHelper.cs @@ -28,6 +28,11 @@ public sealed class SQLiteHelper return await _dbAsync.InsertAllAsync(models, runInTransaction: true).ConfigureAwait(false); } + public async Task ReplaceAllAsync(IEnumerable models) + { + return await _dbAsync.InsertAllAsync(models, extra: "OR REPLACE", runInTransaction: true).ConfigureAwait(false); + } + public async Task InsertAsync(object model) { return await _dbAsync.InsertAsync(model); diff --git a/v2rayN/ServiceLib/Manager/ProfileExManager.cs b/v2rayN/ServiceLib/Manager/ProfileExManager.cs index cd1b94a9..cc0f9a74 100644 --- a/v2rayN/ServiceLib/Manager/ProfileExManager.cs +++ b/v2rayN/ServiceLib/Manager/ProfileExManager.cs @@ -39,15 +39,14 @@ public class ProfileExManager { if (indexId.IsNotEmpty()) { - //Speed tests touch this from many threads; duplicates are harmless here since - //the save loop just writes the same row more than once + //Speed tests touch this from many threads; the save loop deduplicates IDs. _queIndexIds.Enqueue(indexId); } } private async Task SaveQueueIndexIds() { - var lstIds = new List(); + var lstIds = new HashSet(); while (_queIndexIds.TryDequeue(out var id)) { lstIds.Add(id); @@ -55,39 +54,25 @@ public class ProfileExManager if (lstIds.Count > 0) { - var lstExists = await SQLiteHelper.Instance.TableAsync().ToListAsync(); - List lstInserts = []; - List lstUpdates = []; + List lstSaves = []; foreach (var id in lstIds) { - var item = lstExists.FirstOrDefault(t => t.IndexId == id); var itemNew = _lstProfileEx?.FirstOrDefault(t => t.IndexId == id); if (itemNew is null) { continue; } - if (item is not null) - { - lstUpdates.Add(itemNew); - } - else - { - lstInserts.Add(itemNew); - } + lstSaves.Add(itemNew); } try { - if (lstInserts.Count > 0) + if (lstSaves.Count > 0) { - await SQLiteHelper.Instance.InsertAllAsync(lstInserts); - } - - if (lstUpdates.Count > 0) - { - await SQLiteHelper.Instance.UpdateAllAsync(lstUpdates); + //Another save may have inserted these rows since the queue was drained. + await SQLiteHelper.Instance.ReplaceAllAsync(lstSaves); } } catch (Exception ex)