From 57f17d5939e88c8e643ab49b90869d637765e75a Mon Sep 17 00:00:00 2001 From: Nozilla-X <332770878+Nozilla-X@users.noreply.github.com> Date: Tue, 20 Oct 2026 16:58:12 +0800 Subject: [PATCH] fix(profile-ex): make ProfileExItem creation atomic per IndexId (#10420) ConcurrentBag only makes individual operations safe. GetProfileExItem performed FirstOrDefault and AddProfileEx as separate operations, so concurrent speed tests could both miss and create duplicate entries for the same IndexId. Recheck under a dedicated lock before creating, and replace the bag under the same lock in InitData/ClearAll so creation cannot target a bag that is being swapped. Existing reads and item updates stay lock-free. --- v2rayN/ServiceLib/Manager/ProfileExManager.cs | 24 ++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/v2rayN/ServiceLib/Manager/ProfileExManager.cs b/v2rayN/ServiceLib/Manager/ProfileExManager.cs index 89d81ac7..cd1b94a9 100644 --- a/v2rayN/ServiceLib/Manager/ProfileExManager.cs +++ b/v2rayN/ServiceLib/Manager/ProfileExManager.cs @@ -5,6 +5,7 @@ public class ProfileExManager private static readonly Lazy _instance = new(() => new()); private ConcurrentBag _lstProfileEx = []; private readonly ConcurrentQueue _queIndexIds = new(); + private readonly Lock _profileExLock = new(); public static ProfileExManager Instance => _instance.Value; private static readonly string _tag = "ProfileExHandler"; @@ -27,7 +28,11 @@ public class ProfileExManager { await SQLiteHelper.Instance.ExecuteAsync($"delete from ProfileExItem where indexId not in ( select indexId from ProfileItem )"); - _lstProfileEx = new(await SQLiteHelper.Instance.TableAsync().ToListAsync()); + var lstProfileEx = new ConcurrentBag(await SQLiteHelper.Instance.TableAsync().ToListAsync()); + lock (_profileExLock) + { + _lstProfileEx = lstProfileEx; + } } private void IndexIdEnqueue(string indexId) @@ -109,13 +114,26 @@ public class ProfileExManager private ProfileExItem GetProfileExItem(string? indexId) { - return _lstProfileEx.FirstOrDefault(t => t.IndexId == indexId) ?? AddProfileEx(indexId); + var profileEx = _lstProfileEx.FirstOrDefault(t => t.IndexId == indexId); + if (profileEx is not null) + { + return profileEx; + } + + lock (_profileExLock) + { + profileEx = _lstProfileEx.FirstOrDefault(t => t.IndexId == indexId); + return profileEx ?? AddProfileEx(indexId); + } } public async Task ClearAll() { await SQLiteHelper.Instance.ExecuteAsync($"delete from ProfileExItem "); - _lstProfileEx = []; + lock (_profileExLock) + { + _lstProfileEx = []; + } } public async Task SaveTo()