Skip to content

[Bug] delay/speed test results are not persisted for new ProfileExItem rows - duplicate IndexId enqueue + transactional INSERT rolls back the whole flush #10465

Closed

Description

@alirezauser67-coder

Title

Test results (delay/speed/ip-info) are not persisted in ProfileExItem for new rows ΓÇö UNIQUE constraint failed: ProfileExItem.IndexId rolls back the whole flush batch

Environment

Summary

Since #10388, ProfileExManager.IndexIdEnqueue no longer dedupes (_queIndexIds plain → ConcurrentQueue), relying on the assumption "duplicates are harmless here since the save loop just writes the same row more than once".

That assumption is wrong for the INSERT path. SaveQueueIndexIds() classifies every queued id against a DB snapshot and puts unknown ids into lstInserts, then flushes with a single transactional InsertAllAsync(..., runInTransaction: true). A duplicate id for a not-yet-existing row means the second INSERT throws UNIQUE constraint failed: ProfileExItem.IndexId and sqlite-net RunInTransaction rolls back the entire batch ΓÇö every new row of that flush is silently lost (the exception is only logged by SaveTo's catch; the queue is already drained, so nothing is re-scheduled).

Duplicates are routine, not rare: every successful Real Delay writes the same IndexId twice into the flush window in SpeedtestService.DoRealPing (~lines 501ΓÇô516):

ProfileExManager.Instance.SetTestDelay(it.IndexId, responseTime);           // enqueue #1
if (!_config.UiItem.HideColumnIpInfo && responseTime > 0)
{   var ipInfo = await ConnectionHandler.GetIPInfo(webProxy, ct);
    ProfileExManager.Instance.SetTestIpInfo(it.IndexId, ipStr);  // enqueue #2 ΓÇö same id
}

plus the automatic retest of the failed part (RunRealPingBatchAsync(lstFailed, …) → second SetTestDelay for the same ids, SpeedtestService.cs ~274–287), SetTestMessage (:627), SetSort on import.

Conversely, a SaveTo() that runs while another flush is in progress (test-end flush at RunLoopAsync finally, 20-min TaskManager flush, exit flush at AppExitAsync) can classify an id as INSERT from two snapshots before either inserts it → same rollback.

User-visible symptom (verified on a real installation carrying upstream v7.25.6 code)

"Real Delay" values display during the session but disappear after an app restart. On that machine, ProfileExItem has 34 rows for 138 ProfileItems (75% missing), only 1 positive Delay, and its guiLogs contain repeating:

ProfileExHandler,UNIQUE constraint failed: ProfileExItem.IndexId
    at SQLite.SQLiteConnection.InsertAll ⇒ RunInTransaction
    at ServiceLib.Helper.SQLiteHelper.InsertAllAsync … SqliteHelper.cs:line 28
    at ServiceLib.Manager.ProfileExManager.SaveQueueIndexIds() … ProfileExManager.cs:line 85

including failures at the exit flush. The timing explains why it went under the radar: only failed tests (Delay=-1, the IP-info write skipped → single enqueue) persist, which looks like "results recorded" until one notices inserted success values missing.

Repro:

  1. Fresh profile with no ProfileExItem row (or DELETE FROM ProfileExItem; with the app closed),
  2. successful Real Delay test,
  3. restart the app → value gone,
  4. log shows the UNIQUE constraint failure at run end / exit flush.

Suggested fix

Make persistence idempotent instead of restoring dedupe (dedupe alone still allows overlapping concurrent flushes):

SqliteHelper.cs

public async Task<int> InsertOrReplaceAllAsync(IEnumerable models)
    => await _dbAsync.InsertAllAsync(models, runInTransaction: true, extra: "OR REPLACE").ConfigureAwait(false);

ProfileExManager.SaveQueueIndexIds() ΓÇö drain into HashSet<string>, then upsert all touched rows:

var lstIds = new HashSet<string>();
while (_queIndexIds.TryDequeue(out var id))
    if (id.IsNotEmpty()) lstIds.Add(id);

if (lstIds.Count > 0)
{
    try
    {
        var lstUpserts = new List<ProfileExItem>();
        foreach (var id in lstIds)
        {
            var itemNew = _lstProfileEx?.FirstOrDefault(t => t.IndexId == id);
            if (itemNew is not null) lstUpserts.Add(itemNew);
        }
        if (lstUpserts.Count > 0)
            await SQLiteHelper.Instance.InsertOrReplaceAllAsync(lstUpserts);
    }
    catch (Exception ex) { Logging.SaveLog(_tag, ex); }
}

ProfileExItem has an explicit IndexId primary key (no autoincrement), so INSERT OR REPLACE is row-for-row correct and duplicates/overlapping flushes become harmless.

(I verified the pre-#10388 behavior: the older guard if (indexId.IsNotEmpty() && !_queIndexIds.Contains(indexId)) made plain INSERT safe, which is why results persisted before; a side-by-side old/new data folder on the same machine shows 250 persisted positive delays before vs 1 after.)

Activity

  1. changed the title [-][Bug] delay/speed test results are not persisted for new ProfileExItem rows - duplicate IndexId enqueue + transactional INSERT rolls back the whole flush[/-] [+][Withdrawn] - moved to patterniha/PattN#41 (nothing to do here)[/+] on Oct 26, 2026
  2. changed the title [-][Withdrawn] - moved to patterniha/PattN#41 (nothing to do here)[/-] [+][Bug] delay/speed test results are not persisted for new ProfileExItem rows - duplicate IndexId enqueue + transactional INSERT rolls back the whole flush[/+] on Oct 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions